822 lines
27 KiB
Plaintext
822 lines
27 KiB
Plaintext
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
|
|
} |