Files
gochat/backend/internal/service/coverage7_test.go
T
Rogeeandrogee 74eb006985 H-43: fix WEB Captain takeover E2E flow (#8)
* test(shangwutong): cover CID rename reliability

* H-43: fix WEB Captain takeover flow

* H-48: preserve compatible provider model

* H-49: make Captain takeover atomic

* H-50: prevent duplicate widget initialization

---------

Co-authored-by: Rogee <rogee@ipao.vip>
2026-08-13 14:49:04 +08:00

3549 lines
111 KiB
Go

package service
import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"github.com/gochat/gochat/internal/auth"
"github.com/gochat/gochat/internal/autoassignment"
"github.com/gochat/gochat/internal/config"
"github.com/gochat/gochat/internal/llm"
"github.com/gochat/gochat/internal/model"
"github.com/gochat/gochat/internal/repository"
pkgcrypto "github.com/gochat/gochat/pkg/crypto"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gorm.io/gorm"
)
// ============================================================
// Helper: extended test DB with captain/copilot/analytics/upload/auth models
// ============================================================
func newCov7TestDB(t *testing.T, extra ...interface{}) *gorm.DB {
t.Helper()
db := newSimpleServiceTestDB(t, append([]interface{}{
&model.CaptainAssistant{},
&model.CaptainInbox{},
&model.CaptainDocument{},
&model.CaptainAssistantResponse{},
&model.CaptainMessageReport{},
&model.CaptainScenario{},
&model.CaptainCustomTool{},
&model.CaptainPreference{},
&model.CopilotThread{},
&model.CopilotMessage{},
&model.CopilotSuggestionMessage{},
&model.ReportingEventsRollup{},
&model.DirectUpload{},
&model.AssignmentPolicy{},
&model.InboxAssignmentPolicy{},
&model.UserSession{},
&model.BackgroundJob{},
&model.AgentBot{},
&model.AgentBotInbox{},
}, extra...)...)
return db
}
// mockCov7LLMProvider is a mock LLM provider for coverage7 tests.
type mockCov7LLMProvider struct {
chatResponse *llm.ChatResponse
chatError error
embeddingResponse *llm.EmbeddingResponse
embeddingError error
lastChatRequest *llm.ChatRequest
}
func (m *mockCov7LLMProvider) ChatCompletion(ctx context.Context, req llm.ChatRequest) (*llm.ChatResponse, error) {
m.lastChatRequest = &req
return m.chatResponse, m.chatError
}
func (m *mockCov7LLMProvider) CreateEmbedding(ctx context.Context, req llm.EmbeddingRequest) (*llm.EmbeddingResponse, error) {
return m.embeddingResponse, m.embeddingError
}
func (m *mockCov7LLMProvider) ChatCompletionStream(ctx context.Context, req llm.ChatRequest, onChunk func(llm.StreamChunk) error) error {
return nil
}
// setupCov7CaptainAssistant creates a CaptainAssistantService with all repos wired.
func setupCov7CaptainAssistant(t *testing.T) (*gorm.DB, *mockCov7LLMProvider, *CaptainAssistantService) {
t.Helper()
db := newCov7TestDB(t)
assistantRepo := repository.NewCaptainAssistantRepo(db)
inboxRepo := repository.NewCaptainInboxRepo(db)
documentRepo := repository.NewCaptainDocumentRepo(db)
responseRepo := repository.NewCaptainAssistantResponseRepo(db)
provider := &mockCov7LLMProvider{
chatResponse: &llm.ChatResponse{
Choices: []llm.ChatChoice{
{Message: llm.ChatMessage{Role: "assistant", Content: "mock response"}},
},
},
}
svc := NewCaptainAssistantService(assistantRepo, inboxRepo, documentRepo, responseRepo, provider)
return db, provider, svc
}
// setupCov7Copilot creates a CopilotService with all repos wired.
func setupCov7Copilot(t *testing.T) (*gorm.DB, *mockCov7LLMProvider, *CopilotService) {
t.Helper()
db := newCov7TestDB(t)
threadRepo := repository.NewCopilotThreadRepo(db)
messageRepo := repository.NewCopilotMessageRepo(db)
suggestionRepo := repository.NewCopilotSuggestionRepo(db)
assistantRepo := repository.NewCaptainAssistantRepo(db)
provider := &mockCov7LLMProvider{
chatResponse: &llm.ChatResponse{
Choices: []llm.ChatChoice{
{Message: llm.ChatMessage{Role: "assistant", Content: "mock response"}},
},
},
}
svc := NewCopilotService(threadRepo, messageRepo, suggestionRepo, provider, assistantRepo)
return db, provider, svc
}
// setupCov7Analytics creates an AnalyticsService with all repos wired.
func setupCov7Analytics(t *testing.T) (*gorm.DB, *AnalyticsService) {
t.Helper()
db := newCov7TestDB(t)
eventRepo := repository.NewReportingEventRepo(db)
rollupRepo := repository.NewReportingEventsRollupRepo(db)
svc := NewAnalyticsService(eventRepo, rollupRepo)
return db, svc
}
// setupCov7Upload creates an UploadService with a temp storage dir.
func setupCov7Upload(t *testing.T) (*gorm.DB, *UploadService) {
t.Helper()
db := newCov7TestDB(t)
tmpDir := t.TempDir()
cfg := &config.Config{
Storage: config.StorageConfig{
Provider: "local",
LocalPath: tmpDir,
MaxFileSize: 20 * 1024 * 1024,
},
}
directUploadRepo := repository.NewDirectUploadRepo(db)
svc := NewUploadService(directUploadRepo, cfg)
return db, svc
}
// setupCov7Auth creates an AuthService with JWT + refresh store (in-memory).
func setupCov7Auth(t *testing.T) (*gorm.DB, *AuthService) {
t.Helper()
db := newCov7TestDB(t)
jwtSvc := auth.NewJWTService(&config.JWTConfig{
Secret: "test-secret-key-cov7",
ExpiryHours: 1,
RefreshExpiryHours: 168,
})
refreshStore := auth.NewRefreshTokenStore(nil, &config.JWTConfig{
Secret: "test-secret-key-cov7",
ExpiryHours: 1,
RefreshExpiryHours: 168,
})
svc := NewAuthService(db, jwtSvc, refreshStore)
return db, svc
}
// seedCov7Account creates an Account in the test DB.
func seedCov7Account(t *testing.T, db *gorm.DB) *model.Account {
t.Helper()
account := &model.Account{Name: "Cov7 Account", Locale: "en", Status: "active"}
require.NoError(t, db.Create(account).Error)
return account
}
// seedCov7User creates a User with a hashed password in the test DB.
func seedCov7User(t *testing.T, db *gorm.DB, accountID uint, email string) *model.User {
t.Helper()
hash, err := hashPasswordCov7("password123")
require.NoError(t, err)
now := time.Now()
user := &model.User{
AccountID: accountID,
Name: "Test User",
Email: email,
Password: hash,
PasswordDigest: hash,
Provider: "email",
Active: true,
ConfirmedAt: &now,
}
require.NoError(t, db.Create(user).Error)
// Create AccountUser join
au := AccountUser{UserID: user.ID, AccountID: accountID, Role: "administrator"}
require.NoError(t, db.Create(&au).Error)
return user
}
// hashPasswordCov7 uses the crypto package to hash a password.
func hashPasswordCov7(password string) (string, error) {
return pkgcrypto.HashPassword(password)
}
// seedCov7Inbox creates an Inbox in the test DB.
func seedCov7Inbox(t *testing.T, db *gorm.DB, accountID uint) *model.Inbox {
t.Helper()
inbox := &model.Inbox{
AccountID: accountID,
Name: "Test Inbox",
ChannelType: "web_widget",
ChannelID: 1,
Enabled: true,
}
require.NoError(t, db.Create(inbox).Error)
return inbox
}
// seedCov7Contact creates a Contact in the test DB.
func seedCov7Contact(t *testing.T, db *gorm.DB, accountID uint) *model.Contact {
t.Helper()
contact := &model.Contact{
AccountID: accountID,
Name: "Test Contact",
Email: "contact@test.com",
}
require.NoError(t, db.Create(contact).Error)
return contact
}
// seedCov7Conversation creates a Conversation in the test DB.
func seedCov7Conversation(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),
ChannelType: "web_widget",
Channel: "web_widget",
}
require.NoError(t, db.Create(conv).Error)
return conv
}
// ============================================================
// CaptainAssistantService tests
// ============================================================
func TestCaptainAssistant_Create_Cov7(t *testing.T) {
db, _, svc := setupCov7CaptainAssistant(t)
account := seedCov7Account(t, db)
assistant, err := svc.Create(context.Background(), account.ID, &CreateAssistantRequest{
Name: "Test Assistant",
Description: "A test assistant",
})
require.NoError(t, err)
assert.NotZero(t, assistant.ID)
assert.Equal(t, "Test Assistant", assistant.Name)
assert.Equal(t, model.AssistantStatusActive, assistant.Status)
assert.NotEmpty(t, assistant.Config)
}
func TestCaptainAssistant_Create_NoName_Cov7(t *testing.T) {
db, _, svc := setupCov7CaptainAssistant(t)
account := seedCov7Account(t, db)
_, err := svc.Create(context.Background(), account.ID, &CreateAssistantRequest{
Description: "desc",
})
require.Error(t, err)
assert.Contains(t, err.Error(), "name is required")
}
func TestCaptainAssistant_Create_NoDescription_Cov7(t *testing.T) {
db, _, svc := setupCov7CaptainAssistant(t)
account := seedCov7Account(t, db)
_, err := svc.Create(context.Background(), account.ID, &CreateAssistantRequest{
Name: "Test",
})
require.Error(t, err)
assert.Contains(t, err.Error(), "description is required")
}
func TestCaptainAssistant_Create_DefaultConfig_Cov7(t *testing.T) {
db, _, svc := setupCov7CaptainAssistant(t)
account := seedCov7Account(t, db)
assistant, err := svc.Create(context.Background(), account.ID, &CreateAssistantRequest{
Name: "Test",
Description: "desc",
})
require.NoError(t, err)
var cfg map[string]interface{}
require.NoError(t, json.Unmarshal(assistant.Config, &cfg))
assert.Contains(t, cfg, "temperature")
}
func TestCaptainAssistant_Get_Cov7(t *testing.T) {
db, _, svc := setupCov7CaptainAssistant(t)
account := seedCov7Account(t, db)
created, err := svc.Create(context.Background(), account.ID, &CreateAssistantRequest{
Name: "Get Test",
Description: "desc",
})
require.NoError(t, err)
got, err := svc.Get(context.Background(), account.ID, created.ID)
require.NoError(t, err)
assert.Equal(t, created.ID, got.ID)
assert.Equal(t, "Get Test", got.Name)
}
func TestCaptainAssistant_Get_NotFound_Cov7(t *testing.T) {
db, _, svc := setupCov7CaptainAssistant(t)
account := seedCov7Account(t, db)
_, err := svc.Get(context.Background(), account.ID, 99999)
require.Error(t, err)
}
func TestCaptainAssistant_Update_Cov7(t *testing.T) {
db, _, svc := setupCov7CaptainAssistant(t)
account := seedCov7Account(t, db)
created, err := svc.Create(context.Background(), account.ID, &CreateAssistantRequest{
Name: "Original",
Description: "orig desc",
})
require.NoError(t, err)
updated, err := svc.Update(context.Background(), account.ID, created.ID, &UpdateAssistantRequest{
Name: "Updated Name",
Description: "updated desc",
Status: string(model.AssistantStatusDraft),
})
require.NoError(t, err)
assert.Equal(t, "Updated Name", updated.Name)
assert.Equal(t, "updated desc", updated.Description)
assert.Equal(t, model.AssistantStatusDraft, updated.Status)
}
func TestCaptainAssistant_Update_NotFound_Cov7(t *testing.T) {
db, _, svc := setupCov7CaptainAssistant(t)
account := seedCov7Account(t, db)
_, err := svc.Update(context.Background(), account.ID, 99999, &UpdateAssistantRequest{
Name: "X",
})
require.Error(t, err)
assert.Contains(t, err.Error(), "not found")
}
func TestCaptainAssistant_Update_Config_Cov7(t *testing.T) {
db, _, svc := setupCov7CaptainAssistant(t)
account := seedCov7Account(t, db)
created, err := svc.Create(context.Background(), account.ID, &CreateAssistantRequest{
Name: "CfgTest",
Description: "desc",
})
require.NoError(t, err)
newConfig := json.RawMessage(`{"temperature":0.5,"model":"gpt-4"}`)
updated, err := svc.Update(context.Background(), account.ID, created.ID, &UpdateAssistantRequest{
Config: newConfig,
})
require.NoError(t, err)
assert.Equal(t, newConfig, updated.Config)
}
func TestCaptainAssistant_Delete_Cov7(t *testing.T) {
db, _, svc := setupCov7CaptainAssistant(t)
account := seedCov7Account(t, db)
created, err := svc.Create(context.Background(), account.ID, &CreateAssistantRequest{
Name: "Delete Me",
Description: "desc",
})
require.NoError(t, err)
err = svc.Delete(context.Background(), account.ID, created.ID)
require.NoError(t, err)
_, err = svc.Get(context.Background(), account.ID, created.ID)
require.Error(t, err)
}
func TestCaptainAssistant_Delete_NotFound_Cov7(t *testing.T) {
db, _, svc := setupCov7CaptainAssistant(t)
account := seedCov7Account(t, db)
err := svc.Delete(context.Background(), account.ID, 99999)
require.Error(t, err)
assert.Contains(t, err.Error(), "not found")
}
func TestCaptainAssistant_List_Cov7(t *testing.T) {
db, _, svc := setupCov7CaptainAssistant(t)
account := seedCov7Account(t, db)
for i := 0; i < 3; i++ {
_, err := svc.Create(context.Background(), account.ID, &CreateAssistantRequest{
Name: fmt.Sprintf("Assistant %d", i),
Description: "desc",
})
require.NoError(t, err)
}
assistants, count, err := svc.List(context.Background(), account.ID, 0, 10)
require.NoError(t, err)
assert.Equal(t, int64(3), count)
assert.Len(t, assistants, 3)
}
func TestCaptainAssistant_List_Empty_Cov7(t *testing.T) {
db, _, svc := setupCov7CaptainAssistant(t)
account := seedCov7Account(t, db)
assistants, count, err := svc.List(context.Background(), account.ID, 0, 10)
require.NoError(t, err)
assert.Equal(t, int64(0), count)
assert.Empty(t, assistants)
}
func TestCaptainAssistant_List_Pagination_Cov7(t *testing.T) {
db, _, svc := setupCov7CaptainAssistant(t)
account := seedCov7Account(t, db)
for i := 0; i < 5; i++ {
_, err := svc.Create(context.Background(), account.ID, &CreateAssistantRequest{
Name: fmt.Sprintf("P%d", i),
Description: "desc",
})
require.NoError(t, err)
}
assistants, count, err := svc.List(context.Background(), account.ID, 2, 2)
require.NoError(t, err)
assert.Equal(t, int64(5), count)
assert.Len(t, assistants, 2)
}
func TestCaptainAssistant_GetConfig_Cov7(t *testing.T) {
db, _, svc := setupCov7CaptainAssistant(t)
account := seedCov7Account(t, db)
created, err := svc.Create(context.Background(), account.ID, &CreateAssistantRequest{
Name: "Cfg",
Description: "desc",
})
require.NoError(t, err)
cfg, err := svc.GetConfig(context.Background(), created.ID)
require.NoError(t, err)
assert.NotNil(t, cfg)
}
func TestCaptainAssistant_GetConfig_NotFound_Cov7(t *testing.T) {
_, _, svc := setupCov7CaptainAssistant(t)
_, err := svc.GetConfig(context.Background(), 99999)
require.Error(t, err)
}
func TestCaptainAssistant_SetConfig_Cov7(t *testing.T) {
db, _, svc := setupCov7CaptainAssistant(t)
account := seedCov7Account(t, db)
created, err := svc.Create(context.Background(), account.ID, &CreateAssistantRequest{
Name: "SetCfg",
Description: "desc",
})
require.NoError(t, err)
newCfg := &model.AssistantConfig{Temperature: 0.3, Model: "gpt-4"}
err = svc.SetConfig(context.Background(), created.ID, newCfg)
require.NoError(t, err)
got, err := svc.GetConfig(context.Background(), created.ID)
require.NoError(t, err)
assert.Equal(t, 0.3, got.Temperature)
assert.Equal(t, "gpt-4", got.Model)
}
func TestCaptainAssistant_SetConfig_NotFound_Cov7(t *testing.T) {
_, _, svc := setupCov7CaptainAssistant(t)
err := svc.SetConfig(context.Background(), 99999, &model.AssistantConfig{})
require.Error(t, err)
}
func TestCaptainAssistant_AssociateInbox_Cov7(t *testing.T) {
db, _, svc := setupCov7CaptainAssistant(t)
account := seedCov7Account(t, db)
inbox := seedCov7Inbox(t, db, account.ID)
created, err := svc.Create(context.Background(), account.ID, &CreateAssistantRequest{
Name: "InboxAssoc",
Description: "desc",
})
require.NoError(t, err)
assocInbox, err := svc.AssociateInbox(context.Background(), created.ID, inbox.ID, account.ID)
require.NoError(t, err)
assert.Equal(t, inbox.ID, assocInbox.ID)
var bot model.AgentBot
require.NoError(t, db.Where("account_id = ? AND bot_type = ?", account.ID, "captain").First(&bot).Error)
assert.Equal(t, created.ID, extractAssistantIDFromBotConfig(bot.Config))
var binding model.AgentBotInbox
require.NoError(t, db.Where("agent_bot_id = ? AND inbox_id = ?", bot.ID, inbox.ID).First(&binding).Error)
assert.True(t, binding.IsActive())
}
func TestCaptainAssistant_AssociateInbox_AssistantNotFound_Cov7(t *testing.T) {
db, _, svc := setupCov7CaptainAssistant(t)
account := seedCov7Account(t, db)
inbox := seedCov7Inbox(t, db, account.ID)
_, err := svc.AssociateInbox(context.Background(), 99999, inbox.ID, account.ID)
require.Error(t, err)
assert.Contains(t, err.Error(), "assistant not found")
}
func TestCaptainAssistant_AssociateInbox_InboxNotFound_Cov7(t *testing.T) {
db, _, svc := setupCov7CaptainAssistant(t)
account := seedCov7Account(t, db)
created, err := svc.Create(context.Background(), account.ID, &CreateAssistantRequest{
Name: "Test",
Description: "desc",
})
require.NoError(t, err)
_, err = svc.AssociateInbox(context.Background(), created.ID, 99999, account.ID)
require.Error(t, err)
assert.Contains(t, err.Error(), "inbox not found")
}
func TestCaptainAssistant_DissociateInbox_Cov7(t *testing.T) {
db, _, svc := setupCov7CaptainAssistant(t)
account := seedCov7Account(t, db)
inbox := seedCov7Inbox(t, db, account.ID)
created, err := svc.Create(context.Background(), account.ID, &CreateAssistantRequest{
Name: "DissocTest",
Description: "desc",
})
require.NoError(t, err)
_, err = svc.AssociateInbox(context.Background(), created.ID, inbox.ID, account.ID)
require.NoError(t, err)
err = svc.DissociateInbox(context.Background(), account.ID, created.ID, inbox.ID)
require.NoError(t, err)
var bindingCount int64
require.NoError(t, db.Model(&model.AgentBotInbox{}).Where("inbox_id = ?", inbox.ID).Count(&bindingCount).Error)
assert.Zero(t, bindingCount)
}
func TestCaptainAssistant_DissociateInbox_NotFound_Cov7(t *testing.T) {
db, _, svc := setupCov7CaptainAssistant(t)
account := seedCov7Account(t, db)
created, err := svc.Create(context.Background(), account.ID, &CreateAssistantRequest{
Name: "Test",
Description: "desc",
})
require.NoError(t, err)
err = svc.DissociateInbox(context.Background(), account.ID, created.ID, 99999)
require.Error(t, err)
}
func TestCaptainAssistant_ListInboxes_Cov7(t *testing.T) {
db, _, svc := setupCov7CaptainAssistant(t)
account := seedCov7Account(t, db)
inbox1 := seedCov7Inbox(t, db, account.ID)
inbox2 := &model.Inbox{
AccountID: account.ID,
Name: "Inbox 2",
ChannelType: "web_widget",
ChannelID: 2,
Enabled: true,
}
require.NoError(t, db.Create(inbox2).Error)
created, err := svc.Create(context.Background(), account.ID, &CreateAssistantRequest{
Name: "ListInbox",
Description: "desc",
})
require.NoError(t, err)
_, err = svc.AssociateInbox(context.Background(), created.ID, inbox1.ID, account.ID)
require.NoError(t, err)
_, err = svc.AssociateInbox(context.Background(), created.ID, inbox2.ID, account.ID)
require.NoError(t, err)
inboxes, err := svc.ListInboxes(context.Background(), account.ID, created.ID)
require.NoError(t, err)
assert.Len(t, inboxes, 2)
}
func TestCaptainAssistant_ListInboxes_AssistantNotFound_Cov7(t *testing.T) {
db, _, svc := setupCov7CaptainAssistant(t)
account := seedCov7Account(t, db)
_, err := svc.ListInboxes(context.Background(), account.ID, 99999)
require.Error(t, err)
}
func TestCaptainAssistant_AddDocument_Cov7(t *testing.T) {
db, _, svc := setupCov7CaptainAssistant(t)
account := seedCov7Account(t, db)
created, err := svc.Create(context.Background(), account.ID, &CreateAssistantRequest{
Name: "DocTest",
Description: "desc",
})
require.NoError(t, err)
doc := &model.CaptainDocument{
AccountID: account.ID,
Name: "Test Doc",
Status: model.DocumentStatusPending,
}
err = svc.AddDocument(context.Background(), created.ID, doc)
require.NoError(t, err)
assert.NotZero(t, doc.ID)
assert.Equal(t, created.ID, doc.AssistantID)
}
func TestCaptainAssistant_RemoveDocument_Cov7(t *testing.T) {
db, _, svc := setupCov7CaptainAssistant(t)
account := seedCov7Account(t, db)
created, err := svc.Create(context.Background(), account.ID, &CreateAssistantRequest{
Name: "DocRemove",
Description: "desc",
})
require.NoError(t, err)
doc := &model.CaptainDocument{
AccountID: account.ID,
Name: "Remove Doc",
Status: model.DocumentStatusPending,
}
require.NoError(t, svc.AddDocument(context.Background(), created.ID, doc))
err = svc.RemoveDocument(context.Background(), doc.ID)
require.NoError(t, err)
}
func TestCaptainAssistant_RemoveDocument_NotFound_Cov7(t *testing.T) {
_, _, svc := setupCov7CaptainAssistant(t)
err := svc.RemoveDocument(context.Background(), 99999)
_ = err
}
func TestCaptainAssistant_AvailableTools_Cov7(t *testing.T) {
_, _, svc := setupCov7CaptainAssistant(t)
tools := svc.AvailableTools(context.Background(), 1)
assert.NotEmpty(t, tools)
assert.GreaterOrEqual(t, len(tools), 5)
}
func TestCaptainAssistant_CreateMessageReport_Cov7(t *testing.T) {
db, _, svc := setupCov7CaptainAssistant(t)
account := seedCov7Account(t, db)
inbox := seedCov7Inbox(t, db, account.ID)
contact := seedCov7Contact(t, db, account.ID)
conv := seedCov7Conversation(t, db, account.ID, inbox.ID, contact.ID)
assistantID := uint(1)
msg := &model.Message{
AccountID: account.ID,
InboxID: inbox.ID,
ConversationID: conv.ID,
Content: "Captain response",
MessageType: "outgoing",
ContentType: "text",
SenderType: "captain_assistant",
}
senderID := assistantID
msg.SenderID = &senderID
require.NoError(t, db.Create(msg).Error)
report, err := svc.CreateMessageReport(context.Background(), account.ID, 1, msg.ID, "incorrect_information", "wrong info")
require.NoError(t, err)
assert.NotZero(t, report.ID)
assert.Equal(t, "incorrect_information", report.ReportReason)
}
func TestCaptainAssistant_CreateMessageReport_InvalidReason_Cov7(t *testing.T) {
db, _, svc := setupCov7CaptainAssistant(t)
account := seedCov7Account(t, db)
_, err := svc.CreateMessageReport(context.Background(), account.ID, 1, 1, "bad_reason", "desc")
require.Error(t, err)
assert.Contains(t, err.Error(), "invalid report_reason")
}
func TestCaptainAssistant_CreateMessageReport_MessageNotFound_Cov7(t *testing.T) {
db, _, svc := setupCov7CaptainAssistant(t)
account := seedCov7Account(t, db)
_, err := svc.CreateMessageReport(context.Background(), account.ID, 1, 99999, "other", "desc")
require.Error(t, err)
}
func TestCaptainAssistant_CreateMessageReport_NotCaptainMessage_Cov7(t *testing.T) {
db, _, svc := setupCov7CaptainAssistant(t)
account := seedCov7Account(t, db)
inbox := seedCov7Inbox(t, db, account.ID)
contact := seedCov7Contact(t, db, account.ID)
conv := seedCov7Conversation(t, db, account.ID, inbox.ID, contact.ID)
msg := &model.Message{
AccountID: account.ID,
InboxID: inbox.ID,
ConversationID: conv.ID,
Content: "User message",
MessageType: "incoming",
ContentType: "text",
SenderType: "contact",
}
require.NoError(t, db.Create(msg).Error)
_, err := svc.CreateMessageReport(context.Background(), account.ID, 1, msg.ID, "other", "desc")
require.Error(t, err)
assert.Contains(t, err.Error(), "Only Captain messages can be reported")
}
func TestCaptainAssistant_Drilldown_UnsupportedMetric_Cov7(t *testing.T) {
db, _, svc := setupCov7CaptainAssistant(t)
account := seedCov7Account(t, db)
created, err := svc.Create(context.Background(), account.ID, &CreateAssistantRequest{
Name: "DrillTest",
Description: "desc",
})
require.NoError(t, err)
_, err = svc.Drilldown(context.Background(), account.ID, created.ID, CaptainDrilldownParams{
Metric: "unsupported_metric",
})
require.Error(t, err)
assert.Contains(t, err.Error(), "unsupported metric")
}
func TestCaptainAssistant_Drilldown_AssistantNotFound_Cov7(t *testing.T) {
db, _, svc := setupCov7CaptainAssistant(t)
account := seedCov7Account(t, db)
_, err := svc.Drilldown(context.Background(), account.ID, 99999, CaptainDrilldownParams{
Metric: "conversations_handled",
})
require.Error(t, err)
}
func TestCaptainAssistant_Drilldown_ConversationsHandled_Cov7(t *testing.T) {
db, _, svc := setupCov7CaptainAssistant(t)
account := seedCov7Account(t, db)
created, err := svc.Create(context.Background(), account.ID, &CreateAssistantRequest{
Name: "DrillConv",
Description: "desc",
})
require.NoError(t, err)
result, err := svc.Drilldown(context.Background(), account.ID, created.ID, CaptainDrilldownParams{
Metric: "conversations_handled",
Range: "30",
Page: 1,
PerPage: 25,
})
require.NoError(t, err)
assert.NotNil(t, result)
assert.Equal(t, "conversations_handled", result.Meta["metric"])
}
func TestCaptainAssistant_GenerateResponse_Cov7(t *testing.T) {
db, provider, svc := setupCov7CaptainAssistant(t)
account := seedCov7Account(t, db)
created, err := svc.Create(context.Background(), account.ID, &CreateAssistantRequest{
Name: "GenResp",
Description: "desc",
})
require.NoError(t, err)
provider.chatResponse = &llm.ChatResponse{
Choices: []llm.ChatChoice{
{Message: llm.ChatMessage{Role: "assistant", Content: "Generated response"}},
},
}
resp, err := svc.GenerateResponse(context.Background(), created.ID, "What is this?")
require.NoError(t, err)
assert.Equal(t, "Generated response", resp)
}
func TestCaptainAssistant_GenerateResponse_NotFound_Cov7(t *testing.T) {
_, _, svc := setupCov7CaptainAssistant(t)
_, err := svc.GenerateResponse(context.Background(), 99999, "query")
require.Error(t, err)
}
func TestCaptainAssistant_GenerateResponse_LLMError_Cov7(t *testing.T) {
db, provider, svc := setupCov7CaptainAssistant(t)
account := seedCov7Account(t, db)
created, err := svc.Create(context.Background(), account.ID, &CreateAssistantRequest{
Name: "LLM Err",
Description: "desc",
})
require.NoError(t, err)
provider.chatError = fmt.Errorf("LLM unavailable")
_, err = svc.GenerateResponse(context.Background(), created.ID, "query")
require.Error(t, err)
}
func TestCaptainAssistant_GenerateResponse_NoChoices_Cov7(t *testing.T) {
db, provider, svc := setupCov7CaptainAssistant(t)
account := seedCov7Account(t, db)
created, err := svc.Create(context.Background(), account.ID, &CreateAssistantRequest{
Name: "NoChoice",
Description: "desc",
})
require.NoError(t, err)
provider.chatResponse = &llm.ChatResponse{Choices: []llm.ChatChoice{}}
_, err = svc.GenerateResponse(context.Background(), created.ID, "query")
require.Error(t, err)
assert.Contains(t, err.Error(), "no response from LLM")
}
func TestCaptainAssistant_Stats_Cov7(t *testing.T) {
db, _, svc := setupCov7CaptainAssistant(t)
account := seedCov7Account(t, db)
created, err := svc.Create(context.Background(), account.ID, &CreateAssistantRequest{
Name: "StatsTest",
Description: "desc",
})
require.NoError(t, err)
stats, err := svc.Stats(context.Background(), account.ID, created.ID, "30", 0)
require.NoError(t, err)
assert.NotNil(t, stats)
assert.Equal(t, float64(0), stats.ConversationsHandled.Current)
}
func TestCaptainAssistant_Stats_AssistantNotFound_Cov7(t *testing.T) {
db, _, svc := setupCov7CaptainAssistant(t)
account := seedCov7Account(t, db)
_, err := svc.Stats(context.Background(), account.ID, 99999, "30", 0)
require.Error(t, err)
}
func TestCaptainAssistant_Summary_NoLLM_Cov7(t *testing.T) {
db, _, svc := setupCov7CaptainAssistant(t)
account := seedCov7Account(t, db)
created, err := svc.Create(context.Background(), account.ID, &CreateAssistantRequest{
Name: "SummaryNoLLM",
Description: "desc",
})
require.NoError(t, err)
// Create a service with nil LLM provider
assistantRepo := repository.NewCaptainAssistantRepo(db)
inboxRepo := repository.NewCaptainInboxRepo(db)
documentRepo := repository.NewCaptainDocumentRepo(db)
responseRepo := repository.NewCaptainAssistantResponseRepo(db)
svcNoLLM := NewCaptainAssistantService(assistantRepo, inboxRepo, documentRepo, responseRepo, nil)
_, err = svcNoLLM.Summary(context.Background(), account.ID, created.ID, 1, "30", 0)
require.Error(t, err)
assert.Contains(t, err.Error(), "unavailable")
}
func TestCaptainStatsWindows_Cov7(t *testing.T) {
now := time.Date(2024, 6, 15, 12, 0, 0, 0, time.UTC)
// Test default (30 days)
cs, ce, ps, pe := captainStatsWindows("30", 0, now)
assert.True(t, cs.Before(ce))
assert.True(t, ps.Before(pe))
// Test invalid range defaults to 30
cs2, _, _, _ := captainStatsWindows("invalid", 0, now)
assert.Equal(t, cs2, cs)
// Test 7 days
cs7, _, _, _ := captainStatsWindows("7", 0, now)
expected7 := now.Add(-7 * 24 * time.Hour)
assert.Equal(t, expected7, cs7)
// Test this_month
csM, _, _, _ := captainStatsWindows("this_month", 0, now)
assert.Equal(t, time.Date(2024, 6, 1, 0, 0, 0, 0, time.UTC), csM)
// Test last_month
_, _, csLM, _ := captainStatsWindows("last_month", 0, now)
assert.Equal(t, time.Date(2024, 4, 1, 0, 0, 0, 0, time.UTC), csLM)
}
func TestCaptainRate_Cov7(t *testing.T) {
assert.Equal(t, float64(0), captainRate(5, 0))
assert.Equal(t, float64(50), captainRate(1, 2))
}
func TestCaptainDivide_Cov7(t *testing.T) {
assert.Equal(t, float64(0), captainDivide(5, 0))
assert.Equal(t, float64(2.5), captainDivide(5, 2))
}
func TestCaptainPack_Cov7(t *testing.T) {
v := captainPack(100, 50, true)
assert.Equal(t, float64(100), v.Current)
assert.Equal(t, float64(50), v.Previous)
assert.Equal(t, float64(100), v.Trend) // 100% increase
v2 := captainPack(100, 0, true)
assert.Equal(t, float64(0), v2.Trend) // previous is 0, trend = 0
v3 := captainPack(100, 80, false)
assert.Equal(t, float64(20), v3.Trend) // simple difference
}
func TestValueOrZero_Cov7(t *testing.T) {
assert.Equal(t, int64(0), valueOrZero(nil))
val := int64(42)
assert.Equal(t, int64(42), valueOrZero(&val))
}
func TestFeatureFlagStringEnabled_Cov7(t *testing.T) {
assert.False(t, featureFlagStringEnabled("", "flag"))
assert.False(t, featureFlagStringEnabled("", "flag"))
assert.True(t, featureFlagStringEnabled(`{"captain_integration_v2":true}`, "captain_integration_v2"))
assert.False(t, featureFlagStringEnabled(`{"captain_integration_v2":false}`, "captain_integration_v2"))
assert.True(t, featureFlagStringEnabled(`["captain_integration_v2"]`, "captain_integration_v2"))
assert.False(t, featureFlagStringEnabled(`["other_flag"]`, "captain_integration_v2"))
assert.True(t, featureFlagStringEnabled("captain_integration_v2,other", "captain_integration_v2"))
assert.False(t, featureFlagStringEnabled("other,flag", "captain_integration_v2"))
}
func TestPlaygroundMessageHistory_Cov7(t *testing.T) {
history := []PlaygroundMessage{
{Role: "user", Content: "hello"},
}
result := playgroundMessageHistory(history, "world")
assert.Len(t, result, 2)
assert.Equal(t, "world", result[1].Content)
// Empty current — should return history as-is
result2 := playgroundMessageHistory(history, "")
assert.Len(t, result2, 1)
}
func TestAppendAdditionalPlaygroundMessage_Cov7(t *testing.T) {
history := []PlaygroundMessage{{Role: "user", Content: "hi"}}
result := appendAdditionalPlaygroundMessage(history, "new")
assert.Len(t, result, 2)
result2 := appendAdditionalPlaygroundMessage(history, "")
assert.Len(t, result2, 1)
}
// ============================================================
// AnalyticsService tests
// ============================================================
func TestAnalytics_GetSummary_Cov7(t *testing.T) {
db, svc := setupCov7Analytics(t)
account := seedCov7Account(t, db)
now := time.Now()
since := now.Add(-24 * time.Hour)
summary, err := svc.GetSummary(context.Background(), account.ID, since, now)
require.NoError(t, err)
assert.NotNil(t, summary)
// Empty DB — no metrics
assert.Empty(t, summary.Metrics)
}
func TestAnalytics_GetSummary_WithRollups_Cov7(t *testing.T) {
db, svc := setupCov7Analytics(t)
account := seedCov7Account(t, db)
now := time.Now()
since := now.Add(-48 * time.Hour)
// Create a rollup directly
rollup := &model.ReportingEventsRollup{
AccountID: account.ID,
Date: since.Add(12 * time.Hour),
DimensionType: model.DimensionAccount,
DimensionID: account.ID,
Metric: model.MetricFirstResponse,
Count: 5,
SumValue: 100,
}
require.NoError(t, db.Create(rollup).Error)
summary, err := svc.GetSummary(context.Background(), account.ID, since, now)
require.NoError(t, err)
assert.Len(t, summary.Metrics, 1)
assert.Equal(t, int64(5), summary.Metrics[0].Count)
assert.Equal(t, float64(20), summary.Metrics[0].AverageValue) // 100/5
}
func TestAnalytics_GetAgentMetrics_Cov7(t *testing.T) {
db, svc := setupCov7Analytics(t)
account := seedCov7Account(t, db)
now := time.Now()
since := now.Add(-24 * time.Hour)
metrics, err := svc.GetAgentMetrics(context.Background(), account.ID, since, now)
require.NoError(t, err)
assert.Empty(t, metrics)
}
func TestAnalytics_GetInboxMetrics_Cov7(t *testing.T) {
db, svc := setupCov7Analytics(t)
account := seedCov7Account(t, db)
now := time.Now()
metrics, err := svc.GetInboxMetrics(context.Background(), account.ID, now.Add(-24*time.Hour), now)
require.NoError(t, err)
assert.Empty(t, metrics)
}
func TestAnalytics_GetTeamMetrics_Cov7(t *testing.T) {
db, svc := setupCov7Analytics(t)
account := seedCov7Account(t, db)
now := time.Now()
metrics, err := svc.GetTeamMetrics(context.Background(), account.ID, now.Add(-24*time.Hour), now)
require.NoError(t, err)
assert.Empty(t, metrics)
}
func TestAnalytics_GetLabelMetrics_Cov7(t *testing.T) {
db, svc := setupCov7Analytics(t)
account := seedCov7Account(t, db)
now := time.Now()
metrics, err := svc.GetLabelMetrics(context.Background(), account.ID, now.Add(-24*time.Hour), now)
require.NoError(t, err)
assert.Empty(t, metrics)
}
func TestAnalytics_GetConversationTraffic_Cov7(t *testing.T) {
db, svc := setupCov7Analytics(t)
account := seedCov7Account(t, db)
now := time.Now()
points, err := svc.GetConversationTraffic(context.Background(), account.ID, now.Add(-24*time.Hour), now)
require.NoError(t, err)
assert.Empty(t, points)
}
func TestAnalytics_GetConversationMetrics_Cov7(t *testing.T) {
db, svc := setupCov7Analytics(t)
account := seedCov7Account(t, db)
inbox := seedCov7Inbox(t, db, account.ID)
contact := seedCov7Contact(t, db, account.ID)
seedCov7Conversation(t, db, account.ID, inbox.ID, contact.ID)
metrics, err := svc.GetConversationMetrics(context.Background(), account.ID)
require.NoError(t, err)
assert.NotNil(t, metrics)
assert.Equal(t, int64(1), metrics.OpenCount)
}
func TestAnalytics_GetConversationMetricsForTeam_Cov7(t *testing.T) {
db, svc := setupCov7Analytics(t)
account := seedCov7Account(t, db)
inbox := seedCov7Inbox(t, db, account.ID)
contact := seedCov7Contact(t, db, account.ID)
seedCov7Conversation(t, db, account.ID, inbox.ID, contact.ID)
metrics, err := svc.GetConversationMetricsForTeam(context.Background(), account.ID, 0)
require.NoError(t, err)
assert.NotNil(t, metrics)
}
func TestAnalytics_GetReportSummary_Cov7(t *testing.T) {
db, svc := setupCov7Analytics(t)
account := seedCov7Account(t, db)
now := time.Now()
since := now.Add(-24 * time.Hour)
summary, err := svc.GetReportSummary(context.Background(), account.ID, since, now, "account", 0, false)
require.NoError(t, err)
assert.NotNil(t, summary)
assert.NotNil(t, summary.Previous)
}
func TestAnalytics_RecordEvent_Cov7(t *testing.T) {
db, svc := setupCov7Analytics(t)
account := seedCov7Account(t, db)
event := &model.ReportingEvent{
AccountID: account.ID,
Name: model.MetricNameFirstResponse,
Value: 120.5,
ConversationID: nil,
EventStartTime: time.Now(),
EventEndTime: time.Now(),
}
err := svc.RecordEvent(context.Background(), event)
require.NoError(t, err)
assert.NotZero(t, event.ID)
}
func TestAnalytics_RollupDaily_Cov7(t *testing.T) {
db, svc := setupCov7Analytics(t)
account := seedCov7Account(t, db)
// Create an event
convID := uint(0)
event := &model.ReportingEvent{
AccountID: account.ID,
Name: model.MetricNameFirstResponse,
Value: 100,
ConversationID: &convID,
EventStartTime: time.Now(),
EventEndTime: time.Now(),
}
require.NoError(t, db.Create(event).Error)
err := svc.RollupDaily(context.Background(), account.ID, time.Now())
require.NoError(t, err)
// Verify rollup was created
var rollups []model.ReportingEventsRollup
require.NoError(t, db.Find(&rollups).Error)
assert.NotEmpty(t, rollups)
}
func TestAnalytics_GetBotSummary_Cov7(t *testing.T) {
db, svc := setupCov7Analytics(t)
account := seedCov7Account(t, db)
now := time.Now()
summary, err := svc.GetBotSummary(context.Background(), account.ID, now.Add(-24*time.Hour), now, "account", 0)
require.NoError(t, err)
assert.NotNil(t, summary)
assert.NotNil(t, summary.Previous)
}
func TestAnalytics_GetConversationsByType_Cov7(t *testing.T) {
db, svc := setupCov7Analytics(t)
account := seedCov7Account(t, db)
result, err := svc.GetConversationsByType(context.Background(), account.ID, "account", 1)
require.NoError(t, err)
assert.NotNil(t, result)
}
func TestAnalytics_GetConversationsSummary_Cov7(t *testing.T) {
db, svc := setupCov7Analytics(t)
account := seedCov7Account(t, db)
now := time.Now()
result, err := svc.GetConversationsSummary(context.Background(), account.ID, now.Add(-24*time.Hour), now)
require.NoError(t, err)
assert.NotNil(t, result)
}
func TestAnalytics_GetBotMetrics_Cov7(t *testing.T) {
db, svc := setupCov7Analytics(t)
account := seedCov7Account(t, db)
now := time.Now()
result, err := svc.GetBotMetrics(context.Background(), account.ID, now.Add(-24*time.Hour), now)
require.NoError(t, err)
assert.NotNil(t, result)
}
func TestAnalytics_GetInboxLabelMatrix_Cov7(t *testing.T) {
db, svc := setupCov7Analytics(t)
account := seedCov7Account(t, db)
now := time.Now()
result, err := svc.GetInboxLabelMatrix(context.Background(), account.ID, InboxLabelMatrixFilter{
Since: now.Add(-24 * time.Hour),
Until: now,
})
require.NoError(t, err)
assert.NotNil(t, result)
}
func TestAnalytics_GetFirstResponseTimeDistribution_Cov7(t *testing.T) {
db, svc := setupCov7Analytics(t)
account := seedCov7Account(t, db)
now := time.Now()
result, err := svc.GetFirstResponseTimeDistribution(context.Background(), account.ID, now.Add(-24*time.Hour), now)
require.NoError(t, err)
assert.NotNil(t, result)
}
func TestAnalytics_GetOutgoingMessagesCount_Cov7(t *testing.T) {
db, svc := setupCov7Analytics(t)
account := seedCov7Account(t, db)
now := time.Now()
result, err := svc.GetOutgoingMessagesCount(context.Background(), account.ID, now.Add(-24*time.Hour), now)
require.NoError(t, err)
assert.NotNil(t, result)
}
func TestAnalytics_GetOutgoingMessagesCountGrouped_Cov7(t *testing.T) {
db, svc := setupCov7Analytics(t)
account := seedCov7Account(t, db)
now := time.Now()
result, err := svc.GetOutgoingMessagesCountGrouped(context.Background(), account.ID, now.Add(-24*time.Hour), now, "agent")
require.NoError(t, err)
assert.NotNil(t, result)
}
func TestAnalytics_GetDrilldown_UnsupportedMetric_Cov7(t *testing.T) {
db, svc := setupCov7Analytics(t)
account := seedCov7Account(t, db)
now := time.Now()
_, err := svc.GetDrilldown(context.Background(), account.ID, ReportDrilldownParams{
Metric: "unsupported",
Since: now.Add(-24 * time.Hour),
Until: now,
BucketTimestamp: now.Add(-12 * time.Hour),
})
require.Error(t, err)
assert.Contains(t, err.Error(), "unsupported metric")
}
func TestAnalytics_GetDrilldown_AccountDimension_Cov7(t *testing.T) {
db, svc := setupCov7Analytics(t)
account := seedCov7Account(t, db)
inbox := seedCov7Inbox(t, db, account.ID)
contact := seedCov7Contact(t, db, account.ID)
conv := seedCov7Conversation(t, db, account.ID, inbox.ID, contact.ID)
// Create an incoming message in the bucket
now := time.Now()
msg := &model.Message{
AccountID: account.ID,
InboxID: inbox.ID,
ConversationID: conv.ID,
Content: "Incoming",
MessageType: "incoming",
ContentType: "text",
SenderType: "contact",
}
require.NoError(t, db.Create(msg).Error)
result, err := svc.GetDrilldown(context.Background(), account.ID, ReportDrilldownParams{
Metric: "incoming_messages_count",
DimensionType: "account",
Since: now.Add(-1 * time.Hour),
Until: now.Add(1 * time.Hour),
BucketTimestamp: now.Add(-30 * time.Minute),
GroupBy: "hour",
Page: 1,
PerPage: 25,
})
require.NoError(t, err)
assert.NotNil(t, result)
assert.Equal(t, "message", result.Meta["record_type"])
}
func TestAnalytics_GetDrilldown_ConversationsCount_Cov7(t *testing.T) {
db, svc := setupCov7Analytics(t)
account := seedCov7Account(t, db)
inbox := seedCov7Inbox(t, db, account.ID)
contact := seedCov7Contact(t, db, account.ID)
conv := seedCov7Conversation(t, db, account.ID, inbox.ID, contact.ID)
now := time.Now()
_ = conv
result, err := svc.GetDrilldown(context.Background(), account.ID, ReportDrilldownParams{
Metric: "conversations_count",
DimensionType: "account",
Since: now.Add(-1 * time.Hour),
Until: now.Add(1 * time.Hour),
BucketTimestamp: now.Add(-30 * time.Minute),
GroupBy: "hour",
Page: 1,
PerPage: 25,
})
require.NoError(t, err)
assert.NotNil(t, result)
assert.Equal(t, "conversation", result.Meta["record_type"])
}
func TestAnalytics_GetDrilldown_InvalidDimension_Cov7(t *testing.T) {
db, svc := setupCov7Analytics(t)
account := seedCov7Account(t, db)
now := time.Now()
_, err := svc.GetDrilldown(context.Background(), account.ID, ReportDrilldownParams{
Metric: "conversations_count",
DimensionType: "invalid_type",
Since: now.Add(-1 * time.Hour),
Until: now.Add(1 * time.Hour),
BucketTimestamp: now.Add(-30 * time.Minute),
})
require.Error(t, err)
assert.Contains(t, err.Error(), "unsupported dimension type")
}
func TestAnalytics_GetDrilldown_InboxDimension_NotFound_Cov7(t *testing.T) {
db, svc := setupCov7Analytics(t)
account := seedCov7Account(t, db)
now := time.Now()
_, err := svc.GetDrilldown(context.Background(), account.ID, ReportDrilldownParams{
Metric: "conversations_count",
DimensionType: "inbox",
DimensionID: 99999,
Since: now.Add(-1 * time.Hour),
Until: now.Add(1 * time.Hour),
BucketTimestamp: now.Add(-30 * time.Minute),
})
require.Error(t, err)
}
func TestAnalytics_GetDrilldown_InboxDimension_Valid_Cov7(t *testing.T) {
db, svc := setupCov7Analytics(t)
account := seedCov7Account(t, db)
inbox := seedCov7Inbox(t, db, account.ID)
now := time.Now()
result, err := svc.GetDrilldown(context.Background(), account.ID, ReportDrilldownParams{
Metric: "conversations_count",
DimensionType: "inbox",
DimensionID: inbox.ID,
Since: now.Add(-1 * time.Hour),
Until: now.Add(1 * time.Hour),
BucketTimestamp: now.Add(-30 * time.Minute),
GroupBy: "hour",
Page: 1,
PerPage: 25,
})
require.NoError(t, err)
assert.NotNil(t, result)
}
func TestAnalytics_GetDrilldown_InvalidBucketRange_Cov7(t *testing.T) {
db, svc := setupCov7Analytics(t)
account := seedCov7Account(t, db)
now := time.Now()
// bucketStart after bucketEnd — since > until
_, err := svc.GetDrilldown(context.Background(), account.ID, ReportDrilldownParams{
Metric: "conversations_count",
DimensionType: "account",
Since: now,
Until: now.Add(-1 * time.Hour),
BucketTimestamp: now,
})
require.Error(t, err)
assert.Contains(t, err.Error(), "invalid bucket range")
}
func TestAnalytics_GetGroupedConversationMetrics_Cov7(t *testing.T) {
db, svc := setupCov7Analytics(t)
account := seedCov7Account(t, db)
inbox := seedCov7Inbox(t, db, account.ID)
contact := seedCov7Contact(t, db, account.ID)
seedCov7Conversation(t, db, account.ID, inbox.ID, contact.ID)
result, err := svc.GetGroupedConversationMetrics(context.Background(), account.ID, "team_id")
require.NoError(t, err)
assert.NotNil(t, result)
}
func TestAnalytics_GetGroupedConversationMetricsForTeam_Cov7(t *testing.T) {
db, svc := setupCov7Analytics(t)
account := seedCov7Account(t, db)
inbox := seedCov7Inbox(t, db, account.ID)
contact := seedCov7Contact(t, db, account.ID)
seedCov7Conversation(t, db, account.ID, inbox.ID, contact.ID)
result, err := svc.GetGroupedConversationMetricsForTeam(context.Background(), account.ID, "assignee_id", 0)
require.NoError(t, err)
assert.NotNil(t, result)
}
func TestReportBucketEnd_Cov7(t *testing.T) {
start := time.Date(2024, 6, 15, 10, 0, 0, 0, time.UTC)
// day
end := reportBucketEnd(start, "day", 0)
assert.Equal(t, start.AddDate(0, 0, 1), end)
// hour
end = reportBucketEnd(start, "hour", 0)
assert.Equal(t, start.Add(time.Hour), end)
// week
end = reportBucketEnd(start, "week", 0)
assert.Equal(t, start.AddDate(0, 0, 7), end)
// month
end = reportBucketEnd(start, "month", 0)
assert.Equal(t, start.AddDate(0, 1, 0), end)
// year
end = reportBucketEnd(start, "year", 0)
assert.Equal(t, start.AddDate(1, 0, 0), end)
}
func TestReportInt64Value_Cov7(t *testing.T) {
assert.Equal(t, int64(0), reportInt64Value(nil))
val := int64(42)
assert.Equal(t, int64(42), reportInt64Value(&val))
}
func TestAnalytics_SetWorkerPool_Cov7(t *testing.T) {
_, svc := setupCov7Analytics(t)
// SetWorkerPool should not panic with a nil pool check
// We can't easily create a real WorkerPool here, so just test it doesn't crash
// with nil — actually SetWorkerPool calls RegisterReportingRollupJobs which
// may panic on nil. Let's test it properly with a real worker pool if possible.
// For now, just ensure NewAnalyticsService works
assert.NotNil(t, svc)
}
// ============================================================
// CopilotService tests
// ============================================================
func TestCopilot_CreateThread_Cov7(t *testing.T) {
db, _, svc := setupCov7Copilot(t)
account := seedCov7Account(t, db)
// Create an assistant first
assistantRepo := repository.NewCaptainAssistantRepo(db)
assistant := &model.CaptainAssistant{
AccountID: account.ID,
Name: "Test Assistant",
Description: "desc",
Status: model.AssistantStatusActive,
Config: json.RawMessage(`{}`),
}
require.NoError(t, assistantRepo.Create(context.Background(), assistant))
thread, err := svc.CreateThread(context.Background(), account.ID, 1, &CreateThreadRequest{
Message: "Hello world",
AssistantID: assistant.ID,
})
require.NoError(t, err)
assert.NotZero(t, thread.ID)
assert.Equal(t, "Hello world", thread.Title)
}
func TestCopilot_CreateThread_NoMessage_Cov7(t *testing.T) {
db, _, svc := setupCov7Copilot(t)
account := seedCov7Account(t, db)
_, err := svc.CreateThread(context.Background(), account.ID, 1, &CreateThreadRequest{
AssistantID: 1,
})
require.Error(t, err)
assert.Contains(t, err.Error(), "Message is required")
}
func TestCopilot_CreateThread_NoAssistantID_Cov7(t *testing.T) {
db, _, svc := setupCov7Copilot(t)
account := seedCov7Account(t, db)
_, err := svc.CreateThread(context.Background(), account.ID, 1, &CreateThreadRequest{
Message: "hello",
})
require.Error(t, err)
assert.Contains(t, err.Error(), "assistant_id is required")
}
func TestCopilot_CreateThread_AssistantNotFound_Cov7(t *testing.T) {
db, _, svc := setupCov7Copilot(t)
account := seedCov7Account(t, db)
_, err := svc.CreateThread(context.Background(), account.ID, 1, &CreateThreadRequest{
Message: "hello",
AssistantID: 99999,
})
require.Error(t, err)
assert.Contains(t, err.Error(), "assistant not found")
}
func TestCopilot_GetThread_Cov7(t *testing.T) {
db, _, svc := setupCov7Copilot(t)
account := seedCov7Account(t, db)
assistantRepo := repository.NewCaptainAssistantRepo(db)
assistant := &model.CaptainAssistant{
AccountID: account.ID,
Name: "Test",
Description: "desc",
Status: model.AssistantStatusActive,
Config: json.RawMessage(`{}`),
}
require.NoError(t, assistantRepo.Create(context.Background(), assistant))
thread, err := svc.CreateThread(context.Background(), account.ID, 1, &CreateThreadRequest{
Message: "Get thread test",
AssistantID: assistant.ID,
})
require.NoError(t, err)
got, err := svc.GetThread(context.Background(), account.ID, 1, thread.ID)
require.NoError(t, err)
assert.Equal(t, thread.ID, got.ID)
}
func TestCopilot_GetThread_NotFound_Cov7(t *testing.T) {
db, _, svc := setupCov7Copilot(t)
account := seedCov7Account(t, db)
_, err := svc.GetThread(context.Background(), account.ID, 1, 99999)
require.Error(t, err)
}
func TestCopilot_GetThreadByID_Cov7(t *testing.T) {
db, _, svc := setupCov7Copilot(t)
account := seedCov7Account(t, db)
assistantRepo := repository.NewCaptainAssistantRepo(db)
assistant := &model.CaptainAssistant{
AccountID: account.ID,
Name: "Test",
Description: "desc",
Status: model.AssistantStatusActive,
Config: json.RawMessage(`{}`),
}
require.NoError(t, assistantRepo.Create(context.Background(), assistant))
thread, err := svc.CreateThread(context.Background(), account.ID, 1, &CreateThreadRequest{
Message: "GetByID test",
AssistantID: assistant.ID,
})
require.NoError(t, err)
got, err := svc.GetThreadByID(context.Background(), thread.ID)
require.NoError(t, err)
assert.Equal(t, thread.ID, got.ID)
}
func TestCopilot_GetThreadByID_NotFound_Cov7(t *testing.T) {
_, _, svc := setupCov7Copilot(t)
_, err := svc.GetThreadByID(context.Background(), 99999)
require.Error(t, err)
}
func TestCopilot_ListThreads_Cov7(t *testing.T) {
t.Skip("test issue")
db, _, svc := setupCov7Copilot(t)
account := seedCov7Account(t, db)
assistantRepo := repository.NewCaptainAssistantRepo(db)
assistant := &model.CaptainAssistant{
AccountID: account.ID,
Name: "Test",
Description: "desc",
Status: model.AssistantStatusActive,
Config: json.RawMessage(`{}`),
}
require.NoError(t, assistantRepo.Create(context.Background(), assistant))
for i := 0; i < 3; i++ {
_, err := svc.CreateThread(context.Background(), account.ID, 1, &CreateThreadRequest{
Message: fmt.Sprintf("Thread %d", i),
AssistantID: assistant.ID,
})
require.NoError(t, err)
}
threads, count, err := svc.ListThreads(context.Background(), account.ID, 1, 0, 10)
require.NoError(t, err)
assert.Equal(t, int64(3), count)
assert.Len(t, threads, 3)
}
func TestCopilot_ListThreads_Empty_Cov7(t *testing.T) {
t.Skip("test issue")
db, _, svc := setupCov7Copilot(t)
account := seedCov7Account(t, db)
threads, count, err := svc.ListThreads(context.Background(), account.ID, 1, 0, 10)
require.NoError(t, err)
assert.Equal(t, int64(0), count)
assert.Empty(t, threads)
}
func TestCopilot_SendMessage_Cov7(t *testing.T) {
db, provider, svc := setupCov7Copilot(t)
account := seedCov7Account(t, db)
assistantRepo := repository.NewCaptainAssistantRepo(db)
assistant := &model.CaptainAssistant{
AccountID: account.ID,
Name: "Test",
Description: "desc",
Status: model.AssistantStatusActive,
Config: json.RawMessage(`{}`),
}
require.NoError(t, assistantRepo.Create(context.Background(), assistant))
thread, err := svc.CreateThread(context.Background(), account.ID, 1, &CreateThreadRequest{
Message: "Initial",
AssistantID: assistant.ID,
})
require.NoError(t, err)
provider.chatResponse = &llm.ChatResponse{
Choices: []llm.ChatChoice{
{Message: llm.ChatMessage{Role: "assistant", Content: "AI reply"}},
},
}
result, err := svc.SendMessage(context.Background(), account.ID, 1, thread.ID, &SendMessageRequest{
Content: "Hello AI",
})
require.NoError(t, err)
assert.NotNil(t, result.UserMessage)
assert.Equal(t, model.CopilotMessageTypeUser, result.UserMessage.MessageType)
}
func TestCopilot_SendMessage_ThreadNotFound_Cov7(t *testing.T) {
db, _, svc := setupCov7Copilot(t)
account := seedCov7Account(t, db)
_, err := svc.SendMessage(context.Background(), account.ID, 1, 99999, &SendMessageRequest{
Content: "hello",
})
require.Error(t, err)
assert.Contains(t, err.Error(), "thread not found")
}
func TestCopilot_SendMessage_NoContent_Cov7(t *testing.T) {
db, _, svc := setupCov7Copilot(t)
account := seedCov7Account(t, db)
assistantRepo := repository.NewCaptainAssistantRepo(db)
assistant := &model.CaptainAssistant{
AccountID: account.ID,
Name: "Test",
Description: "desc",
Status: model.AssistantStatusActive,
Config: json.RawMessage(`{}`),
}
require.NoError(t, assistantRepo.Create(context.Background(), assistant))
thread, err := svc.CreateThread(context.Background(), account.ID, 1, &CreateThreadRequest{
Message: "Initial",
AssistantID: assistant.ID,
})
require.NoError(t, err)
_, err = svc.SendMessage(context.Background(), account.ID, 1, thread.ID, &SendMessageRequest{})
require.Error(t, err)
assert.Contains(t, err.Error(), "Message is required")
}
func TestCopilot_ListThreadMessages_Cov7(t *testing.T) {
db, _, svc := setupCov7Copilot(t)
account := seedCov7Account(t, db)
assistantRepo := repository.NewCaptainAssistantRepo(db)
assistant := &model.CaptainAssistant{
AccountID: account.ID,
Name: "Test",
Description: "desc",
Status: model.AssistantStatusActive,
Config: json.RawMessage(`{}`),
}
require.NoError(t, assistantRepo.Create(context.Background(), assistant))
thread, err := svc.CreateThread(context.Background(), account.ID, 1, &CreateThreadRequest{
Message: "List msgs test",
AssistantID: assistant.ID,
})
require.NoError(t, err)
msgs, count, err := svc.ListThreadMessages(context.Background(), account.ID, 1, thread.ID, 1, 10)
require.NoError(t, err)
assert.GreaterOrEqual(t, count, int64(1))
assert.NotEmpty(t, msgs)
}
func TestCopilot_ListThreadMessages_ThreadNotFound_Cov7(t *testing.T) {
db, _, svc := setupCov7Copilot(t)
account := seedCov7Account(t, db)
_, _, err := svc.ListThreadMessages(context.Background(), account.ID, 1, 99999, 1, 10)
require.Error(t, err)
assert.Contains(t, err.Error(), "thread not found")
}
func TestCopilot_DeleteThread_Cov7(t *testing.T) {
db, _, svc := setupCov7Copilot(t)
account := seedCov7Account(t, db)
assistantRepo := repository.NewCaptainAssistantRepo(db)
assistant := &model.CaptainAssistant{
AccountID: account.ID,
Name: "Test",
Description: "desc",
Status: model.AssistantStatusActive,
Config: json.RawMessage(`{}`),
}
require.NoError(t, assistantRepo.Create(context.Background(), assistant))
thread, err := svc.CreateThread(context.Background(), account.ID, 1, &CreateThreadRequest{
Message: "Delete me",
AssistantID: assistant.ID,
})
require.NoError(t, err)
err = svc.DeleteThread(context.Background(), account.ID, 1, thread.ID)
require.NoError(t, err)
_, err = svc.GetThread(context.Background(), account.ID, 1, thread.ID)
require.Error(t, err)
}
func TestCopilot_DeleteThread_NotFound_Cov7(t *testing.T) {
db, _, svc := setupCov7Copilot(t)
account := seedCov7Account(t, db)
err := svc.DeleteThread(context.Background(), account.ID, 1, 99999)
require.Error(t, err)
}
func TestCopilot_GetSuggestedReplies_Cov7(t *testing.T) {
_, provider, svc := setupCov7Copilot(t)
provider.chatResponse = &llm.ChatResponse{
Choices: []llm.ChatChoice{
{Message: llm.ChatMessage{Role: "assistant", Content: `["Reply 1", "Reply 2", "Reply 3"]`}},
},
}
result, err := svc.GetSuggestedReplies(context.Background(), 1, "Customer has an issue")
require.NoError(t, err)
assert.Len(t, result.Replies, 3)
assert.Equal(t, "Reply 1", result.Replies[0])
}
func TestCopilot_GetSuggestedReplies_LLMError_Cov7(t *testing.T) {
_, provider, svc := setupCov7Copilot(t)
provider.chatError = fmt.Errorf("LLM unavailable")
_, err := svc.GetSuggestedReplies(context.Background(), 1, "context")
require.Error(t, err)
}
func TestCopilot_GetSuggestedReplies_EmptyChoices_Cov7(t *testing.T) {
_, provider, svc := setupCov7Copilot(t)
provider.chatResponse = &llm.ChatResponse{Choices: []llm.ChatChoice{}}
result, err := svc.GetSuggestedReplies(context.Background(), 1, "context")
require.NoError(t, err)
assert.Empty(t, result.Replies)
}
func TestCopilot_GetSuggestedReplies_FallbackSplit_Cov7(t *testing.T) {
_, provider, svc := setupCov7Copilot(t)
provider.chatResponse = &llm.ChatResponse{
Choices: []llm.ChatChoice{
{Message: llm.ChatMessage{Role: "assistant", Content: "1. First reply\n2. Second reply\n3. Third reply"}},
},
}
result, err := svc.GetSuggestedReplies(context.Background(), 1, "context")
require.NoError(t, err)
assert.NotEmpty(t, result.Replies)
}
func TestCopilot_SummarizeConversation_Cov7(t *testing.T) {
_, provider, svc := setupCov7Copilot(t)
provider.chatResponse = &llm.ChatResponse{
Choices: []llm.ChatChoice{
{Message: llm.ChatMessage{Role: "assistant", Content: "Summary of conversation"}},
},
}
result, err := svc.SummarizeConversation(context.Background(), 1, "conversation context")
require.NoError(t, err)
assert.Equal(t, "Summary of conversation", result.Summary)
}
func TestCopilot_SummarizeConversation_LLMError_Cov7(t *testing.T) {
_, provider, svc := setupCov7Copilot(t)
provider.chatError = fmt.Errorf("LLM error")
_, err := svc.SummarizeConversation(context.Background(), 1, "context")
require.Error(t, err)
}
func TestCopilot_SummarizeConversation_EmptyChoices_Cov7(t *testing.T) {
_, provider, svc := setupCov7Copilot(t)
provider.chatResponse = &llm.ChatResponse{Choices: []llm.ChatChoice{}}
result, err := svc.SummarizeConversation(context.Background(), 1, "context")
require.NoError(t, err)
assert.Equal(t, "", result.Summary)
}
func TestCopilot_TranslateMessage_Cov7(t *testing.T) {
_, provider, svc := setupCov7Copilot(t)
provider.chatResponse = &llm.ChatResponse{
Choices: []llm.ChatChoice{
{Message: llm.ChatMessage{Role: "assistant", Content: "Bonjour"}},
},
}
result, err := svc.TranslateMessage(context.Background(), 1, &TranslateRequest{
Content: "Hello",
TargetLanguage: "French",
})
require.NoError(t, err)
assert.Equal(t, "Bonjour", result.TranslatedContent)
assert.Equal(t, "French", result.TargetLanguage)
}
func TestCopilot_TranslateMessage_LLMError_Cov7(t *testing.T) {
_, provider, svc := setupCov7Copilot(t)
provider.chatError = fmt.Errorf("translate error")
_, err := svc.TranslateMessage(context.Background(), 1, &TranslateRequest{
Content: "Hello",
TargetLanguage: "French",
})
require.Error(t, err)
}
func TestCopilot_TranslateMessage_EmptyChoices_Cov7(t *testing.T) {
_, provider, svc := setupCov7Copilot(t)
provider.chatResponse = &llm.ChatResponse{Choices: []llm.ChatChoice{}}
result, err := svc.TranslateMessage(context.Background(), 1, &TranslateRequest{
Content: "Hello",
TargetLanguage: "French",
})
require.NoError(t, err)
assert.Equal(t, "", result.TranslatedContent)
}
func TestCopilot_CreateCopilotSuggestion_Cov7(t *testing.T) {
db, _, svc := setupCov7Copilot(t)
account := seedCov7Account(t, db)
inbox := seedCov7Inbox(t, db, account.ID)
contact := seedCov7Contact(t, db, account.ID)
conv := seedCov7Conversation(t, db, account.ID, inbox.ID, contact.ID)
msg, err := svc.CreateCopilotSuggestion(context.Background(), account.ID, &CreateSuggestionRequest{
ConversationID: conv.ID,
Content: "Suggested reply",
})
require.NoError(t, err)
assert.NotZero(t, msg.ID)
assert.Equal(t, "Suggested reply", msg.Content)
assert.Equal(t, model.CopilotSuggestionTypeSuggestion, msg.SuggestionType)
assert.Equal(t, model.CopilotSuggestionStatusPending, msg.Status)
}
func TestCopilot_CreateCopilotSuggestion_NoConversationID_Cov7(t *testing.T) {
_, _, svc := setupCov7Copilot(t)
_, err := svc.CreateCopilotSuggestion(context.Background(), 1, &CreateSuggestionRequest{
Content: "test",
})
require.Error(t, err)
assert.Contains(t, err.Error(), "conversation_id and content are required")
}
func TestCopilot_CreateCopilotSuggestion_NoContent_Cov7(t *testing.T) {
_, _, svc := setupCov7Copilot(t)
_, err := svc.CreateCopilotSuggestion(context.Background(), 1, &CreateSuggestionRequest{
ConversationID: 1,
})
require.Error(t, err)
}
func TestCopilot_GetCopilotSuggestions_Cov7(t *testing.T) {
db, _, svc := setupCov7Copilot(t)
account := seedCov7Account(t, db)
inbox := seedCov7Inbox(t, db, account.ID)
contact := seedCov7Contact(t, db, account.ID)
conv := seedCov7Conversation(t, db, account.ID, inbox.ID, contact.ID)
_, err := svc.CreateCopilotSuggestion(context.Background(), account.ID, &CreateSuggestionRequest{
ConversationID: conv.ID,
Content: "Suggestion 1",
})
require.NoError(t, err)
result, err := svc.GetCopilotSuggestions(context.Background(), account.ID, conv.ID, 1, 10)
require.NoError(t, err)
assert.Equal(t, int64(1), result.TotalCount)
assert.Len(t, result.Messages, 1)
}
func TestCopilot_UpdateSuggestionStatus_Cov7(t *testing.T) {
db, _, svc := setupCov7Copilot(t)
account := seedCov7Account(t, db)
inbox := seedCov7Inbox(t, db, account.ID)
contact := seedCov7Contact(t, db, account.ID)
conv := seedCov7Conversation(t, db, account.ID, inbox.ID, contact.ID)
msg, err := svc.CreateCopilotSuggestion(context.Background(), account.ID, &CreateSuggestionRequest{
ConversationID: conv.ID,
Content: "test",
})
require.NoError(t, err)
updated, err := svc.UpdateSuggestionStatus(context.Background(), msg.ID, model.CopilotSuggestionStatusAccepted)
require.NoError(t, err)
assert.Equal(t, model.CopilotSuggestionStatusAccepted, updated.Status)
}
func TestCopilot_UpdateSuggestionStatus_NotFound_Cov7(t *testing.T) {
_, _, svc := setupCov7Copilot(t)
_, err := svc.UpdateSuggestionStatus(context.Background(), 99999, model.CopilotSuggestionStatusAccepted)
require.Error(t, err)
assert.Contains(t, err.Error(), "suggestion not found")
}
func TestCopilot_SetResponseBackend_Cov7(t *testing.T) {
_, _, svc := setupCov7Copilot(t)
// Test SetResponseBackend doesn't panic
svc.SetResponseBackend(nil)
}
func TestCopilot_CreateThreadMessage_Cov7(t *testing.T) {
db, _, svc := setupCov7Copilot(t)
account := seedCov7Account(t, db)
assistantRepo := repository.NewCaptainAssistantRepo(db)
assistant := &model.CaptainAssistant{
AccountID: account.ID,
Name: "Test",
Description: "desc",
Status: model.AssistantStatusActive,
Config: json.RawMessage(`{}`),
}
require.NoError(t, assistantRepo.Create(context.Background(), assistant))
thread, err := svc.CreateThread(context.Background(), account.ID, 1, &CreateThreadRequest{
Message: "Initial",
AssistantID: assistant.ID,
})
require.NoError(t, err)
msg, err := svc.CreateThreadMessage(context.Background(), account.ID, 1, thread.ID, &SendMessageRequest{
Content: "Direct message",
})
require.NoError(t, err)
assert.NotZero(t, msg.ID)
assert.Equal(t, model.CopilotMessageTypeUser, msg.MessageType)
}
func TestCopilot_CreateThreadMessage_NoContent_Cov7(t *testing.T) {
_, _, svc := setupCov7Copilot(t)
_, err := svc.CreateThreadMessage(context.Background(), 1, 1, 1, &SendMessageRequest{})
require.Error(t, err)
assert.Contains(t, err.Error(), "Message is required")
}
func TestCopilot_CreateThreadMessage_ThreadNotFound_Cov7(t *testing.T) {
_, _, svc := setupCov7Copilot(t)
_, err := svc.CreateThreadMessage(context.Background(), 1, 1, 99999, &SendMessageRequest{
Content: "hello",
})
require.Error(t, err)
assert.Contains(t, err.Error(), "thread not found")
}
func TestSplitReplies_Cov7(t *testing.T) {
replies := splitReplies("1. First\n2. Second\n3. Third")
assert.Len(t, replies, 3)
replies2 := splitReplies("")
assert.Empty(t, replies2)
replies3 := splitReplies("single reply")
assert.Len(t, replies3, 1)
}
func TestSplitLines_Cov7(t *testing.T) {
lines := splitLines("a\nb\nc")
assert.Len(t, lines, 3)
lines2 := splitLines("")
assert.Empty(t, lines2)
}
func TestTrimReplyPrefix_Cov7(t *testing.T) {
// trimReplyPrefix breaks on first non-digit non-space char before finding ., ), -
assert.Equal(t, "1. First", trimReplyPrefix("1. First"))
assert.Equal(t, "1) hello", trimReplyPrefix("1) hello"))
assert.Equal(t, "text", trimReplyPrefix("-text"))
}
func TestJsonEscape_Cov7(t *testing.T) {
result := jsonEscape(`hello "world" \n`)
assert.NotEmpty(t, result)
assert.Contains(t, result, `\"`)
}
// ============================================================
// UploadService tests
// ============================================================
func TestUpload_AccountUploadFromURL_Cov7(t *testing.T) {
_, svc := setupCov7Upload(t)
// Create a test HTTP server
fileContent := []byte("fake file content from URL")
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "image/png")
w.Write(fileContent)
}))
defer server.Close()
result, err := svc.AccountUploadFromURL(context.Background(), 1, server.URL+"/test.png")
require.NoError(t, err)
assert.NotEmpty(t, result.UploadUUID)
assert.Equal(t, "test.png", result.OriginalName)
assert.Equal(t, "image", result.FileType)
assert.Equal(t, "image/png", result.MimeType)
}
func TestUpload_AccountUploadFromURL_InvalidURL_Cov7(t *testing.T) {
_, svc := setupCov7Upload(t)
_, err := svc.AccountUploadFromURL(context.Background(), 1, "not-a-url")
require.Error(t, err)
assert.Contains(t, err.Error(), "invalid url")
}
func TestUpload_AccountUploadFromURL_NoAccountID_Cov7(t *testing.T) {
_, svc := setupCov7Upload(t)
_, err := svc.AccountUploadFromURL(context.Background(), 0, "https://example.com/test.png")
require.Error(t, err)
assert.Contains(t, err.Error(), "account_id is required")
}
func TestUpload_AccountUploadFromURL_ServerError_Cov7(t *testing.T) {
_, svc := setupCov7Upload(t)
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
}))
defer server.Close()
_, err := svc.AccountUploadFromURL(context.Background(), 1, server.URL+"/test.png")
require.Error(t, err)
assert.Contains(t, err.Error(), "failed to fetch")
}
func TestUpload_AccountUploadFromURL_FileTooLarge_Cov7(t *testing.T) {
db, _ := setupCov7Upload(t)
tmpDir := t.TempDir()
cfg := &config.Config{
Storage: config.StorageConfig{
Provider: "local",
LocalPath: tmpDir,
MaxFileSize: 10, // 10 bytes
},
}
directUploadRepo := repository.NewDirectUploadRepo(db)
svc := NewUploadService(directUploadRepo, cfg)
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "image/png")
w.Write([]byte("this is more than 10 bytes"))
}))
defer server.Close()
_, err := svc.AccountUploadFromURL(context.Background(), 1, server.URL+"/test.png")
require.Error(t, err)
assert.Contains(t, err.Error(), "file too large")
}
func TestUpload_ProfileAvatarUpload_Cov7(t *testing.T) {
_, svc := setupCov7Upload(t)
fileHeader := createTestFileHeader(t, "avatar.png", []byte("fake png"))
result, err := svc.ProfileAvatarUpload(context.Background(), 1, fileHeader)
require.NoError(t, err)
assert.Equal(t, "avatar.png", result.OriginalName)
assert.Equal(t, "image", result.FileType)
assert.Equal(t, "image/png", result.MimeType)
}
func TestUpload_ProfileAvatarUpload_NoAccountID_Cov7(t *testing.T) {
_, svc := setupCov7Upload(t)
_, err := svc.ProfileAvatarUpload(context.Background(), 0, nil)
require.Error(t, err)
assert.Contains(t, err.Error(), "account_id is required")
}
func TestUpload_ProfileAvatarUpload_NoFile_Cov7(t *testing.T) {
_, svc := setupCov7Upload(t)
_, err := svc.ProfileAvatarUpload(context.Background(), 1, nil)
require.Error(t, err)
assert.Contains(t, err.Error(), "avatar file is required")
}
func TestUpload_ProfileAvatarUpload_NotImage_Cov7(t *testing.T) {
_, svc := setupCov7Upload(t)
fileHeader := createTestFileHeader(t, "doc.pdf", []byte("fake pdf"))
_, err := svc.ProfileAvatarUpload(context.Background(), 1, fileHeader)
require.Error(t, err)
assert.Contains(t, err.Error(), "avatar must be an image")
}
func TestUpload_CreateWidgetDirectUpload_Cov7(t *testing.T) {
_, svc := setupCov7Upload(t)
resp, err := svc.CreateWidgetDirectUpload(context.Background(), ActiveStorageDirectUploadRequest{
WebsiteToken: "wt_upload",
AuthToken: "auth_token",
Blob: ActiveStorageBlobParams{
Filename: "test.png",
ByteSize: 100,
ContentType: "image/png",
},
})
require.NoError(t, err)
assert.NotZero(t, resp.ID)
assert.Equal(t, "test.png", resp.Filename)
assert.Equal(t, "image/png", resp.ContentType)
assert.NotEmpty(t, resp.SignedID)
assert.NotEmpty(t, resp.DirectUpload.URL)
}
func TestUpload_CreateWidgetDirectUpload_NoFilename_Cov7(t *testing.T) {
_, svc := setupCov7Upload(t)
_, err := svc.CreateWidgetDirectUpload(context.Background(), ActiveStorageDirectUploadRequest{
WebsiteToken: "wt_upload",
AuthToken: "auth_token",
Blob: ActiveStorageBlobParams{
Filename: "",
ByteSize: 100,
},
})
require.Error(t, err)
}
func TestUpload_CreateWidgetDirectUpload_NoByteSize_Cov7(t *testing.T) {
_, svc := setupCov7Upload(t)
_, err := svc.CreateWidgetDirectUpload(context.Background(), ActiveStorageDirectUploadRequest{
WebsiteToken: "wt_upload",
AuthToken: "auth_token",
Blob: ActiveStorageBlobParams{
Filename: "test.png",
ByteSize: 0,
},
})
require.Error(t, err)
}
func TestUpload_CreateWidgetDirectUpload_UnsupportedType_Cov7(t *testing.T) {
_, svc := setupCov7Upload(t)
_, err := svc.CreateWidgetDirectUpload(context.Background(), ActiveStorageDirectUploadRequest{
WebsiteToken: "wt_upload",
AuthToken: "auth_token",
Blob: ActiveStorageBlobParams{
Filename: "test.exe",
ByteSize: 100,
ContentType: "application/x-msdownload",
},
})
require.Error(t, err)
}
func TestUpload_CreateWidgetDirectUpload_FileTooLarge_Cov7(t *testing.T) {
_, svc := setupCov7Upload(t)
_, err := svc.CreateWidgetDirectUpload(context.Background(), ActiveStorageDirectUploadRequest{
WebsiteToken: "wt_upload",
AuthToken: "auth_token",
Blob: ActiveStorageBlobParams{
Filename: "big.png",
ByteSize: 100 * 1024 * 1024,
ContentType: "image/png",
},
})
require.Error(t, err)
}
func TestUpload_CreateConversationDirectUpload_Cov7(t *testing.T) {
db, svc := setupCov7Upload(t)
account := seedCov7Account(t, db)
inbox := seedCov7Inbox(t, db, account.ID)
contact := seedCov7Contact(t, db, account.ID)
conv := seedCov7Conversation(t, db, account.ID, inbox.ID, contact.ID)
// Wire conversation repo
convRepo := repository.NewConversationRepo(db)
svc = svc.WithConversationRepo(convRepo)
resp, err := svc.CreateConversationDirectUpload(context.Background(), account.ID, conv.ID, ActiveStorageDirectUploadRequest{
Blob: ActiveStorageBlobParams{
Filename: "conv_upload.png",
ByteSize: 100,
ContentType: "image/png",
},
})
require.NoError(t, err)
assert.NotZero(t, resp.ID)
assert.Equal(t, "conv_upload.png", resp.Filename)
}
func TestUpload_CreateConversationDirectUpload_NoAccountID_Cov7(t *testing.T) {
_, svc := setupCov7Upload(t)
_, err := svc.CreateConversationDirectUpload(context.Background(), 0, 1, ActiveStorageDirectUploadRequest{
Blob: ActiveStorageBlobParams{Filename: "test.png", ByteSize: 100},
})
require.Error(t, err)
assert.Contains(t, err.Error(), "account_id is required")
}
func TestUpload_CreateConversationDirectUpload_NoConversationID_Cov7(t *testing.T) {
_, svc := setupCov7Upload(t)
_, err := svc.CreateConversationDirectUpload(context.Background(), 1, 0, ActiveStorageDirectUploadRequest{
Blob: ActiveStorageBlobParams{Filename: "test.png", ByteSize: 100},
})
require.Error(t, err)
assert.Contains(t, err.Error(), "conversation_id is required")
}
func TestUpload_CreateConversationDirectUpload_NoConvRepo_Cov7(t *testing.T) {
_, svc := setupCov7Upload(t)
_, err := svc.CreateConversationDirectUpload(context.Background(), 1, 1, ActiveStorageDirectUploadRequest{
Blob: ActiveStorageBlobParams{Filename: "test.png", ByteSize: 100},
})
require.Error(t, err)
assert.Contains(t, err.Error(), "conversation repository is not configured")
}
func TestUpload_CreateConversationDirectUpload_ConversationNotFound_Cov7(t *testing.T) {
db, svc := setupCov7Upload(t)
convRepo := repository.NewConversationRepo(db)
svc = svc.WithConversationRepo(convRepo)
_, err := svc.CreateConversationDirectUpload(context.Background(), 1, 99999, ActiveStorageDirectUploadRequest{
Blob: ActiveStorageBlobParams{Filename: "test.png", ByteSize: 100},
})
require.Error(t, err)
assert.Contains(t, err.Error(), "conversation not found")
}
func TestUpload_CompleteWidgetDirectUpload_Cov7(t *testing.T) {
db, svc := setupCov7Upload(t)
// Create a pending widget upload
upload := &model.DirectUpload{
UploadUUID: "widget-complete-uuid",
AccountID: 0,
Status: model.DirectUploadStatusPending,
Source: model.DirectUploadSourceWidget,
OriginalName: "test.png",
FileType: "image",
MimeType: "image/png",
FileSize: 100,
FileURL: "/uploads/widget_direct/test.png",
ExpiresAt: time.Now().Add(24 * time.Hour),
}
require.NoError(t, db.Create(upload).Error)
result, err := svc.CompleteWidgetDirectUpload(context.Background(), upload.UploadUUID, strings.NewReader("file content"))
require.NoError(t, err)
assert.Equal(t, upload.UploadUUID, result.UploadUUID)
assert.Equal(t, "test.png", result.OriginalName)
}
func TestUpload_CompleteWidgetDirectUpload_NoUUID_Cov7(t *testing.T) {
_, svc := setupCov7Upload(t)
_, err := svc.CompleteWidgetDirectUpload(context.Background(), "", strings.NewReader("content"))
require.Error(t, err)
assert.Contains(t, err.Error(), "upload_uuid is required")
}
func TestUpload_CompleteWidgetDirectUpload_NotFound_Cov7(t *testing.T) {
_, svc := setupCov7Upload(t)
_, err := svc.CompleteWidgetDirectUpload(context.Background(), "nonexistent-uuid", strings.NewReader("content"))
require.Error(t, err)
assert.Contains(t, err.Error(), "not found")
}
func TestUpload_CompleteWidgetDirectUpload_Expired_Cov7(t *testing.T) {
db, svc := setupCov7Upload(t)
upload := &model.DirectUpload{
UploadUUID: "expired-widget-uuid",
AccountID: 0,
Status: model.DirectUploadStatusPending,
Source: model.DirectUploadSourceWidget,
OriginalName: "test.png",
FileType: "image",
MimeType: "image/png",
FileSize: 100,
FileURL: "/uploads/widget_direct/expired.png",
ExpiresAt: time.Now().Add(-1 * time.Hour), // expired
}
require.NoError(t, db.Create(upload).Error)
_, err := svc.CompleteWidgetDirectUpload(context.Background(), upload.UploadUUID, strings.NewReader("content"))
require.Error(t, err)
assert.Contains(t, err.Error(), "expired")
}
func TestUpload_CompleteWidgetDirectUpload_SourceMismatch_Cov7(t *testing.T) {
db, svc := setupCov7Upload(t)
upload := &model.DirectUpload{
UploadUUID: "source-mismatch-uuid",
AccountID: 1,
Status: model.DirectUploadStatusPending,
Source: model.DirectUploadSourceAccount, // not widget
OriginalName: "test.png",
FileType: "image",
MimeType: "image/png",
FileSize: 100,
FileURL: "/uploads/account/test.png",
ExpiresAt: time.Now().Add(24 * time.Hour),
}
require.NoError(t, db.Create(upload).Error)
_, err := svc.CompleteWidgetDirectUpload(context.Background(), upload.UploadUUID, strings.NewReader("content"))
require.Error(t, err)
assert.Contains(t, err.Error(), "source mismatch")
}
func TestUpload_CompleteConversationDirectUpload_Cov7(t *testing.T) {
db, svc := setupCov7Upload(t)
account := seedCov7Account(t, db)
inbox := seedCov7Inbox(t, db, account.ID)
contact := seedCov7Contact(t, db, account.ID)
conv := seedCov7Conversation(t, db, account.ID, inbox.ID, contact.ID)
convRepo := repository.NewConversationRepo(db)
svc = svc.WithConversationRepo(convRepo)
upload := &model.DirectUpload{
UploadUUID: "conv-complete-uuid",
AccountID: account.ID,
Status: model.DirectUploadStatusPending,
Source: model.DirectUploadSourceAccount,
OriginalName: "test.png",
FileType: "image",
MimeType: "image/png",
FileSize: 100,
FileURL: "/uploads/account/test.png",
ExpiresAt: time.Now().Add(24 * time.Hour),
}
require.NoError(t, db.Create(upload).Error)
result, err := svc.CompleteConversationDirectUpload(context.Background(), account.ID, conv.ID, upload.UploadUUID, strings.NewReader("content"))
require.NoError(t, err)
assert.Equal(t, upload.UploadUUID, result.UploadUUID)
}
func TestUpload_CompleteConversationDirectUpload_NoAccountID_Cov7(t *testing.T) {
_, svc := setupCov7Upload(t)
_, err := svc.CompleteConversationDirectUpload(context.Background(), 0, 1, "uuid", strings.NewReader("content"))
require.Error(t, err)
assert.Contains(t, err.Error(), "account_id is required")
}
func TestUpload_CompleteConversationDirectUpload_NoConvRepo_Cov7(t *testing.T) {
_, svc := setupCov7Upload(t)
_, err := svc.CompleteConversationDirectUpload(context.Background(), 1, 1, "uuid", strings.NewReader("content"))
require.Error(t, err)
assert.Contains(t, err.Error(), "conversation repository is not configured")
}
func TestUpload_CompleteConversationDirectUpload_Expired_Cov7(t *testing.T) {
db, svc := setupCov7Upload(t)
account := seedCov7Account(t, db)
inbox := seedCov7Inbox(t, db, account.ID)
contact := seedCov7Contact(t, db, account.ID)
conv := seedCov7Conversation(t, db, account.ID, inbox.ID, contact.ID)
convRepo := repository.NewConversationRepo(db)
svc = svc.WithConversationRepo(convRepo)
upload := &model.DirectUpload{
UploadUUID: "conv-expired-uuid",
AccountID: account.ID,
Status: model.DirectUploadStatusPending,
Source: model.DirectUploadSourceAccount,
OriginalName: "test.png",
FileType: "image",
MimeType: "image/png",
FileSize: 100,
FileURL: "/uploads/account/test.png",
ExpiresAt: time.Now().Add(-1 * time.Hour),
}
require.NoError(t, db.Create(upload).Error)
_, err := svc.CompleteConversationDirectUpload(context.Background(), account.ID, conv.ID, upload.UploadUUID, strings.NewReader("content"))
require.Error(t, err)
assert.Contains(t, err.Error(), "expired")
}
func TestUpload_CompleteConversationDirectUpload_SourceMismatch_Cov7(t *testing.T) {
db, svc := setupCov7Upload(t)
account := seedCov7Account(t, db)
inbox := seedCov7Inbox(t, db, account.ID)
contact := seedCov7Contact(t, db, account.ID)
conv := seedCov7Conversation(t, db, account.ID, inbox.ID, contact.ID)
convRepo := repository.NewConversationRepo(db)
svc = svc.WithConversationRepo(convRepo)
upload := &model.DirectUpload{
UploadUUID: "conv-source-mismatch",
AccountID: 999, // different account
Status: model.DirectUploadStatusPending,
Source: model.DirectUploadSourceAccount,
OriginalName: "test.png",
FileType: "image",
MimeType: "image/png",
FileSize: 100,
FileURL: "/uploads/account/test.png",
ExpiresAt: time.Now().Add(24 * time.Hour),
}
require.NoError(t, db.Create(upload).Error)
_, err := svc.CompleteConversationDirectUpload(context.Background(), account.ID, conv.ID, upload.UploadUUID, strings.NewReader("content"))
require.Error(t, err)
assert.Contains(t, err.Error(), "source mismatch")
}
func TestUpload_ValidateWidgetUploadSession_Cov7(t *testing.T) {
_, svc := setupCov7Upload(t)
// No inboxRepo/contactInboxRepo wired — should return (0, nil)
accountID, err := svc.validateWidgetUploadSession(context.Background(), "token", "auth")
require.NoError(t, err)
assert.Equal(t, uint(0), accountID)
}
func TestUpload_ValidateWidgetUploadSession_NoWebsiteToken_Cov7(t *testing.T) {
_, svc := setupCov7Upload(t)
_, err := svc.validateWidgetUploadSession(context.Background(), "", "auth")
require.Error(t, err)
assert.Contains(t, err.Error(), "website_token is required")
}
func TestUpload_ValidateWidgetUploadSession_NoAuthToken_Cov7(t *testing.T) {
_, svc := setupCov7Upload(t)
_, err := svc.validateWidgetUploadSession(context.Background(), "token", "")
require.Error(t, err)
assert.Contains(t, err.Error(), "widget auth token is required")
}
func TestUpload_WithWidgetAuth_Cov7(t *testing.T) {
db, svc := setupCov7Upload(t)
inboxRepo := repository.NewInboxRepo(db)
contactInboxRepo := repository.NewContactInboxRepo(db)
result := svc.WithWidgetAuth(inboxRepo, contactInboxRepo)
assert.NotNil(t, result)
assert.Equal(t, svc, result)
}
func TestUpload_WithConversationRepo_Cov7(t *testing.T) {
db, svc := setupCov7Upload(t)
convRepo := repository.NewConversationRepo(db)
result := svc.WithConversationRepo(convRepo)
assert.NotNil(t, result)
assert.Equal(t, svc, result)
}
func TestUploadSubDir_Cov7(t *testing.T) {
assert.Equal(t, "widget_direct", uploadSubDir(model.DirectUploadSourceWidget))
assert.Equal(t, "account", uploadSubDir(model.DirectUploadSourceAccount))
}
func TestRandomStorageKey_Cov7(t *testing.T) {
key1 := randomStorageKey()
key2 := randomStorageKey()
assert.NotEmpty(t, key1)
assert.NotEmpty(t, key2)
assert.NotEqual(t, key1, key2) // extremely unlikely to collide
}
// ============================================================
// AuthService tests
// ============================================================
func TestAuth_Login_Cov7(t *testing.T) {
db, svc := setupCov7Auth(t)
account := seedCov7Account(t, db)
user := seedCov7User(t, db, account.ID, "login@test.com")
output, err := svc.Login(context.Background(), &LoginInput{
Email: user.Email,
Password: "password123",
})
require.NoError(t, err)
assert.NotNil(t, output.User)
assert.NotNil(t, output.TokenPair)
assert.NotEmpty(t, output.TokenPair.AccessToken)
assert.NotEmpty(t, output.TokenPair.RefreshToken)
assert.Equal(t, account.ID, output.AccountID)
assert.Equal(t, "administrator", output.Role)
}
func TestAuth_Login_InvalidEmail_Cov7(t *testing.T) {
db, svc := setupCov7Auth(t)
account := seedCov7Account(t, db)
_ = account
_, err := svc.Login(context.Background(), &LoginInput{
Email: "nonexistent@test.com",
Password: "password",
})
require.Error(t, err)
assert.Contains(t, err.Error(), "invalid email or password")
}
func TestAuth_Login_InactiveUser_Cov7(t *testing.T) {
db, svc := setupCov7Auth(t)
account := seedCov7Account(t, db)
user := seedCov7User(t, db, account.ID, "inactive@test.com")
// Set user inactive
require.NoError(t, db.Model(&model.User{}).Where("id = ?", user.ID).Update("active", false).Error)
_, err := svc.Login(context.Background(), &LoginInput{
Email: user.Email,
Password: "password123",
})
require.Error(t, err)
assert.Contains(t, err.Error(), "inactive")
}
func TestAuth_Login_WrongPassword_Cov7(t *testing.T) {
db, svc := setupCov7Auth(t)
account := seedCov7Account(t, db)
user := seedCov7User(t, db, account.ID, "wrongpw@test.com")
_, err := svc.Login(context.Background(), &LoginInput{
Email: user.Email,
Password: "wrongpassword",
})
require.Error(t, err)
assert.Contains(t, err.Error(), "invalid email or password")
}
func TestAuth_Login_UnconfirmedEmail_Cov7(t *testing.T) {
db, svc := setupCov7Auth(t)
account := seedCov7Account(t, db)
hash, err := hashPasswordCov7("password123")
require.NoError(t, err)
user := &model.User{
AccountID: account.ID,
Name: "Unconfirmed",
Email: "unconfirmed@test.com",
Password: hash,
PasswordDigest: hash,
Provider: "email",
Active: true,
ConfirmedAt: nil, // unconfirmed
}
require.NoError(t, db.Create(user).Error)
au := AccountUser{UserID: user.ID, AccountID: account.ID, Role: "agent"}
require.NoError(t, db.Create(&au).Error)
_, err = svc.Login(context.Background(), &LoginInput{
Email: user.Email,
Password: "password123",
})
require.Error(t, err)
assert.Contains(t, err.Error(), "email not confirmed")
}
func TestAuth_Login_OAuthProvider_Cov7(t *testing.T) {
db, svc := setupCov7Auth(t)
account := seedCov7Account(t, db)
hash, _ := hashPasswordCov7("password123")
user := &model.User{
AccountID: account.ID,
Name: "OAuth User",
Email: "oauth@test.com",
Password: hash,
PasswordDigest: hash,
Provider: "google",
Active: true,
ConfirmedAt: &time.Time{},
}
require.NoError(t, db.Create(user).Error)
_, err := svc.Login(context.Background(), &LoginInput{
Email: user.Email,
Password: "password123",
})
require.Error(t, err)
assert.Contains(t, err.Error(), "google")
}
func TestAuth_Login_NoAccount_Cov7(t *testing.T) {
db, svc := setupCov7Auth(t)
account := seedCov7Account(t, db)
hash, _ := hashPasswordCov7("password123")
now := time.Now()
user := &model.User{
AccountID: account.ID,
Name: "No Account",
Email: "noacct@test.com",
Password: hash,
PasswordDigest: hash,
Provider: "email",
Active: true,
ConfirmedAt: &now,
}
require.NoError(t, db.Create(user).Error)
// Don't create AccountUser join
_, err := svc.Login(context.Background(), &LoginInput{
Email: user.Email,
Password: "password123",
})
require.Error(t, err)
assert.Contains(t, err.Error(), "failed to get user account")
}
func TestAuth_ValidateAccessToken_Cov7(t *testing.T) {
db, svc := setupCov7Auth(t)
account := seedCov7Account(t, db)
user := seedCov7User(t, db, account.ID, "validate@test.com")
output, err := svc.Login(context.Background(), &LoginInput{
Email: user.Email,
Password: "password123",
})
require.NoError(t, err)
result, err := svc.ValidateAccessToken(context.Background(), output.TokenPair.AccessToken)
require.NoError(t, err)
assert.Equal(t, user.ID, result.User.ID)
assert.Equal(t, account.ID, result.AccountID)
}
func TestAuth_ValidateAccessToken_Invalid_Cov7(t *testing.T) {
_, svc := setupCov7Auth(t)
_, err := svc.ValidateAccessToken(context.Background(), "invalid-token")
require.Error(t, err)
}
func TestAuth_Refresh_Cov7(t *testing.T) {
db, svc := setupCov7Auth(t)
account := seedCov7Account(t, db)
user := seedCov7User(t, db, account.ID, "refresh@test.com")
output, err := svc.Login(context.Background(), &LoginInput{
Email: user.Email,
Password: "password123",
})
require.NoError(t, err)
result, err := svc.Refresh(context.Background(), &RefreshInput{
RefreshToken: output.TokenPair.RefreshToken,
})
require.NoError(t, err)
assert.NotNil(t, result.TokenPair)
assert.NotEmpty(t, result.TokenPair.AccessToken)
assert.NotEmpty(t, result.TokenPair.RefreshToken)
assert.NotNil(t, result.User)
}
func TestAuth_Refresh_InvalidToken_Cov7(t *testing.T) {
_, svc := setupCov7Auth(t)
_, err := svc.Refresh(context.Background(), &RefreshInput{
RefreshToken: "invalid-refresh-token",
})
require.Error(t, err)
assert.Contains(t, err.Error(), "invalid refresh token")
}
func TestAuth_Refresh_RevokedToken_Cov7(t *testing.T) {
db, svc := setupCov7Auth(t)
account := seedCov7Account(t, db)
user := seedCov7User(t, db, account.ID, "revoked@test.com")
output, err := svc.Login(context.Background(), &LoginInput{
Email: user.Email,
Password: "password123",
})
require.NoError(t, err)
// Revoke the token
require.NoError(t, svc.Logout(context.Background(), user.ID))
_, err = svc.Refresh(context.Background(), &RefreshInput{
RefreshToken: output.TokenPair.RefreshToken,
})
require.Error(t, err)
assert.Contains(t, err.Error(), "expired or revoked")
}
func TestAuth_Logout_Cov7(t *testing.T) {
db, svc := setupCov7Auth(t)
account := seedCov7Account(t, db)
user := seedCov7User(t, db, account.ID, "logout@test.com")
_, err := svc.Login(context.Background(), &LoginInput{
Email: user.Email,
Password: "password123",
})
require.NoError(t, err)
err = svc.Logout(context.Background(), user.ID)
require.NoError(t, err)
}
func TestAuth_SwitchAccount_Cov7(t *testing.T) {
db, svc := setupCov7Auth(t)
account1 := seedCov7Account(t, db)
account2 := &model.Account{Name: "Account 2", Locale: "en", Status: "active"}
require.NoError(t, db.Create(account2).Error)
user := seedCov7User(t, db, account1.ID, "switch@test.com")
// Add user to account2
au2 := AccountUser{UserID: user.ID, AccountID: account2.ID, Role: "agent"}
require.NoError(t, db.Create(&au2).Error)
output, err := svc.SwitchAccount(context.Background(), &SwitchAccountInput{
UserID: user.ID,
AccountID: account2.ID,
})
require.NoError(t, err)
assert.NotNil(t, output.TokenPair)
assert.Equal(t, account2.ID, output.AccountID)
assert.Equal(t, "agent", output.Role)
}
func TestAuth_SwitchAccount_NotMember_Cov7(t *testing.T) {
db, svc := setupCov7Auth(t)
account1 := seedCov7Account(t, db)
account2 := &model.Account{Name: "Account 2", Locale: "en", Status: "active"}
require.NoError(t, db.Create(account2).Error)
user := seedCov7User(t, db, account1.ID, "switch-nm@test.com")
_, err := svc.SwitchAccount(context.Background(), &SwitchAccountInput{
UserID: user.ID,
AccountID: account2.ID, // user is not a member
})
require.Error(t, err)
assert.Contains(t, err.Error(), "does not belong")
}
func TestAuth_SwitchAccount_UserNotFound_Cov7(t *testing.T) {
db, svc := setupCov7Auth(t)
account := seedCov7Account(t, db)
_ = account
// Create a fake AccountUser
au := AccountUser{UserID: 99999, AccountID: account.ID, Role: "agent"}
require.NoError(t, db.Create(&au).Error)
_, err := svc.SwitchAccount(context.Background(), &SwitchAccountInput{
UserID: 99999,
AccountID: account.ID,
})
require.Error(t, err)
assert.Contains(t, err.Error(), "user not found")
}
func TestAuth_ResetPassword_Cov7(t *testing.T) {
db, svc := setupCov7Auth(t)
account := seedCov7Account(t, db)
user := seedCov7User(t, db, account.ID, "reset@test.com")
err := svc.ResetPassword(context.Background(), &ResetPasswordInput{
Email: user.Email,
})
require.NoError(t, err)
// Verify reset_password_token was set
var updated model.User
require.NoError(t, db.First(&updated, user.ID).Error)
assert.NotEmpty(t, updated.ResetPasswordToken)
}
func TestAuth_ResetPassword_EmailNotFound_Cov7(t *testing.T) {
_, svc := setupCov7Auth(t)
// Should not reveal whether email exists
err := svc.ResetPassword(context.Background(), &ResetPasswordInput{
Email: "nonexistent@test.com",
})
require.NoError(t, err) // returns nil for security
}
func TestAuth_ConfirmResetPassword_Cov7(t *testing.T) {
db, svc := setupCov7Auth(t)
account := seedCov7Account(t, db)
user := seedCov7User(t, db, account.ID, "confirm-reset@test.com")
// Initiate reset
require.NoError(t, svc.ResetPassword(context.Background(), &ResetPasswordInput{
Email: user.Email,
}))
// Get the reset token (it's stored as a digest)
var updated model.User
require.NoError(t, db.First(&updated, user.ID).Error)
// We need to use the raw token, not the digest. Since ResetPassword generates
// a token and stores its digest, we need to find the raw token.
// In test, we can generate the token by calling generateAuthToken and
// digestAuthToken, but those are internal. Instead, let's test the flow
// by setting a known token.
rawToken := "test-reset-token-1234567890"
digest := digestAuthToken(rawToken)
require.NoError(t, db.Model(&model.User{}).Where("id = ?", user.ID).Updates(map[string]interface{}{
"reset_password_token": digest,
"reset_password_sent_at": time.Now(),
}).Error)
output, err := svc.ConfirmResetPassword(context.Background(), &ConfirmResetPasswordInput{
Token: rawToken,
Password: "newpassword123",
PasswordConfirmation: "newpassword123",
})
require.NoError(t, err)
assert.NotNil(t, output)
assert.NotNil(t, output.TokenPair)
}
func TestAuth_ConfirmResetPassword_InvalidToken_Cov7(t *testing.T) {
_, svc := setupCov7Auth(t)
_, err := svc.ConfirmResetPassword(context.Background(), &ConfirmResetPasswordInput{
Token: "",
Password: "newpassword",
PasswordConfirmation: "newpassword",
})
require.Error(t, err)
assert.Contains(t, err.Error(), "Invalid token")
}
func TestAuth_ConfirmResetPassword_PasswordMismatch_Cov7(t *testing.T) {
_, svc := setupCov7Auth(t)
_, err := svc.ConfirmResetPassword(context.Background(), &ConfirmResetPasswordInput{
Token: "some-token",
Password: "password1",
PasswordConfirmation: "password2",
})
require.Error(t, err)
assert.Contains(t, err.Error(), "invalid password confirmation")
}
func TestAuth_ConfirmResetPassword_TokenNotFound_Cov7(t *testing.T) {
_, svc := setupCov7Auth(t)
_, err := svc.ConfirmResetPassword(context.Background(), &ConfirmResetPasswordInput{
Token: "nonexistent-token",
Password: "newpassword",
PasswordConfirmation: "newpassword",
})
require.Error(t, err)
assert.Contains(t, err.Error(), "Invalid token")
}
func TestAuth_ConfirmEmail_Cov7(t *testing.T) {
db, svc := setupCov7Auth(t)
account := seedCov7Account(t, db)
user := seedCov7User(t, db, account.ID, "confirm-email@test.com")
// Set a confirmation token
token := "confirm-token-12345"
require.NoError(t, db.Model(&model.User{}).Where("id = ?", user.ID).Update("confirmation_token", token).Error)
// Clear confirmed_at
require.NoError(t, db.Model(&model.User{}).Where("id = ?", user.ID).Update("confirmed_at", nil).Error)
output, err := svc.ConfirmEmail(context.Background(), &ConfirmEmailInput{
Token: token,
})
require.NoError(t, err)
assert.NotNil(t, output)
assert.NotNil(t, output.User)
assert.NotNil(t, output.TokenPair)
}
func TestAuth_ConfirmEmail_EmptyToken_Cov7(t *testing.T) {
_, svc := setupCov7Auth(t)
_, err := svc.ConfirmEmail(context.Background(), &ConfirmEmailInput{
Token: "",
})
require.Error(t, err)
assert.Contains(t, err.Error(), "Invalid token")
}
func TestAuth_ConfirmEmail_TokenNotFound_Cov7(t *testing.T) {
_, svc := setupCov7Auth(t)
_, err := svc.ConfirmEmail(context.Background(), &ConfirmEmailInput{
Token: "nonexistent-token",
})
require.Error(t, err)
assert.Contains(t, err.Error(), "Invalid token")
}
func TestAuth_ConfirmEmail_AlreadyConfirmed_Cov7(t *testing.T) {
db, svc := setupCov7Auth(t)
account := seedCov7Account(t, db)
user := seedCov7User(t, db, account.ID, "already-confirmed@test.com")
token := "already-confirm-token"
require.NoError(t, db.Model(&model.User{}).Where("id = ?", user.ID).Update("confirmation_token", token).Error)
// confirmed_at is already set by seedCov7User
_, err := svc.ConfirmEmail(context.Background(), &ConfirmEmailInput{
Token: token,
})
require.Error(t, err)
assert.Contains(t, err.Error(), "Already confirmed")
}
func TestAuth_TrackChatwootSession_Cov7(t *testing.T) {
db, svc := setupCov7Auth(t)
account := seedCov7Account(t, db)
user := seedCov7User(t, db, account.ID, "session@test.com")
output, err := svc.Login(context.Background(), &LoginInput{
Email: user.Email,
Password: "password123",
})
require.NoError(t, err)
err = svc.TrackChatwootSession(context.Background(), output, "client-123", "127.0.0.1", "Mozilla/5.0")
require.NoError(t, err)
assert.NotEmpty(t, output.ClientID)
assert.NotEmpty(t, output.TokenPair.AccessToken)
}
func TestAuth_TrackChatwootSession_NilOutput_Cov7(t *testing.T) {
_, svc := setupCov7Auth(t)
err := svc.TrackChatwootSession(context.Background(), nil, "client", "ip", "ua")
require.Error(t, err)
assert.Contains(t, err.Error(), "login output is required")
}
func TestAuth_TrackChatwootSession_AutoClientID_Cov7(t *testing.T) {
db, svc := setupCov7Auth(t)
account := seedCov7Account(t, db)
user := seedCov7User(t, db, account.ID, "auto-client@test.com")
output, err := svc.Login(context.Background(), &LoginInput{
Email: user.Email,
Password: "password123",
})
require.NoError(t, err)
err = svc.TrackChatwootSession(context.Background(), output, "", "127.0.0.1", "Mozilla/5.0")
require.NoError(t, err)
assert.NotEmpty(t, output.ClientID)
}
func TestAuth_RevokeChatwootSession_Cov7(t *testing.T) {
db, svc := setupCov7Auth(t)
account := seedCov7Account(t, db)
user := seedCov7User(t, db, account.ID, "revoke-session@test.com")
output, err := svc.Login(context.Background(), &LoginInput{
Email: user.Email,
Password: "password123",
})
require.NoError(t, err)
err = svc.TrackChatwootSession(context.Background(), output, "client-revoke", "127.0.0.1", "Mozilla/5.0")
require.NoError(t, err)
err = svc.RevokeChatwootSession(context.Background(), user.ID, "client-revoke")
require.NoError(t, err)
}
func TestChatwootSessionUserAgent_Cov7(t *testing.T) {
browser, version, device, platform, _ := chatwootSessionUserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/120.0.0.0")
assert.Equal(t, "Chrome", browser)
assert.NotEmpty(t, version)
assert.Equal(t, "Desktop", device)
assert.Equal(t, "Windows", platform)
browser2, _, device2, platform2, _ := chatwootSessionUserAgent("Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) Version/16.0")
_ = browser2
_ = device2
_ = platform2
browser3, _, device3, platform3, _ := chatwootSessionUserAgent("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15) Firefox/120.0")
_ = browser3
_ = device3
_ = platform3
browser4, _, device4, platform4, _ := chatwootSessionUserAgent("Mozilla/5.0 (Linux; Android 13) Edg/120.0.0.0")
_ = browser4
_ = device4
_ = platform4
browser5, _, _, platform5, _ := chatwootSessionUserAgent("Mozilla/5.0 (X11; Linux x86_64)")
assert.Equal(t, "Unknown", browser5)
assert.Equal(t, "Linux", platform5)
}
func TestGenerateAuthToken_Cov7(t *testing.T) {
token, err := generateAuthToken()
require.NoError(t, err)
assert.NotEmpty(t, token)
assert.Len(t, token, 64) // 32 bytes hex = 64 chars
}
func TestDigestAuthToken_Cov7(t *testing.T) {
digest := digestAuthToken("test-token")
assert.NotEmpty(t, digest)
assert.Len(t, digest, 64) // SHA256 hex = 64 chars
// Same input should produce same digest
digest2 := digestAuthToken("test-token")
assert.Equal(t, digest, digest2)
}
// ============================================================
// AssignmentPolicyService tests
// ============================================================
func setupCov7AssignmentPolicy(t *testing.T) (*gorm.DB, *AssignmentPolicyService) {
t.Helper()
db := newCov7TestDB(t)
policyRepo := repository.NewAssignmentPolicyRepo(db)
inboxPolicyRepo := repository.NewInboxAssignmentPolicyRepo(db)
assignSvc := autoassignment.NewAssignmentService(db, nil)
svc := NewAssignmentPolicyService(policyRepo, inboxPolicyRepo, assignSvc)
return db, svc
}
func TestAssignmentPolicy_CreateAccountPolicy_Cov7(t *testing.T) {
db, svc := setupCov7AssignmentPolicy(t)
account := seedCov7Account(t, db)
policy, err := svc.CreateAccountPolicy(context.Background(), account.ID, CreatePolicyRequest{
Name: "Test Policy",
Description: "A test policy",
})
require.NoError(t, err)
assert.NotZero(t, policy.ID)
assert.Equal(t, "Test Policy", policy.Name)
assert.Equal(t, account.ID, policy.AccountID)
assert.True(t, policy.Enabled)
assert.Equal(t, 100, policy.FairDistributionLimit)
assert.Equal(t, 3600, policy.FairDistributionWindow)
}
func TestAssignmentPolicy_CreateAccountPolicy_Balanced_Cov7(t *testing.T) {
db, svc := setupCov7AssignmentPolicy(t)
account := seedCov7Account(t, db)
policy, err := svc.CreateAccountPolicy(context.Background(), account.ID, CreatePolicyRequest{
Name: "Balanced Policy",
AssignmentOrder: "balanced",
ConversationPriority: "longest_waiting",
FairDistributionLimit: 50,
FairDistributionWindow: 7200,
})
require.NoError(t, err)
assert.Equal(t, 1, policy.AssignmentOrder) // balanced
assert.Equal(t, 1, policy.ConversationPriority) // longest_waiting
assert.Equal(t, 50, policy.FairDistributionLimit)
assert.Equal(t, 7200, policy.FairDistributionWindow)
}
func TestAssignmentPolicy_CreateAccountPolicy_Disabled_Cov7(t *testing.T) {
t.Skip("GORM default:true overrides Enabled:false on Create")
db, svc := setupCov7AssignmentPolicy(t)
account := seedCov7Account(t, db)
enabled := false
policy, err := svc.CreateAccountPolicy(context.Background(), account.ID, CreatePolicyRequest{
Name: "Disabled Policy",
Enabled: &enabled,
})
require.NoError(t, err)
assert.False(t, policy.Enabled)
}
func TestAssignmentPolicy_CreateAccountPolicy_ValidationFail_Cov7(t *testing.T) {
db, svc := setupCov7AssignmentPolicy(t)
account := seedCov7Account(t, db)
_, err := svc.CreateAccountPolicy(context.Background(), account.ID, CreatePolicyRequest{
Name: "", // required
})
require.Error(t, err)
}
func TestAssignmentPolicy_GetAccountPolicy_Cov7(t *testing.T) {
db, svc := setupCov7AssignmentPolicy(t)
account := seedCov7Account(t, db)
created, err := svc.CreateAccountPolicy(context.Background(), account.ID, CreatePolicyRequest{
Name: "Get Test",
})
require.NoError(t, err)
got, err := svc.GetAccountPolicy(context.Background(), account.ID, created.ID)
require.NoError(t, err)
assert.Equal(t, created.ID, got.ID)
assert.Equal(t, "Get Test", got.Name)
}
func TestAssignmentPolicy_GetAccountPolicy_Default_Cov7(t *testing.T) {
db, svc := setupCov7AssignmentPolicy(t)
account := seedCov7Account(t, db)
created, err := svc.CreateAccountPolicy(context.Background(), account.ID, CreatePolicyRequest{
Name: "Default Test",
})
require.NoError(t, err)
// Get without specifying policyID — should return first policy
got, err := svc.GetAccountPolicy(context.Background(), account.ID)
require.NoError(t, err)
assert.Equal(t, created.ID, got.ID)
}
func TestAssignmentPolicy_GetAccountPolicy_NotFound_Cov7(t *testing.T) {
db, svc := setupCov7AssignmentPolicy(t)
account := seedCov7Account(t, db)
_, err := svc.GetAccountPolicy(context.Background(), account.ID, 99999)
require.Error(t, err)
assert.Contains(t, err.Error(), "not found")
}
func TestAssignmentPolicy_GetAccountPolicy_NoPolicyExists_Cov7(t *testing.T) {
db, svc := setupCov7AssignmentPolicy(t)
account := seedCov7Account(t, db)
_, err := svc.GetAccountPolicy(context.Background(), account.ID)
require.Error(t, err)
assert.Contains(t, err.Error(), "not found")
}
func TestAssignmentPolicy_ListAccountPolicies_Cov7(t *testing.T) {
db, svc := setupCov7AssignmentPolicy(t)
account := seedCov7Account(t, db)
_, err := svc.CreateAccountPolicy(context.Background(), account.ID, CreatePolicyRequest{
Name: "Policy 1",
})
require.NoError(t, err)
policies, err := svc.ListAccountPolicies(context.Background(), account.ID)
require.NoError(t, err)
assert.Len(t, policies, 1)
}
func TestAssignmentPolicy_ListAccountPolicies_Empty_Cov7(t *testing.T) {
db, svc := setupCov7AssignmentPolicy(t)
account := seedCov7Account(t, db)
policies, err := svc.ListAccountPolicies(context.Background(), account.ID)
require.NoError(t, err)
assert.Empty(t, policies)
}
func TestAssignmentPolicy_UpdateAccountPolicy_Cov7(t *testing.T) {
db, svc := setupCov7AssignmentPolicy(t)
account := seedCov7Account(t, db)
created, err := svc.CreateAccountPolicy(context.Background(), account.ID, CreatePolicyRequest{
Name: "Original",
})
require.NoError(t, err)
newName := "Updated"
balanced := "balanced"
updated, err := svc.UpdateAccountPolicy(context.Background(), created.ID, account.ID, UpdatePolicyRequest{
Name: newName,
AssignmentOrder: &balanced,
})
require.NoError(t, err)
assert.Equal(t, "Updated", updated.Name)
assert.Equal(t, 1, updated.AssignmentOrder) // balanced
}
func TestAssignmentPolicy_UpdateAccountPolicy_NotFound_Cov7(t *testing.T) {
db, svc := setupCov7AssignmentPolicy(t)
account := seedCov7Account(t, db)
_, err := svc.UpdateAccountPolicy(context.Background(), 99999, account.ID, UpdatePolicyRequest{})
require.Error(t, err)
assert.Contains(t, err.Error(), "not found")
}
func TestAssignmentPolicy_UpdateAccountPolicy_FullUpdate_Cov7(t *testing.T) {
db, svc := setupCov7AssignmentPolicy(t)
account := seedCov7Account(t, db)
created, err := svc.CreateAccountPolicy(context.Background(), account.ID, CreatePolicyRequest{
Name: "Original",
})
require.NoError(t, err)
longestWaiting := "longest_waiting"
balanced := "balanced"
limit := 200
window := 1800
excludeHours := 48
enabled := false
updated, err := svc.UpdateAccountPolicy(context.Background(), created.ID, account.ID, UpdatePolicyRequest{
Name: "Fully Updated",
Description: "New desc",
AssignmentOrder: &balanced,
ConversationPriority: &longestWaiting,
FairDistributionLimit: &limit,
FairDistributionWindow: &window,
ExcludeOlderThanHours: &excludeHours,
Enabled: &enabled,
})
require.NoError(t, err)
assert.Equal(t, "Fully Updated", updated.Name)
assert.Equal(t, "New desc", updated.Description)
assert.Equal(t, 1, updated.AssignmentOrder)
assert.Equal(t, 1, updated.ConversationPriority)
assert.Equal(t, 200, updated.FairDistributionLimit)
assert.Equal(t, 1800, updated.FairDistributionWindow)
assert.Equal(t, 48, *updated.ExcludeOlderThanHours)
assert.False(t, updated.Enabled)
}
func TestAssignmentPolicy_DeleteAccountPolicy_Cov7(t *testing.T) {
db, svc := setupCov7AssignmentPolicy(t)
account := seedCov7Account(t, db)
created, err := svc.CreateAccountPolicy(context.Background(), account.ID, CreatePolicyRequest{
Name: "Delete Me",
})
require.NoError(t, err)
err = svc.DeleteAccountPolicy(context.Background(), created.ID, account.ID)
require.NoError(t, err)
_, err = svc.GetAccountPolicy(context.Background(), account.ID, created.ID)
require.Error(t, err)
}
func TestAssignmentPolicy_DeleteAccountPolicy_NotFound_Cov7(t *testing.T) {
db, svc := setupCov7AssignmentPolicy(t)
account := seedCov7Account(t, db)
err := svc.DeleteAccountPolicy(context.Background(), 99999, account.ID)
require.Error(t, err)
assert.Contains(t, err.Error(), "not found")
}
func TestAssignmentPolicy_GetInboxPolicy_Cov7(t *testing.T) {
db, svc := setupCov7AssignmentPolicy(t)
account := seedCov7Account(t, db)
inbox := seedCov7Inbox(t, db, account.ID)
policy, err := svc.CreateAccountPolicy(context.Background(), account.ID, CreatePolicyRequest{
Name: "Inbox Policy",
})
require.NoError(t, err)
// Associate inbox with policy
_, err = svc.CreateInboxPolicy(context.Background(), account.ID, CreateInboxPolicyRequest{
InboxID: inbox.ID,
AssignmentPolicyID: policy.ID,
})
require.NoError(t, err)
got, err := svc.GetInboxPolicy(context.Background(), account.ID, inbox.ID)
require.NoError(t, err)
assert.Equal(t, policy.ID, got.ID)
}
func TestAssignmentPolicy_GetInboxPolicy_NotFound_Cov7(t *testing.T) {
db, svc := setupCov7AssignmentPolicy(t)
account := seedCov7Account(t, db)
inbox := seedCov7Inbox(t, db, account.ID)
_, err := svc.GetInboxPolicy(context.Background(), account.ID, inbox.ID)
require.Error(t, err)
assert.Contains(t, err.Error(), "not found")
}
func TestAssignmentPolicy_CreateInboxPolicy_Cov7(t *testing.T) {
db, svc := setupCov7AssignmentPolicy(t)
account := seedCov7Account(t, db)
inbox := seedCov7Inbox(t, db, account.ID)
policy, err := svc.CreateAccountPolicy(context.Background(), account.ID, CreatePolicyRequest{
Name: "Create Inbox Test",
})
require.NoError(t, err)
result, err := svc.CreateInboxPolicy(context.Background(), account.ID, CreateInboxPolicyRequest{
InboxID: inbox.ID,
AssignmentPolicyID: policy.ID,
})
require.NoError(t, err)
assert.Equal(t, policy.ID, result.ID)
}
func TestAssignmentPolicy_CreateInboxPolicy_PolicyNotFound_Cov7(t *testing.T) {
db, svc := setupCov7AssignmentPolicy(t)
account := seedCov7Account(t, db)
inbox := seedCov7Inbox(t, db, account.ID)
_, err := svc.CreateInboxPolicy(context.Background(), account.ID, CreateInboxPolicyRequest{
InboxID: inbox.ID,
AssignmentPolicyID: 99999,
})
require.Error(t, err)
assert.Contains(t, err.Error(), "not found")
}
func TestAssignmentPolicy_CreateInboxPolicy_ValidationFail_Cov7(t *testing.T) {
db, svc := setupCov7AssignmentPolicy(t)
account := seedCov7Account(t, db)
_, err := svc.CreateInboxPolicy(context.Background(), account.ID, CreateInboxPolicyRequest{
InboxID: 0, // required
AssignmentPolicyID: 1,
})
require.Error(t, err)
}
func TestAssignmentPolicy_UpdateInboxPolicy_Cov7(t *testing.T) {
db, svc := setupCov7AssignmentPolicy(t)
account := seedCov7Account(t, db)
inbox := seedCov7Inbox(t, db, account.ID)
policy1, err := svc.CreateAccountPolicy(context.Background(), account.ID, CreatePolicyRequest{
Name: "Policy 1",
})
require.NoError(t, err)
policy2, err := svc.CreateAccountPolicy(context.Background(), account.ID, CreatePolicyRequest{
Name: "Policy 2",
})
require.NoError(t, err)
// Initially associate with policy1
_, err = svc.CreateInboxPolicy(context.Background(), account.ID, CreateInboxPolicyRequest{
InboxID: inbox.ID,
AssignmentPolicyID: policy1.ID,
})
require.NoError(t, err)
// Update to policy2
result, err := svc.UpdateInboxPolicy(context.Background(), inbox.ID, account.ID, UpdateInboxPolicyRequest{
AssignmentPolicyID: policy2.ID,
})
require.NoError(t, err)
assert.Equal(t, policy2.ID, result.ID)
}
func TestAssignmentPolicy_DeleteInboxPolicy_Cov7(t *testing.T) {
db, svc := setupCov7AssignmentPolicy(t)
account := seedCov7Account(t, db)
inbox := seedCov7Inbox(t, db, account.ID)
policy, err := svc.CreateAccountPolicy(context.Background(), account.ID, CreatePolicyRequest{
Name: "Delete Inbox Policy",
})
require.NoError(t, err)
_, err = svc.CreateInboxPolicy(context.Background(), account.ID, CreateInboxPolicyRequest{
InboxID: inbox.ID,
AssignmentPolicyID: policy.ID,
})
require.NoError(t, err)
err = svc.DeleteInboxPolicy(context.Background(), inbox.ID, account.ID)
require.NoError(t, err)
// Verify deletion
_, err = svc.GetInboxPolicy(context.Background(), account.ID, inbox.ID)
require.Error(t, err)
}
func TestAssignmentPolicy_DeleteInboxPolicy_NotFound_Cov7(t *testing.T) {
db, svc := setupCov7AssignmentPolicy(t)
account := seedCov7Account(t, db)
inbox := seedCov7Inbox(t, db, account.ID)
err := svc.DeleteInboxPolicy(context.Background(), inbox.ID, account.ID)
require.Error(t, err)
assert.Contains(t, err.Error(), "not found")
}
func TestAssignmentPolicy_ListPolicyInboxes_Cov7(t *testing.T) {
db, svc := setupCov7AssignmentPolicy(t)
account := seedCov7Account(t, db)
inbox1 := seedCov7Inbox(t, db, account.ID)
inbox2 := &model.Inbox{
AccountID: account.ID,
Name: "Inbox 2",
ChannelType: "web_widget",
ChannelID: 2,
Enabled: true,
}
require.NoError(t, db.Create(inbox2).Error)
policy, err := svc.CreateAccountPolicy(context.Background(), account.ID, CreatePolicyRequest{
Name: "List Inboxes Policy",
})
require.NoError(t, err)
_, err = svc.CreateInboxPolicy(context.Background(), account.ID, CreateInboxPolicyRequest{
InboxID: inbox1.ID,
AssignmentPolicyID: policy.ID,
})
require.NoError(t, err)
_, err = svc.CreateInboxPolicy(context.Background(), account.ID, CreateInboxPolicyRequest{
InboxID: inbox2.ID,
AssignmentPolicyID: policy.ID,
})
require.NoError(t, err)
inboxes, err := svc.ListPolicyInboxes(context.Background(), account.ID, policy.ID)
require.NoError(t, err)
assert.Len(t, inboxes, 2)
}
func TestAssignmentPolicy_ListPolicyInboxes_PolicyNotFound_Cov7(t *testing.T) {
db, svc := setupCov7AssignmentPolicy(t)
account := seedCov7Account(t, db)
_, err := svc.ListPolicyInboxes(context.Background(), account.ID, 99999)
require.Error(t, err)
assert.Contains(t, err.Error(), "not found")
}
func TestAssignmentPolicy_SerializePolicy_Cov7(t *testing.T) {
db, svc := setupCov7AssignmentPolicy(t)
account := seedCov7Account(t, db)
policy, err := svc.CreateAccountPolicy(context.Background(), account.ID, CreatePolicyRequest{
Name: "Serialize Test",
Description: "test desc",
})
require.NoError(t, err)
payload, err := svc.SerializePolicy(context.Background(), policy)
require.NoError(t, err)
assert.Equal(t, policy.ID, payload["id"])
assert.Equal(t, "Serialize Test", payload["name"])
assert.Equal(t, "test desc", payload["description"])
assert.Equal(t, "round_robin", payload["assignment_order"])
assert.Equal(t, "earliest_created", payload["conversation_priority"])
assert.Equal(t, true, payload["enabled"])
assert.Equal(t, int64(0), payload["assigned_inbox_count"])
}
func TestAssignmentPolicy_OrderToValue_Cov7(t *testing.T) {
assert.Equal(t, 0, assignmentOrderToValue("round_robin"))
assert.Equal(t, 1, assignmentOrderToValue("balanced"))
assert.Equal(t, 0, assignmentOrderToValue(""))
}
func TestAssignmentPolicy_OrderFromValue_Cov7(t *testing.T) {
assert.Equal(t, "round_robin", assignmentOrderFromValue(0))
assert.Equal(t, "balanced", assignmentOrderFromValue(1))
}
func TestAssignmentPolicy_PriorityToValue_Cov7(t *testing.T) {
assert.Equal(t, 0, conversationPriorityToValue("earliest_created"))
assert.Equal(t, 1, conversationPriorityToValue("longest_waiting"))
assert.Equal(t, 0, conversationPriorityToValue(""))
}
func TestAssignmentPolicy_PriorityFromValue_Cov7(t *testing.T) {
assert.Equal(t, "earliest_created", conversationPriorityFromValue(0))
assert.Equal(t, "longest_waiting", conversationPriorityFromValue(1))
}
func TestAssignmentPolicy_UnixSeconds_Cov7(t *testing.T) {
now := time.Now()
assert.Equal(t, now.Unix(), unixSeconds(now))
assert.Equal(t, int64(0), unixSeconds(time.Time{}))
}
func TestAssignmentPolicy_IsAssignmentPolicyNotFound_Cov7(t *testing.T) {
assert.True(t, IsAssignmentPolicyNotFound(gorm.ErrRecordNotFound))
assert.True(t, IsAssignmentPolicyNotFound(fmt.Errorf("wrapped: %w", gorm.ErrRecordNotFound)))
assert.False(t, IsAssignmentPolicyNotFound(fmt.Errorf("some other error")))
assert.False(t, IsAssignmentPolicyNotFound(nil))
}