package service import ( "context" "encoding/json" "testing" "github.com/gochat/gochat/internal/model" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) // ========== Create ========== func TestDashboardAppService_Create_成功(t *testing.T) { db, _, svc := setupDashboardAppService(t) account := createTestAccount(t, db) req := &CreateDashboardAppRequest{ Title: "销售仪表盘", Description: "销售数据看板", Kind: "frame", Content: json.RawMessage(`[{"type":"frame","url":"https://example.com/widget"}]`), } app, err := svc.Create(context.Background(), account.ID, nil, req) require.NoError(t, err) assert.NotZero(t, app.ID) assert.Equal(t, account.ID, app.AccountID) assert.Nil(t, app.UserID) assert.Equal(t, "销售仪表盘", app.Title) assert.Equal(t, "销售数据看板", app.Description) assert.Equal(t, "frame", app.Kind) assert.True(t, *app.Active) assert.Equal(t, json.RawMessage(`[{"type":"frame","url":"https://example.com/widget"}]`), app.Content) } func TestDashboardAppService_Create_带UserID(t *testing.T) { db, _, svc := setupDashboardAppService(t) account := createTestAccount(t, db) user := createTestUser(t, db, account.ID) userID := user.ID req := &CreateDashboardAppRequest{ Title: "个人仪表盘", Content: json.RawMessage(`[{"type":"frame","url":"https://example.com/personal"}]`), } app, err := svc.Create(context.Background(), account.ID, &userID, req) require.NoError(t, err) assert.NotZero(t, app.ID) assert.Equal(t, account.ID, app.AccountID) assert.NotNil(t, app.UserID) assert.Equal(t, user.ID, *app.UserID) } func TestDashboardAppService_Create_空Content(t *testing.T) { db, _, svc := setupDashboardAppService(t) account := createTestAccount(t, db) req := &CreateDashboardAppRequest{ Title: "空内容仪表盘", Content: json.RawMessage(""), } app, err := svc.Create(context.Background(), account.ID, nil, req) require.NoError(t, err) assert.NotZero(t, app.ID) assert.Equal(t, "空内容仪表盘", app.Title) // Content should default to [] assert.Equal(t, json.RawMessage("[]"), app.Content) } func TestDashboardAppService_Create_设置ActiveFalse(t *testing.T) { db, _, svc := setupDashboardAppService(t) account := createTestAccount(t, db) active := false req := &CreateDashboardAppRequest{ Title: "禁用仪表盘", Active: &active, } app, err := svc.Create(context.Background(), account.ID, nil, req) require.NoError(t, err) assert.False(t, *app.Active) } func TestDashboardAppService_Create_无效Content(t *testing.T) { db, _, svc := setupDashboardAppService(t) account := createTestAccount(t, db) req := &CreateDashboardAppRequest{ Title: "无效内容", Content: json.RawMessage(`{"not_array":true}`), } app, err := svc.Create(context.Background(), account.ID, nil, req) assert.Error(t, err) assert.Nil(t, app) // Error should mention "must be a JSON array" assert.Contains(t, err.Error(), "JSON array") } func TestDashboardAppService_Create_无效WidgetURL(t *testing.T) { db, _, svc := setupDashboardAppService(t) account := createTestAccount(t, db) req := &CreateDashboardAppRequest{ Title: "无效URL", Content: json.RawMessage(`[{"type":"frame","url":"ftp://bad.com"}]`), } app, err := svc.Create(context.Background(), account.ID, nil, req) assert.Error(t, err) assert.Nil(t, app) assert.Contains(t, err.Error(), "http/https") } func TestDashboardAppService_Create_无效WidgetType(t *testing.T) { db, _, svc := setupDashboardAppService(t) account := createTestAccount(t, db) req := &CreateDashboardAppRequest{ Title: "无效类型", Content: json.RawMessage(`[{"type":"link","url":"https://example.com"}]`), } app, err := svc.Create(context.Background(), account.ID, nil, req) assert.Error(t, err) assert.Nil(t, app) assert.Contains(t, err.Error(), "must be 'frame'") } // ========== GetByID ========== func TestDashboardAppService_GetByID_成功(t *testing.T) { db, _, svc := setupDashboardAppService(t) account := createTestAccount(t, db) created := createTestDashboardApp(t, db, account.ID) app, err := svc.GetByID(context.Background(), created.ID) require.NoError(t, err) assert.Equal(t, created.ID, app.ID) assert.Equal(t, created.Title, app.Title) assert.Equal(t, created.AccountID, app.AccountID) assert.Equal(t, "frame", app.Kind) assert.True(t, *app.Active) } func TestDashboardAppService_GetByID_不存在(t *testing.T) { _, _, svc := setupDashboardAppService(t) app, err := svc.GetByID(context.Background(), 99999) assert.Error(t, err) assert.Nil(t, app) } // ========== Update ========== func TestDashboardAppService_Update_修改Title(t *testing.T) { db, _, svc := setupDashboardAppService(t) account := createTestAccount(t, db) created := createTestDashboardApp(t, db, account.ID) req := &UpdateDashboardAppRequest{ Title: "更新后的标题", Content: json.RawMessage(""), } app, err := svc.Update(context.Background(), created.ID, req) require.NoError(t, err) assert.Equal(t, "更新后的标题", app.Title) // Content unchanged because req.Content is "" assert.Equal(t, created.Content, app.Content) } func TestDashboardAppService_Update_修改Content(t *testing.T) { db, _, svc := setupDashboardAppService(t) account := createTestAccount(t, db) created := createTestDashboardApp(t, db, account.ID) req := &UpdateDashboardAppRequest{ Title: "", Content: json.RawMessage(`[{"type":"frame","url":"https://example.com/kpi"}]`), } app, err := svc.Update(context.Background(), created.ID, req) require.NoError(t, err) // Title unchanged because req.Title is "" assert.Equal(t, created.Title, app.Title) assert.Equal(t, json.RawMessage(`[{"type":"frame","url":"https://example.com/kpi"}]`), app.Content) } func TestDashboardAppService_Update_修改Description和Active(t *testing.T) { db, _, svc := setupDashboardAppService(t) account := createTestAccount(t, db) created := createTestDashboardApp(t, db, account.ID) active := false req := &UpdateDashboardAppRequest{ Description: "新描述", Active: &active, } app, err := svc.Update(context.Background(), created.ID, req) require.NoError(t, err) assert.Equal(t, "新描述", app.Description) assert.False(t, *app.Active) // Title unchanged assert.Equal(t, created.Title, app.Title) } func TestDashboardAppService_Update_不存在(t *testing.T) { _, _, svc := setupDashboardAppService(t) req := &UpdateDashboardAppRequest{ Title: "不存在", Content: json.RawMessage(""), } app, err := svc.Update(context.Background(), 99999, req) assert.Error(t, err) assert.Nil(t, app) } // ========== Patch (partial update via Update) ========== func TestDashboardAppService_Patch_只修改Active(t *testing.T) { db, _, svc := setupDashboardAppService(t) account := createTestAccount(t, db) created := createTestDashboardApp(t, db, account.ID) active := false req := &UpdateDashboardAppRequest{ Active: &active, } app, err := svc.Update(context.Background(), created.ID, req) require.NoError(t, err) assert.False(t, *app.Active) // All other fields unchanged assert.Equal(t, created.Title, app.Title) assert.Equal(t, created.Content, app.Content) assert.Equal(t, created.Description, app.Description) } // ========== Delete ========== func TestDashboardAppService_Delete_成功(t *testing.T) { db, _, svc := setupDashboardAppService(t) account := createTestAccount(t, db) created := createTestDashboardApp(t, db, account.ID) err := svc.Delete(context.Background(), created.ID) require.NoError(t, err) // Verify deletion — GetByID should fail app, err := svc.GetByID(context.Background(), created.ID) assert.Error(t, err) assert.Nil(t, app) } func TestDashboardAppService_Delete_不存在(t *testing.T) { _, _, svc := setupDashboardAppService(t) // Deleting non-existent ID should not error (gorm soft-delete with non-existent ID is OK) err := svc.Delete(context.Background(), 99999) assert.NoError(t, err) } // ========== ListByAccount ========== func TestDashboardAppService_ListByAccount_成功(t *testing.T) { db, _, svc := setupDashboardAppService(t) account := createTestAccount(t, db) // Create 3 dashboard apps for the same account for i := 0; i < 3; i++ { createTestDashboardApp(t, db, account.ID, func(app *model.DashboardApp) { app.Title = "仪表盘-" + string(rune('A'+i)) }) } apps, count, err := svc.ListByAccount(context.Background(), account.ID, 1, 10) require.NoError(t, err) assert.Equal(t, int64(3), count) assert.Len(t, apps, 3) } func TestDashboardAppService_ListByAccount_分页(t *testing.T) { db, _, svc := setupDashboardAppService(t) account := createTestAccount(t, db) // Create 5 dashboard apps for i := 0; i < 5; i++ { createTestDashboardApp(t, db, account.ID) } // Page 1: perPage=2 → 2 items, total count=5 apps, count, err := svc.ListByAccount(context.Background(), account.ID, 1, 2) require.NoError(t, err) assert.Equal(t, int64(5), count) assert.Len(t, apps, 2) // Page 2: perPage=2 → 2 items apps, count, err = svc.ListByAccount(context.Background(), account.ID, 2, 2) require.NoError(t, err) assert.Equal(t, int64(5), count) assert.Len(t, apps, 2) // Page 3: perPage=2 → 1 item apps, count, err = svc.ListByAccount(context.Background(), account.ID, 3, 2) require.NoError(t, err) assert.Equal(t, int64(5), count) assert.Len(t, apps, 1) } func TestDashboardAppService_ListByAccount_空列表(t *testing.T) { db, _, svc := setupDashboardAppService(t) account := createTestAccount(t, db) apps, count, err := svc.ListByAccount(context.Background(), account.ID, 1, 10) require.NoError(t, err) assert.Equal(t, int64(0), count) assert.Len(t, apps, 0) } func TestDashboardAppService_ListByAccount_不同账户隔离(t *testing.T) { db, _, svc := setupDashboardAppService(t) account1 := createTestAccount(t, db) account2 := createTestAccount(t, db) createTestDashboardApp(t, db, account1.ID) createTestDashboardApp(t, db, account1.ID) createTestDashboardApp(t, db, account2.ID) // account1 should have 2 apps, count, err := svc.ListByAccount(context.Background(), account1.ID, 1, 10) require.NoError(t, err) assert.Equal(t, int64(2), count) assert.Len(t, apps, 2) // account2 should have 1 apps, count, err = svc.ListByAccount(context.Background(), account2.ID, 1, 10) require.NoError(t, err) assert.Equal(t, int64(1), count) assert.Len(t, apps, 1) } // ========== ListActiveByAccount ========== func TestDashboardAppService_ListActiveByAccount_只返回Active(t *testing.T) { db, _, svc := setupDashboardAppService(t) account := createTestAccount(t, db) // Create 2 active apps createTestDashboardApp(t, db, account.ID) createTestDashboardApp(t, db, account.ID) // Create 1 inactive app createTestDashboardApp(t, db, account.ID, func(app *model.DashboardApp) { app.Active = model.BoolPtr(false) }) apps, count, err := svc.ListActiveByAccount(context.Background(), account.ID, 1, 10) require.NoError(t, err) assert.Equal(t, int64(2), count) assert.Len(t, apps, 2) for _, a := range apps { assert.True(t, *a.Active) } } // ========== Search ========== func TestDashboardAppService_Search_按标题搜索(t *testing.T) { db, _, svc := setupDashboardAppService(t) account := createTestAccount(t, db) // Create apps with different titles createTestDashboardApp(t, db, account.ID, func(app *model.DashboardApp) { app.Title = "销售仪表盘" }) createTestDashboardApp(t, db, account.ID, func(app *model.DashboardApp) { app.Title = "客服监控面板" }) createTestDashboardApp(t, db, account.ID, func(app *model.DashboardApp) { app.Title = "运营数据看板" }) apps, count, err := svc.Search(context.Background(), account.ID, "仪表盘", 1, 10) require.NoError(t, err) assert.Equal(t, int64(1), count) assert.Len(t, apps, 1) assert.Equal(t, "销售仪表盘", apps[0].Title) } func TestDashboardAppService_Search_空结果(t *testing.T) { db, _, svc := setupDashboardAppService(t) account := createTestAccount(t, db) createTestDashboardApp(t, db, account.ID, func(app *model.DashboardApp) { app.Title = "客服监控" }) apps, count, err := svc.Search(context.Background(), account.ID, "不存在", 1, 10) require.NoError(t, err) assert.Equal(t, int64(0), count) assert.Len(t, apps, 0) } func TestDashboardAppService_Search_账户隔离(t *testing.T) { db, _, svc := setupDashboardAppService(t) account1 := createTestAccount(t, db) account2 := createTestAccount(t, db) createTestDashboardApp(t, db, account1.ID, func(app *model.DashboardApp) { app.Title = "仪表盘A" }) createTestDashboardApp(t, db, account2.ID, func(app *model.DashboardApp) { app.Title = "仪表盘B" }) // Search in account1 should only find account1's apps apps, count, err := svc.Search(context.Background(), account1.ID, "仪表盘", 1, 10) require.NoError(t, err) assert.Equal(t, int64(1), count) assert.Len(t, apps, 1) assert.Equal(t, "仪表盘A", apps[0].Title) } // ========== Widget Management ========== func TestDashboardAppService_AddWidget_成功(t *testing.T) { db, _, svc := setupDashboardAppService(t) account := createTestAccount(t, db) created := createTestDashboardApp(t, db, account.ID) req := &AddWidgetRequest{ Type: "frame", URL: "https://example.com/new-widget", Name: "新Widget", } app, err := svc.AddWidget(context.Background(), created.ID, req) require.NoError(t, err) var widgets []model.DashboardWidget err = json.Unmarshal(app.Content, &widgets) require.NoError(t, err) // Should have original widget + new widget assert.Len(t, widgets, 2) lastWidget := widgets[len(widgets)-1] assert.Equal(t, "frame", lastWidget.Type) assert.Equal(t, "https://example.com/new-widget", lastWidget.URL) assert.Equal(t, "新Widget", lastWidget.Name) } func TestDashboardAppService_AddWidget_到空Content(t *testing.T) { db, _, svc := setupDashboardAppService(t) account := createTestAccount(t, db) // Create app with empty content created := createTestDashboardApp(t, db, account.ID, func(app *model.DashboardApp) { app.Content = json.RawMessage(`[]`) }) req := &AddWidgetRequest{ Type: "frame", URL: "https://example.com/widget", } app, err := svc.AddWidget(context.Background(), created.ID, req) require.NoError(t, err) var widgets []model.DashboardWidget err = json.Unmarshal(app.Content, &widgets) require.NoError(t, err) assert.Len(t, widgets, 1) assert.Equal(t, "https://example.com/widget", widgets[0].URL) } func TestDashboardAppService_AddWidget_无效Type(t *testing.T) { db, _, svc := setupDashboardAppService(t) account := createTestAccount(t, db) created := createTestDashboardApp(t, db, account.ID) req := &AddWidgetRequest{ Type: "link", URL: "https://example.com/widget", } app, err := svc.AddWidget(context.Background(), created.ID, req) assert.Error(t, err) assert.Nil(t, app) assert.Contains(t, err.Error(), "must be 'frame'") } func TestDashboardAppService_AddWidget_无效URL(t *testing.T) { db, _, svc := setupDashboardAppService(t) account := createTestAccount(t, db) created := createTestDashboardApp(t, db, account.ID) req := &AddWidgetRequest{ Type: "frame", URL: "ftp://bad.com", } app, err := svc.AddWidget(context.Background(), created.ID, req) assert.Error(t, err) assert.Nil(t, app) assert.Contains(t, err.Error(), "http/https") } func TestDashboardAppService_GetWidgets_成功(t *testing.T) { db, _, svc := setupDashboardAppService(t) account := createTestAccount(t, db) created := createTestDashboardApp(t, db, account.ID) widgets, err := svc.GetWidgets(context.Background(), created.ID) require.NoError(t, err) assert.Len(t, widgets, 1) assert.Equal(t, "frame", widgets[0].Type) } func TestDashboardAppService_GetWidgets_空Content(t *testing.T) { db, _, svc := setupDashboardAppService(t) account := createTestAccount(t, db) created := createTestDashboardApp(t, db, account.ID, func(app *model.DashboardApp) { app.Content = json.RawMessage(`[]`) }) widgets, err := svc.GetWidgets(context.Background(), created.ID) require.NoError(t, err) assert.Len(t, widgets, 0) } func TestDashboardAppService_GetWidgets_不存在(t *testing.T) { _, _, svc := setupDashboardAppService(t) widgets, err := svc.GetWidgets(context.Background(), 99999) assert.Error(t, err) assert.Nil(t, widgets) } func TestDashboardAppService_UpdateWidget_成功(t *testing.T) { db, _, svc := setupDashboardAppService(t) account := createTestAccount(t, db) // Create app with 2 widgets created := createTestDashboardApp(t, db, account.ID, func(app *model.DashboardApp) { app.Content = json.RawMessage(`[{"type":"frame","url":"https://a.com"},{"type":"frame","url":"https://b.com"}]`) }) req := &UpdateWidgetRequest{ URL: "https://updated.com", Name: "更新名称", } app, err := svc.UpdateWidget(context.Background(), created.ID, 0, req) require.NoError(t, err) var widgets []model.DashboardWidget err = json.Unmarshal(app.Content, &widgets) require.NoError(t, err) assert.Len(t, widgets, 2) assert.Equal(t, "https://updated.com", widgets[0].URL) assert.Equal(t, "更新名称", widgets[0].Name) // Second widget unchanged assert.Equal(t, "https://b.com", widgets[1].URL) } func TestDashboardAppService_UpdateWidget_越界(t *testing.T) { db, _, svc := setupDashboardAppService(t) account := createTestAccount(t, db) created := createTestDashboardApp(t, db, account.ID) req := &UpdateWidgetRequest{ URL: "https://updated.com", } app, err := svc.UpdateWidget(context.Background(), created.ID, 5, req) assert.Error(t, err) assert.Nil(t, app) assert.Contains(t, err.Error(), "out of range") } func TestDashboardAppService_RemoveWidget_成功(t *testing.T) { db, _, svc := setupDashboardAppService(t) account := createTestAccount(t, db) // Create app with 2 widgets created := createTestDashboardApp(t, db, account.ID, func(app *model.DashboardApp) { app.Content = json.RawMessage(`[{"type":"frame","url":"https://a.com"},{"type":"frame","url":"https://b.com"}]`) }) app, err := svc.RemoveWidget(context.Background(), created.ID, 0) require.NoError(t, err) var widgets []model.DashboardWidget err = json.Unmarshal(app.Content, &widgets) require.NoError(t, err) assert.Len(t, widgets, 1) // Only second widget remains assert.Equal(t, "https://b.com", widgets[0].URL) } func TestDashboardAppService_RemoveWidget_越界(t *testing.T) { db, _, svc := setupDashboardAppService(t) account := createTestAccount(t, db) created := createTestDashboardApp(t, db, account.ID) app, err := svc.RemoveWidget(context.Background(), created.ID, 5) assert.Error(t, err) assert.Nil(t, app) assert.Contains(t, err.Error(), "out of range") } func TestDashboardAppService_RemoveWidget_最后一个(t *testing.T) { db, _, svc := setupDashboardAppService(t) account := createTestAccount(t, db) created := createTestDashboardApp(t, db, account.ID) app, err := svc.RemoveWidget(context.Background(), created.ID, 0) require.NoError(t, err) var widgets []model.DashboardWidget err = json.Unmarshal(app.Content, &widgets) require.NoError(t, err) assert.Len(t, widgets, 0) } // ========== ValidateContent (model-level) ========== func TestDashboardAppService_Create_验证ValidContent(t *testing.T) { db, _, svc := setupDashboardAppService(t) account := createTestAccount(t, db) // Valid: array of frame widgets with http URLs req := &CreateDashboardAppRequest{ Title: "验证通过", Content: json.RawMessage(`[{"type":"frame","url":"https://example.com"},{"type":"frame","url":"http://other.com"}]`), } app, err := svc.Create(context.Background(), account.ID, nil, req) require.NoError(t, err) assert.NotZero(t, app.ID) } func TestDashboardAppService_Create_验证空数组(t *testing.T) { db, _, svc := setupDashboardAppService(t) account := createTestAccount(t, db) req := &CreateDashboardAppRequest{ Title: "空数组", Content: json.RawMessage(`[]`), } app, err := svc.Create(context.Background(), account.ID, nil, req) require.NoError(t, err) assert.NotZero(t, app.ID) }