package service import ( "encoding/json" "fmt" "testing" "time" "gorm.io/driver/sqlite" "gorm.io/gorm" "gorm.io/gorm/logger" "github.com/gochat/gochat/internal/model" "github.com/gochat/gochat/internal/repository" ) // createTestAccount creates a test Account in the database. func createTestAccount(t *testing.T, db *gorm.DB) *model.Account { t.Helper() account := &model.Account{Name: "Test Account", Locale: "en", Status: "active"} if err := db.Create(account).Error; err != nil { t.Fatalf("failed to create test account: %v", err) } return account } // createTestAccountWithName creates a test Account with a custom name. 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("failed to create test account: %v", err) } return account } // setupServiceTestDB creates an in-memory SQLite database for service tests. 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("failed to open SQLite test database: %v", err) } if err := db.AutoMigrate( &model.Account{}, &model.User{}, &model.AccountUser{}, &model.Inbox{}, &model.Contact{}, &model.ContactInbox{}, &model.Conversation{}, &model.ConversationParticipant{}, &model.Message{}, &model.Attachment{}, &model.InboxMember{}, &model.Notification{}, &model.NotificationPreference{}, &model.DashboardApp{}, &model.Portal{}, &model.Category{}, &model.RelatedCategory{}, &model.Article{}, &model.Folder{}, &model.PortalMember{}, &model.Company{}, &model.CompanyNote{}, &model.Tag{}, &model.ContactLabel{}, &model.ContactExport{}, &model.DataImport{}, &model.Notification{}, ); err != nil { t.Fatalf("failed to auto-migrate models: %v", err) } t.Cleanup(func() { sqlDB, _ := db.DB() sqlDB.Close() }) return db } // marshalAssistantConfig converts an arbitrary value to json.RawMessage for the Config field. func marshalAssistantConfig(cfg interface{}) json.RawMessage { b, _ := json.Marshal(cfg) return json.RawMessage(b) } // ========== DashboardApp test helpers ========== // setupDashboardAppService creates DashboardAppRepo + DashboardAppService test instances. 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 } // createTestUser creates a test User in the database. func createTestUser(t *testing.T, db *gorm.DB, accountID uint) *model.User { t.Helper() user := &model.User{ Name: "Test User", Email: fmt.Sprintf("test-%d@example.com", time.Now().UnixNano()), AccountID: accountID, } if err := db.Create(user).Error; err != nil { t.Fatalf("failed to create test user: %v", err) } return user } // createTestDashboardApp creates a test DashboardApp in the database. 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 test helpers ========== // setupKBServiceTestDB creates an in-memory SQLite with KB models migrated. func setupKBServiceTestDB(t *testing.T) *gorm.DB { t.Helper() db := setupServiceTestDB(t) return db } // createTestPortal creates a test Portal in the database. func createTestPortal(t *testing.T, db *gorm.DB, accountID uint, overrides ...func(*model.Portal)) *model.Portal { t.Helper() portal := &model.Portal{ AccountID: accountID, Name: "Test KB", Slug: fmt.Sprintf("test-kb-%d", time.Now().UnixNano()), Color: "#1f93ff", Locale: "en", SSLSettings: json.RawMessage(`{}`), } for _, fn := range overrides { fn(portal) } if err := db.Create(portal).Error; err != nil { t.Fatalf("failed to create test portal: %v", err) } return portal } // createTestCategory creates a test Category in the database. func createTestCategory(t *testing.T, db *gorm.DB, portalID uint, overrides ...func(*model.Category)) *model.Category { t.Helper() category := &model.Category{ PortalID: portalID, Name: "Test Category", Description: "A test category", Slug: fmt.Sprintf("test-cat-%d", time.Now().UnixNano()), } for _, fn := range overrides { fn(category) } if err := db.Create(category).Error; err != nil { t.Fatalf("failed to create test category: %v", err) } return category } // createTestFolder creates a test Folder in the database. func createTestFolder(t *testing.T, db *gorm.DB, portalID uint, overrides ...func(*model.Folder)) *model.Folder { t.Helper() folder := &model.Folder{ PortalID: portalID, Name: "Test Folder", } for _, fn := range overrides { fn(folder) } if err := db.Create(folder).Error; err != nil { t.Fatalf("failed to create test folder: %v", err) } return folder } // createTestArticle creates a test Article in the database. func createTestArticle(t *testing.T, db *gorm.DB, accountID uint, portalID uint, overrides ...func(*model.Article)) *model.Article { t.Helper() article := &model.Article{ AccountID: accountID, PortalID: portalID, Title: "Test Article", Slug: fmt.Sprintf("test-article-%d", time.Now().UnixNano()), Content: "Test content", Status: string(model.ArticleStatusDraft), } for _, fn := range overrides { fn(article) } if err := db.Create(article).Error; err != nil { t.Fatalf("failed to create test article: %v", err) } return article } // ========== Notification test helpers ========== // setupNotificationService creates NotificationRepo + NotificationPreferenceRepo + NotificationService test instances. 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 creates a test Notification in the database. 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("failed to create test notification: %v", err) } return notification } // ========== Account test helpers ========== // setupAccountService creates AccountRepo + AccountService test instances. 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 } // ========== Contact test helpers ========== // setupContactServiceTestDB creates an in-memory SQLite database with Contact-related models migrated. func setupContactServiceTestDB(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("failed to open SQLite test database: %v", err) } if err := db.AutoMigrate( &model.Account{}, &model.User{}, &model.AccountUser{}, &model.Inbox{}, &model.Contact{}, &model.ContactInbox{}, &model.Tag{}, &model.ContactLabel{}, &model.ContactExport{}, &model.DataImport{}, &model.Notification{}, ); err != nil { t.Fatalf("failed to auto-migrate contact models: %v", err) } t.Cleanup(func() { sqlDB, _ := db.DB() sqlDB.Close() }) return db } // setupContactService creates ContactRepo + ContactInboxService + ContactService test instances. func setupContactService(t *testing.T) (*gorm.DB, *repository.ContactRepo, *ContactService) { t.Helper() db := setupContactServiceTestDB(t) repo := repository.NewContactRepo(db) contactInboxSvc := NewContactInboxService(repository.NewContactInboxRepo(db)) noteRepo := repository.NewNoteRepo(db) svc := NewContactService(repo, contactInboxSvc, noteRepo) return db, repo, svc } // createTestContact creates a test Contact in the database. func createTestContact(t *testing.T, db *gorm.DB, accountID uint, overrides ...func(*model.Contact)) *model.Contact { t.Helper() contact := &model.Contact{ AccountID: accountID, Name: "Test Contact", Email: fmt.Sprintf("contact-%d@test.com", time.Now().UnixNano()), } for _, fn := range overrides { fn(contact) } if err := db.Create(contact).Error; err != nil { t.Fatalf("failed to create test contact: %v", err) } return contact } // createTestInbox creates a test Inbox in the database. func createTestInbox(t *testing.T, db *gorm.DB, accountID uint, channelType string, overrides ...func(*model.Inbox)) *model.Inbox { t.Helper() inbox := &model.Inbox{ AccountID: accountID, Name: "Test Inbox", ChannelType: channelType, ChannelID: 1, } for _, fn := range overrides { fn(inbox) } if err := db.Create(inbox).Error; err != nil { t.Fatalf("failed to create test inbox: %v", err) } return inbox } // ========== Conversation test helpers ========== // createTestConversation creates a test Conversation in the database. 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), ChannelType: "web_widget", Channel: "web_widget", } if err := db.Create(conv).Error; err != nil { t.Fatalf("failed to create test conversation: %v", err) } return conv } // ========== Message test helpers ========== // createTestMessage creates a test Message in the database. func createTestMessage(t *testing.T, db *gorm.DB, accountID, inboxID, conversationID uint, overrides ...func(*model.Message)) *model.Message { t.Helper() msg := &model.Message{ AccountID: accountID, InboxID: inboxID, ConversationID: conversationID, Content: "Test message", MessageType: "incoming", ContentType: "text", SenderType: "contact", } for _, fn := range overrides { fn(msg) } if err := db.Create(msg).Error; err != nil { t.Fatalf("failed to create test message: %v", err) } return msg } // skipIfSQLite skips the test when the underlying database is SQLite. // Service tests always use SQLite in-memory, so this always skips. // Use for PG-only features like ILIKE, trigram, pgvector, etc. func skipIfSQLite(t *testing.T) { t.Helper() t.Skip("Skipping: this test requires PostgreSQL (ILIKE / trigram / pgvector etc.)") }