package service import ( "context" "encoding/json" "fmt" "os" "testing" "time" "github.com/alicebob/miniredis/v2" "github.com/redis/go-redis/v9" "gorm.io/driver/sqlite" "gorm.io/gorm" "gorm.io/gorm/logger" "github.com/gochat/gochat/internal/auth" "github.com/gochat/gochat/internal/channel" "github.com/gochat/gochat/internal/config" "github.com/gochat/gochat/internal/llm" "github.com/gochat/gochat/internal/model" "github.com/gochat/gochat/internal/repository" ) // setupServiceTestDB 为 service 测试创建 SQLite 内存数据库并自动迁移所有模型。 // 每个测试函数获取独立的数据库实例,通过 t.Cleanup 关闭连接。 func setupServiceTestDB(t *testing.T) *gorm.DB { t.Helper() db, err := gorm.Open(sqlite.Open("file::memory:"), &gorm.Config{ Logger: logger.Default.LogMode(logger.Silent), }) if err != nil { t.Fatalf("无法打开 SQLite 测试数据库: %v", err) } // 自动迁移所有核心模型 if err := db.AutoMigrate( &model.Account{}, &model.User{}, &model.AccountUser{}, &model.Inbox{}, &model.Contact{}, &model.ContactInbox{}, &model.Conversation{}, &model.Message{}, &model.Notification{}, &model.NotificationPreference{}, &model.Attachment{}, &model.InboxMember{}, &model.CustomRole{}, &model.PlatformApp{}, &model.CaptainCustomTool{}, &model.ReportingEvent{}, &model.ReportingEventsRollup{}, &model.DashboardApp{}, &model.Portal{}, &model.Category{}, &model.Article{}, &model.Folder{}, &model.PortalMember{}, &model.Team{}, &model.TeamMember{}, &model.Tag{}, &model.ConversationLabel{}, &model.PushToken{}, &model.WebhookSubscription{}, &model.WebhookDelivery{}, ); err != nil { t.Fatalf("无法自动迁移模型: %v", err) } t.Cleanup(func() { sqlDB, _ := db.DB() sqlDB.Close() }) return db } // setupAccountService 创建 AccountRepo + AccountService 测试实例。 func setupAccountService(t *testing.T) (*gorm.DB, *repository.AccountRepo, *AccountService) { t.Helper() db := setupServiceTestDB(t) repo := repository.NewAccountRepo(db) svc := NewAccountService(repo) return db, repo, svc } // setupInboxMemberService 创建 InboxMemberRepo + InboxMemberService 测试实例。 func setupInboxMemberService(t *testing.T) (*gorm.DB, *repository.InboxMemberRepo, *InboxMemberService) { t.Helper() db := setupServiceTestDB(t) repo := repository.NewInboxMemberRepo(db) svc := NewInboxMemberService(repo) return db, repo, svc } // setupInboxService 创建 InboxRepo + InboxService 测试实例。 func setupInboxService(t *testing.T) (*gorm.DB, *repository.InboxRepo, *InboxService) { t.Helper() db := setupServiceTestDB(t) repo := repository.NewInboxRepo(db) svc := NewInboxService(repo) return db, repo, svc } // setupContactService 创建 ContactRepo + ContactInboxService + ContactService 测试实例。 func setupContactService(t *testing.T) (*gorm.DB, *repository.ContactRepo, *ContactService) { t.Helper() db := setupServiceTestDB(t) repo := repository.NewContactRepo(db) contactInboxSvc := NewContactInboxService(repository.NewContactInboxRepo(db)) svc := NewContactService(repo, contactInboxSvc) return db, repo, svc } // setupContactInboxService 创建 ContactInboxRepo + ContactInboxService 测试实例。 func setupContactInboxService(t *testing.T) (*gorm.DB, *repository.ContactInboxRepo, *ContactInboxService) { t.Helper() db := setupServiceTestDB(t) repo := repository.NewContactInboxRepo(db) svc := NewContactInboxService(repo) return db, repo, svc } // setupConversationService 创建 ConversationRepo + MessageRepo + ConversationService 测试实例。 func setupConversationService(t *testing.T) (*gorm.DB, *repository.ConversationRepo, *repository.MessageRepo, *ConversationService) { t.Helper() db := setupServiceTestDB(t) convRepo := repository.NewConversationRepo(db) msgRepo := repository.NewMessageRepo(db) dispatcher := channel.NewDispatcher() inboxMemberSvc := NewInboxMemberService(repository.NewInboxMemberRepo(db)) svc := NewConversationService(convRepo, msgRepo, dispatcher, inboxMemberSvc) return db, convRepo, msgRepo, svc } // setupAttachmentService 创建 AttachmentRepo + AttachmentService 测试实例。 func setupAttachmentService(t *testing.T) (*gorm.DB, *repository.AttachmentRepo, *AttachmentService) { t.Helper() db := setupServiceTestDB(t) repo := repository.NewAttachmentRepo(db) svc := NewAttachmentService(repo) return db, repo, svc } // createTestAccount 在数据库中创建一个测试 Account 并返回。 func createTestAccount(t *testing.T, db *gorm.DB) *model.Account { t.Helper() account := &model.Account{Name: "测试账户", Locale: "en", Status: "active"} if err := db.Create(account).Error; err != nil { t.Fatalf("无法创建测试账户: %v", err) } return account } // createTestAccountWithName 在数据库中创建一个指定名称的测试 Account 并返回。 func createTestAccountWithName(t *testing.T, db *gorm.DB, name string) *model.Account { t.Helper() account := &model.Account{Name: name, Locale: "en", Status: "active"} if err := db.Create(account).Error; err != nil { t.Fatalf("无法创建测试账户: %v", err) } return account } // createTestUser 在数据库中创建一个测试 User 并返回。 func createTestUser(t *testing.T, db *gorm.DB, accountID uint) *model.User { t.Helper() user := &model.User{ AccountID: accountID, Name: "测试用户", Email: "test@example.com", Password: "hashedpassword", Role: "agent", Active: true, } if err := db.Create(user).Error; err != nil { t.Fatalf("无法创建测试用户: %v", err) } return user } // createTestInbox 在数据库中创建一个测试 Inbox 并返回。 func createTestInbox(t *testing.T, db *gorm.DB, accountID uint, channelType string) *model.Inbox { t.Helper() inbox := &model.Inbox{ AccountID: accountID, Name: "测试收件箱", ChannelType: channelType, Enabled: true, } if err := db.Create(inbox).Error; err != nil { t.Fatalf("无法创建测试收件箱: %v", err) } return inbox } // createTestContact 在数据库中创建一个测试 Contact 并返回。 func createTestContact(t *testing.T, db *gorm.DB, accountID uint) *model.Contact { t.Helper() contact := &model.Contact{ AccountID: accountID, Name: "测试联系人", Email: "contact@example.com", } if err := db.Create(contact).Error; err != nil { t.Fatalf("无法创建测试联系人: %v", err) } return contact } // createTestConversation 在数据库中创建一个测试 Conversation 并返回。 func createTestConversation(t *testing.T, db *gorm.DB, accountID, inboxID, contactID uint) *model.Conversation { t.Helper() conv := &model.Conversation{ AccountID: accountID, InboxID: inboxID, ContactID: contactID, Status: string(model.ConversationStatusOpen), Priority: string(model.ConversationPriorityMedium), } if err := db.Create(conv).Error; err != nil { t.Fatalf("无法创建测试对话: %v", err) } return conv } // createTestMessage 在数据库中创建一个测试 Message 并返回。 func createTestMessage(t *testing.T, db *gorm.DB, accountID, inboxID, conversationID uint) *model.Message { t.Helper() msg := &model.Message{ AccountID: accountID, InboxID: inboxID, ConversationID: conversationID, Content: "测试消息", MessageType: "incoming", ContentType: "text", SenderType: "contact", } if err := db.Create(msg).Error; err != nil { t.Fatalf("无法创建测试消息: %v", err) } return msg } // createTestAttachment 在数据库中创建一个测试 Attachment 并返回。 func createTestAttachment(t *testing.T, db *gorm.DB, messageID, accountID uint) *model.Attachment { t.Helper() att := &model.Attachment{ MessageID: messageID, AccountID: accountID, FileType: "image", FileURL: "https://example.com/test.png", FileName: "test.png", FileSize: 1024, } if err := db.Create(att).Error; err != nil { t.Fatalf("无法创建测试附件: %v", err) } return att } // setupCaptainCustomToolService 创建 CaptainCustomToolRepo + CaptainCustomToolService 测试实例。 func setupCaptainCustomToolService(t *testing.T) (*gorm.DB, *repository.CaptainCustomToolRepo, *CaptainCustomToolService) { t.Helper() db := setupServiceTestDB(t) repo := repository.NewCaptainCustomToolRepo(db) svc := NewCaptainCustomToolService(repo) return db, repo, svc } // createTestCaptainCustomTool 在数据库中创建一个测试 CaptainCustomTool 并返回。 func createTestCaptainCustomTool(t *testing.T, db *gorm.DB, accountID uint, overrides ...func(*model.CaptainCustomTool)) *model.CaptainCustomTool { t.Helper() tool := &model.CaptainCustomTool{ AccountID: accountID, Title: "测试工具", Slug: "test-tool", Description: "用于测试的自定义工具", EndpointURL: "https://example.com/api/test", HTTPMethod: "GET", AuthType: model.ToolAuthTypeNone, Enabled: true, } for _, fn := range overrides { fn(tool) } if err := db.Create(tool).Error; err != nil { t.Fatalf("无法创建测试自定义工具: %v", err) } return tool } // setupAuthService 创建 AuthService 测试实例(含 JWTService、RefreshTokenStore、OAuthService、MFAService)。 // 返回 db、JWTService、RefreshTokenStore、OAuthService、MFAService、AuthService,以及用于验证的 miniredis 实例。 func setupAuthService(t *testing.T) (*gorm.DB, *auth.JWTService, *auth.RefreshTokenStore, *auth.OAuthService, *auth.MFAService, *AuthService) { t.Helper() db := setupServiceTestDB(t) // 创建 miniredis 代替真实 Redis mr := miniredis.RunT(t) rdb := redis.NewClient(&redis.Options{Addr: mr.Addr()}) t.Cleanup(func() { rdb.Close() mr.Close() }) jwtCfg := &config.JWTConfig{ Secret: "test-secret-key-min-32-chars!!", ExpiryHours: 24, RefreshExpiryHours: 7 * 24, AccessExpiryMinutes: 15, } jwtSvc := auth.NewJWTService(jwtCfg) refreshStore := auth.NewRefreshTokenStore(rdb, jwtCfg) appCfg := &config.Config{ JWT: *jwtCfg, OAuth: config.OAuthConfig{ Google: config.OAuthProviderConfig{ ClientID: "test-google-client-id", ClientSecret: "test-google-client-secret", RedirectURL: "http://localhost:8080/auth/google/callback", }, GitHub: config.OAuthProviderConfig{ ClientID: "test-github-client-id", ClientSecret: "test-github-client-secret", RedirectURL: "http://localhost:8080/auth/github/callback", }, }, } oauthSvc := auth.NewOAuthService(db, appCfg) mfaSvc := auth.NewMFAService(db) svc := NewAuthService(db, jwtSvc, refreshStore, nil, oauthSvc, mfaSvc) return db, jwtSvc, refreshStore, oauthSvc, mfaSvc, svc } // setupNotificationService 创建 NotificationRepo + NotificationPreferenceRepo + NotificationService 测试实例。 func setupNotificationService(t *testing.T) (*gorm.DB, *repository.NotificationRepo, *repository.NotificationPreferenceRepo, *NotificationService) { t.Helper() db := setupServiceTestDB(t) notifRepo := repository.NewNotificationRepo(db) prefRepo := repository.NewNotificationPreferenceRepo(db) svc := NewNotificationService(db, notifRepo, prefRepo) return db, notifRepo, prefRepo, svc } // createTestNotification 在数据库中创建一个测试 Notification 并返回。 func createTestNotification(t *testing.T, db *gorm.DB, userID uint, accountID *uint, notifType string) *model.Notification { t.Helper() notification := &model.Notification{ UserID: userID, AccountID: accountID, NotificationType: notifType, PrimaryActorType: "Conversation", PrimaryActorID: 1, PushEnabled: false, EmailEnabled: false, } if err := db.Create(notification).Error; err != nil { t.Fatalf("无法创建测试通知: %v", err) } return notification } // setupRBACService 创建 RBACService 测试实例。 func setupRBACService(t *testing.T) (*gorm.DB, *RBACService) { t.Helper() db := setupServiceTestDB(t) svc := NewRBACService(db) return db, svc } // createTestAccountForRBAC 在数据库中创建一个测试 Account 并返回。 func createTestAccountForRBAC(t *testing.T, db *gorm.DB) *model.Account { t.Helper() acc := &model.Account{ Name: "Test Account RBAC", Active: true, Status: "active", } if err := db.Create(acc).Error; err != nil { t.Fatalf("无法创建测试账户: %v", err) } return acc } // createTestUserForRBAC 在数据库中创建一个测试 User 并返回。 func createTestUserForRBAC(t *testing.T, db *gorm.DB) *model.User { t.Helper() user := &model.User{ Name: "Test User RBAC", Email: fmt.Sprintf("rbac_user_%d@test.com", time.Now().UnixNano()), Password: "hashed_password", Active: true, } if err := db.Create(user).Error; err != nil { t.Fatalf("无法创建测试用户: %v", err) } return user } // mockLLMProvider is a mock implementation of llm.Provider for testing. // It allows tests to control the response returned by ChatCompletion and CreateEmbedding. type mockLLMProvider struct { chatResponse *llm.ChatResponse chatError error embeddingResponse *llm.EmbeddingResponse embeddingError error lastChatRequest *llm.ChatRequest } func (m *mockLLMProvider) ChatCompletion(ctx context.Context, req llm.ChatRequest) (*llm.ChatResponse, error) { m.lastChatRequest = &req return m.chatResponse, m.chatError } func (m *mockLLMProvider) CreateEmbedding(ctx context.Context, req llm.EmbeddingRequest) (*llm.EmbeddingResponse, error) { return m.embeddingResponse, m.embeddingError } // setupCaptainDocumentService 创建 CaptainDocumentRepo + mockLLMProvider + CaptainDocumentService 测试实例。 // 使用 mock LLM provider 避免真实 API 调用,返回 mock 以便测试中自定义 LLM 行为。 func setupCaptainDocumentService(t *testing.T) (*gorm.DB, *mockLLMProvider, *CaptainDocumentService) { t.Helper() db := setupServiceTestDB(t) // 迁移 captain_document 相关模型 if err := db.AutoMigrate(&model.CaptainDocument{}); err != nil { t.Fatalf("无法自动迁移 captain_document 模型: %v", err) } docRepo := repository.NewCaptainDocumentRepo(db) mockProvider := &mockLLMProvider{} svc := NewCaptainDocumentService(docRepo, mockProvider) return db, mockProvider, svc } // createTestCaptainDocument 在数据库中创建一个测试 CaptainDocument 并返回。 func createTestCaptainDocument(t *testing.T, db *gorm.DB, accountID, assistantID uint, name, externalLink string) *model.CaptainDocument { t.Helper() doc := &model.CaptainDocument{ AccountID: accountID, AssistantID: assistantID, Name: name, ExternalLink: externalLink, Status: model.DocumentStatusPending, } if err := db.Create(doc).Error; err != nil { t.Fatalf("无法创建测试 CaptainDocument: %v", err) } return doc } // setupCaptainAssistantService 创建 CaptainAssistantRepo + CaptainInboxRepo + CaptainDocumentRepo + CaptainAssistantResponseRepo + mockLLMProvider + CaptainAssistantService 测试实例。 // 使用 mock LLM provider 避免真实 API 调用,返回 mock 以便测试中自定义 LLM 行为。 func setupCaptainAssistantService(t *testing.T) (*gorm.DB, *mockLLMProvider, *CaptainAssistantService) { t.Helper() db := setupServiceTestDB(t) // 迁移 captain_assistant 相关模型 if err := db.AutoMigrate( &model.CaptainAssistant{}, &model.CaptainInbox{}, &model.CaptainDocument{}, &model.CaptainAssistantResponse{}, ); err != nil { t.Fatalf("无法自动迁移 captain_assistant 相关模型: %v", err) } assistantRepo := repository.NewCaptainAssistantRepo(db) inboxRepo := repository.NewCaptainInboxRepo(db) documentRepo := repository.NewCaptainDocumentRepo(db) responseRepo := repository.NewCaptainAssistantResponseRepo(db) mockProvider := &mockLLMProvider{} svc := NewCaptainAssistantService(assistantRepo, inboxRepo, documentRepo, responseRepo, mockProvider) return db, mockProvider, svc } // setupCaptainScenarioService 创建 CaptainScenarioRepo + CaptainScenarioService 测试实例。 func setupCaptainScenarioService(t *testing.T) (*gorm.DB, *repository.CaptainScenarioRepo, *CaptainScenarioService) { t.Helper() db := setupServiceTestDB(t) // 迁移 captain_scenario 相关模型 if err := db.AutoMigrate(&model.CaptainScenario{}, &model.CaptainAssistant{}); err != nil { t.Fatalf("无法自动迁移 captain_scenario 模型: %v", err) } repo := repository.NewCaptainScenarioRepo(db) svc := NewCaptainScenarioService(repo) return db, repo, svc } // createTestCaptainAssistant 在数据库中创建一个测试 CaptainAssistant 并返回。 func createTestCaptainAssistant(t *testing.T, db *gorm.DB, accountID uint, overrides ...func(*model.CaptainAssistant)) *model.CaptainAssistant { t.Helper() assistant := &model.CaptainAssistant{ AccountID: accountID, Name: "测试助手", Status: model.AssistantStatusActive, } for _, fn := range overrides { fn(assistant) } if err := db.Create(assistant).Error; err != nil { t.Fatalf("无法创建测试助手: %v", err) } return assistant } // createTestCaptainScenario 在数据库中创建一个测试 CaptainScenario 并返回。 func createTestCaptainScenario(t *testing.T, db *gorm.DB, accountID, assistantID uint, overrides ...func(*model.CaptainScenario)) *model.CaptainScenario { t.Helper() scenario := &model.CaptainScenario{ AccountID: accountID, AssistantID: assistantID, Title: "测试场景", Description: "用于测试的场景", Instruction: "按照以下步骤操作", Enabled: true, } for _, fn := range overrides { fn(scenario) } if err := db.Create(scenario).Error; err != nil { t.Fatalf("无法创建测试场景: %v", err) } return scenario } // setupCopilotService 创建 CopilotThreadRepo + CopilotMessageRepo + CopilotService 测试实例。 // 使用 mock LLM provider 避免真实 API 调用,返回 mock 以便测试中自定义 LLM 行为。 func setupCopilotService(t *testing.T) (*gorm.DB, *mockLLMProvider, *CopilotService) { t.Helper() db := setupServiceTestDB(t) // 迁移 copilot 相关模型 if err := db.AutoMigrate(&model.CopilotThread{}, &model.CopilotMessage{}); err != nil { t.Fatalf("无法自动迁移 copilot 模型: %v", err) } threadRepo := repository.NewCopilotThreadRepo(db) messageRepo := repository.NewCopilotMessageRepo(db) mockProvider := &mockLLMProvider{} svc := NewCopilotService(threadRepo, messageRepo, mockProvider) return db, mockProvider, svc } // --- Analytics Service Helpers --- // skipIfSQLite skips the test when the underlying database is SQLite. // PG-only features (ILIKE, NOW(), NULLS LAST, etc.) should call this. // In service tests, setupServiceTestDB creates a SQLite in-memory DB by default. // To test PG-only features, set GOCHAT_TEST_DB=postgres and the helper must be // updated to use PG instead. For now, service tests always use SQLite so // PG-only features are skipped. func skipIfSQLite(t *testing.T) { t.Helper() // Service test helpers currently always use SQLite in-memory. // When GOCHAT_TEST_DB=postgres support is added at service level, // this should check the actual driver like the repository version does. if os.Getenv("GOCHAT_TEST_DB") != "postgres" { t.Skip("Skipping: this test requires PostgreSQL (ILIKE / NOW() / NULLS LAST etc.)") } } // setupAnalyticsService 创建 ReportingEventRepo + ReportingEventsRollupRepo + AnalyticsService 测试实例。 func setupAnalyticsService(t *testing.T) (*gorm.DB, *repository.ReportingEventRepo, *repository.ReportingEventsRollupRepo, *AnalyticsService) { t.Helper() db := setupServiceTestDB(t) eventRepo := repository.NewReportingEventRepo(db) rollupRepo := repository.NewReportingEventsRollupRepo(db) svc := NewAnalyticsService(eventRepo, rollupRepo) return db, eventRepo, rollupRepo, svc } // createTestReportingEvent 在数据库中创建一个测试 ReportingEvent 并返回。 func createTestReportingEvent(t *testing.T, db *gorm.DB, accountID uint, overrides ...func(*model.ReportingEvent)) *model.ReportingEvent { t.Helper() event := &model.ReportingEvent{ AccountID: accountID, Name: model.MetricNameFirstResponse, Value: 120.5, ValueInBusinessHours: 60.0, } for _, fn := range overrides { fn(event) } if err := db.Create(event).Error; err != nil { t.Fatalf("无法创建测试 ReportingEvent: %v", err) } return event } // createTestRollup 在数据库中创建一个测试 ReportingEventsRollup 并返回。 func createTestRollup(t *testing.T, db *gorm.DB, accountID uint, overrides ...func(*model.ReportingEventsRollup)) *model.ReportingEventsRollup { t.Helper() rollup := &model.ReportingEventsRollup{ AccountID: accountID, Date: time.Date(2026, 1, 15, 0, 0, 0, 0, time.UTC), DimensionType: model.DimensionAccount, DimensionID: accountID, Metric: model.MetricFirstResponse, Count: 5, SumValue: 600.0, SumValueBusinessHours: 300.0, } for _, fn := range overrides { fn(rollup) } if err := db.Create(rollup).Error; err != nil { t.Fatalf("无法创建测试 ReportingEventsRollup: %v", err) } return rollup } // setupDashboardAppService 创建 DashboardAppRepo + DashboardAppService 测试实例。 func setupDashboardAppService(t *testing.T) (*gorm.DB, *repository.DashboardAppRepo, *DashboardAppService) { t.Helper() db := setupServiceTestDB(t) repo := repository.NewDashboardAppRepo(db) svc := NewDashboardAppService(repo) return db, repo, svc } // setupPlatformAppService 创建 PlatformAppRepo + PlatformAppService 测试实例。 func setupPlatformAppService(t *testing.T) (*gorm.DB, *repository.PlatformAppRepo, *PlatformAppService) { t.Helper() db := setupServiceTestDB(t) repo := repository.NewPlatformAppRepo(db) svc := NewPlatformAppService(repo) return db, repo, svc } // createTestPlatformApp 在数据库中创建一个测试 PlatformApp 并返回。 func createTestPlatformApp(t *testing.T, db *gorm.DB, accountID uint, overrides ...func(*model.PlatformApp)) *model.PlatformApp { t.Helper() app := &model.PlatformApp{ AccountID: accountID, Name: "测试平台应用", APIKey: "test-api-key-" + fmt.Sprintf("%d", time.Now().UnixNano()), Description: "测试描述", Type: "api", Status: "active", } for _, fn := range overrides { fn(app) } if err := db.Create(app).Error; err != nil { t.Fatalf("无法创建测试 PlatformApp: %v", err) } return app } // createTestDashboardApp 在数据库中创建一个测试 DashboardApp 并返回。 func createTestDashboardApp(t *testing.T, db *gorm.DB, accountID uint, overrides ...func(*model.DashboardApp)) *model.DashboardApp { t.Helper() app := &model.DashboardApp{ AccountID: accountID, Title: "测试仪表盘", Kind: "frame", Active: model.BoolPtr(true), Content: json.RawMessage(`[{"type":"frame","url":"https://example.com/widget"}]`), } for _, fn := range overrides { fn(app) } if err := db.Create(app).Error; err != nil { t.Fatalf("无法创建测试 DashboardApp: %v", err) } return app } // ========== KnowledgeBase setup helpers ========== // setupPortalService creates PortalRepo + PortalService test instances. func setupPortalService(t *testing.T) (*gorm.DB, *repository.PortalRepo, *PortalService) { t.Helper() db := setupServiceTestDB(t) repo := repository.NewPortalRepo(db) svc := NewPortalService(repo) return db, repo, svc } // setupCategoryService creates CategoryRepo + CategoryService test instances. func setupCategoryService(t *testing.T) (*gorm.DB, *repository.CategoryRepo, *CategoryService) { t.Helper() db := setupServiceTestDB(t) repo := repository.NewCategoryRepo(db) relatedRepo := repository.NewRelatedCategoryRepo(db) svc := NewCategoryService(repo, relatedRepo) return db, repo, svc } // setupArticleService creates ArticleRepo + ArticleService test instances. func setupArticleService(t *testing.T) (*gorm.DB, *repository.ArticleRepo, *ArticleService) { t.Helper() db := setupServiceTestDB(t) repo := repository.NewArticleRepo(db) svc := NewArticleService(repo) return db, repo, svc } // setupFolderService creates FolderRepo + FolderService test instances. func setupFolderService(t *testing.T) (*gorm.DB, *repository.FolderRepo, *FolderService) { t.Helper() db := setupServiceTestDB(t) repo := repository.NewFolderRepo(db) svc := NewFolderService(repo) return db, repo, svc } // setupPortalMemberService creates PortalMemberRepo + PortalMemberService test instances. func setupPortalMemberService(t *testing.T) (*gorm.DB, *repository.PortalMemberRepo, *PortalMemberService) { t.Helper() db := setupServiceTestDB(t) repo := repository.NewPortalMemberRepo(db) svc := NewPortalMemberService(repo) return db, repo, svc } // createTestPortal creates a test Portal in the database. func createTestPortal(t *testing.T, db *gorm.DB, accountID uint) *model.Portal { t.Helper() portal := &model.Portal{ AccountID: accountID, Name: "测试知识库门户", Slug: "test-portal", Description: "测试描述", Locale: "en", } if err := db.Create(portal).Error; err != nil { t.Fatalf("无法创建测试 Portal: %v", err) } return portal } // createTestCategory creates a test Category in the database. func createTestCategory(t *testing.T, db *gorm.DB, portalID uint) *model.Category { t.Helper() cat := &model.Category{ PortalID: portalID, Name: "测试分类", Description: "测试分类描述", Slug: "test-category", } if err := db.Create(cat).Error; err != nil { t.Fatalf("无法创建测试 Category: %v", err) } return cat } // createTestFolder creates a test Folder in the database. func createTestFolder(t *testing.T, db *gorm.DB, portalID uint) *model.Folder { t.Helper() folder := &model.Folder{ PortalID: portalID, Name: "测试文件夹", } if err := db.Create(folder).Error; err != nil { t.Fatalf("无法创建测试 Folder: %v", err) } return folder } // ========== Team & Profile service helpers ========== // teamRepoFromDB creates a TeamRepo from a gorm.DB instance for tests. func teamRepoFromDB(db *gorm.DB) *repository.TeamRepo { return repository.NewTeamRepo(db) } // teamMemberRepoFromDB creates a TeamMemberRepo from a gorm.DB instance for tests. func teamMemberRepoFromDB(db *gorm.DB) *repository.TeamMemberRepo { return repository.NewTeamMemberRepo(db) } // userRepoFromDB creates a UserRepo from a gorm.DB instance for tests. func userRepoFromDB(db *gorm.DB) *repository.UserRepo { return repository.NewUserRepo(db) } // setupTeamService creates TeamRepo + TeamMemberRepo + TeamService test instances. func setupTeamService(t *testing.T) (*gorm.DB, *TeamService) { t.Helper() db := setupServiceTestDB(t) svc := NewTeamService(teamRepoFromDB(db), teamMemberRepoFromDB(db)) return db, svc } // setupProfileService creates UserRepo + ProfileService test instances. func setupProfileService(t *testing.T) (*gorm.DB, *ProfileService) { t.Helper() db := setupServiceTestDB(t) svc := NewProfileService(userRepoFromDB(db)) return db, svc } // setupTagService creates TagRepo + TagService test instances. func setupTagService(t *testing.T) (*gorm.DB, *repository.TagRepo, *TagService) { t.Helper() db := setupServiceTestDB(t) tagRepo := repository.NewTagRepo(db) svc := NewTagService(tagRepo) return db, tagRepo, svc } // setupLabelService creates ConversationLabelRepo + TagRepo + LabelService test instances. func setupLabelService(t *testing.T) (*gorm.DB, *repository.ConversationLabelRepo, *repository.TagRepo, *LabelService) { t.Helper() db := setupServiceTestDB(t) convLabelRepo := repository.NewConversationLabelRepo(db) tagRepo := repository.NewTagRepo(db) svc := NewLabelService(convLabelRepo, tagRepo) return db, convLabelRepo, tagRepo, svc }