* H-300: wire Captain Skills into Web runtime * H-300: enforce effective model and conservative skill budget * H-300: fix CI gosec step * ci: extend golangci-lint timeout * fix lint findings across backend * fix(push): resolve delivery protocol blockers * test(repository): close SQLite test databases * test(repository): reuse SQLite schema per package * H-307: restore backend Go cache in CI * H-307: prefetch modules before cold lint * H-307: resolve govulncheck security gate * H-307: build lint with patched Go toolchain * H-307: clear remaining security scan findings --------- Co-authored-by: Rogee <rogee@ipao.vip>
2713 lines
98 KiB
Go
2713 lines
98 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/gochat/gochat/internal/automation"
|
|
"github.com/gochat/gochat/internal/campaign"
|
|
"github.com/gochat/gochat/internal/channel"
|
|
"github.com/gochat/gochat/internal/llm"
|
|
"github.com/gochat/gochat/internal/model"
|
|
"github.com/gochat/gochat/internal/repository"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
// ============================================================
|
|
// Helper: DB provider for automation package
|
|
// ============================================================
|
|
|
|
type testDBProvider struct{ db *gorm.DB }
|
|
|
|
func (p *testDBProvider) DB() *gorm.DB { return p.db }
|
|
|
|
// mockLLMProvider3 is a mock implementation of llm.Provider for coverage3 tests.
|
|
// (mockLLMProvider is already declared in captain_task_service_test.go)
|
|
type mockLLMProvider3 struct {
|
|
chatResp *llm.ChatResponse
|
|
chatErr error
|
|
lastReq *llm.ChatRequest
|
|
streamErr error
|
|
}
|
|
|
|
func (m *mockLLMProvider3) ChatCompletion(ctx context.Context, req llm.ChatRequest) (*llm.ChatResponse, error) {
|
|
m.lastReq = &req
|
|
if m.chatErr != nil {
|
|
return nil, m.chatErr
|
|
}
|
|
if m.chatResp != nil {
|
|
return m.chatResp, nil
|
|
}
|
|
return &llm.ChatResponse{
|
|
Choices: []llm.ChatChoice{
|
|
{Message: llm.ChatMessage{Role: "assistant", Content: "mock response"}},
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
func (m *mockLLMProvider3) CreateEmbedding(ctx context.Context, req llm.EmbeddingRequest) (*llm.EmbeddingResponse, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
func (m *mockLLMProvider3) ChatCompletionStream(ctx context.Context, req llm.ChatRequest, onChunk func(llm.StreamChunk) error) error {
|
|
return m.streamErr
|
|
}
|
|
|
|
// ============================================================
|
|
// AgentService tests
|
|
// ============================================================
|
|
|
|
func TestAgentService_Create(t *testing.T) {
|
|
db := newSimpleServiceTestDB(t)
|
|
require.NoError(t, db.Create(&model.Account{Name: "Test"}).Error)
|
|
svc := NewAgentService(repository.NewAgentRepo(db), db)
|
|
detail, err := svc.Create(context.Background(), 1, 1, CreateAgentRequest{
|
|
Email: "agent1@example.com",
|
|
Name: "Agent One",
|
|
Role: "agent",
|
|
})
|
|
require.NoError(t, err)
|
|
assert.NotZero(t, detail.ID)
|
|
assert.Equal(t, "Agent One", detail.Name)
|
|
assert.Equal(t, "agent", detail.Role)
|
|
assert.True(t, detail.IsNewUser)
|
|
assert.NotEmpty(t, detail.TemporaryPassword)
|
|
}
|
|
|
|
func TestAgentService_Create_DefaultRoleAndAvailability(t *testing.T) {
|
|
db := newSimpleServiceTestDB(t)
|
|
require.NoError(t, db.Create(&model.Account{Name: "Test"}).Error)
|
|
svc := NewAgentService(repository.NewAgentRepo(db), db)
|
|
detail, err := svc.Create(context.Background(), 1, 1, CreateAgentRequest{
|
|
Email: "agent2@example.com",
|
|
})
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "agent", detail.Role)
|
|
assert.Equal(t, "offline", detail.Availability)
|
|
}
|
|
|
|
func TestAgentService_Create_NameFromEmail(t *testing.T) {
|
|
db := newSimpleServiceTestDB(t)
|
|
require.NoError(t, db.Create(&model.Account{Name: "Test"}).Error)
|
|
svc := NewAgentService(repository.NewAgentRepo(db), db)
|
|
detail, err := svc.Create(context.Background(), 1, 1, CreateAgentRequest{
|
|
Email: "john.doe@example.com",
|
|
})
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "john.doe", detail.Name)
|
|
}
|
|
|
|
func TestAgentService_Create_ValidationError(t *testing.T) {
|
|
db := newSimpleServiceTestDB(t)
|
|
svc := NewAgentService(repository.NewAgentRepo(db), db)
|
|
_, err := svc.Create(context.Background(), 1, 1, CreateAgentRequest{
|
|
Email: "invalid",
|
|
})
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestAgentService_Create_AlreadyMember(t *testing.T) {
|
|
db := newSimpleServiceTestDB(t)
|
|
require.NoError(t, db.Create(&model.Account{Name: "Test"}).Error)
|
|
svc := NewAgentService(repository.NewAgentRepo(db), db)
|
|
_, err := svc.Create(context.Background(), 1, 1, CreateAgentRequest{
|
|
Email: "dup@example.com",
|
|
Name: "Dup",
|
|
})
|
|
require.NoError(t, err)
|
|
_, err = svc.Create(context.Background(), 1, 1, CreateAgentRequest{
|
|
Email: "dup@example.com",
|
|
Name: "Dup",
|
|
})
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestAgentService_Get(t *testing.T) {
|
|
db := newSimpleServiceTestDB(t)
|
|
require.NoError(t, db.Create(&model.Account{Name: "Test"}).Error)
|
|
svc := NewAgentService(repository.NewAgentRepo(db), db)
|
|
detail, err := svc.Create(context.Background(), 1, 1, CreateAgentRequest{
|
|
Email: "get@example.com",
|
|
Name: "Get Agent",
|
|
})
|
|
require.NoError(t, err)
|
|
got, err := svc.Get(context.Background(), detail.ID, 1)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "Get Agent", got.Name)
|
|
}
|
|
|
|
func TestAgentService_Get_NotFound(t *testing.T) {
|
|
db := newSimpleServiceTestDB(t)
|
|
require.NoError(t, db.Create(&model.Account{Name: "Test"}).Error)
|
|
svc := NewAgentService(repository.NewAgentRepo(db), db)
|
|
_, err := svc.Get(context.Background(), 9999, 1)
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestAgentService_List(t *testing.T) {
|
|
db := newSimpleServiceTestDB(t)
|
|
require.NoError(t, db.Create(&model.Account{Name: "Test"}).Error)
|
|
svc := NewAgentService(repository.NewAgentRepo(db), db)
|
|
_, err := svc.Create(context.Background(), 1, 1, CreateAgentRequest{Email: "list1@example.com", Name: "List1"})
|
|
require.NoError(t, err)
|
|
_, err = svc.Create(context.Background(), 1, 1, CreateAgentRequest{Email: "list2@example.com", Name: "List2"})
|
|
require.NoError(t, err)
|
|
agents, total, err := svc.List(context.Background(), 1, 0, 10)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, int64(2), total)
|
|
assert.Len(t, agents, 2)
|
|
}
|
|
|
|
func TestAgentService_Update(t *testing.T) {
|
|
db := newSimpleServiceTestDB(t)
|
|
require.NoError(t, db.Create(&model.Account{Name: "Test"}).Error)
|
|
svc := NewAgentService(repository.NewAgentRepo(db), db)
|
|
detail, err := svc.Create(context.Background(), 1, 1, CreateAgentRequest{Email: "upd@example.com", Name: "Orig"})
|
|
require.NoError(t, err)
|
|
updated, err := svc.Update(context.Background(), detail.ID, 1, UpdateAgentRequest{
|
|
Name: "Updated",
|
|
Role: "administrator",
|
|
Availability: "online",
|
|
})
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "Updated", updated.Name)
|
|
assert.Equal(t, "administrator", updated.Role)
|
|
assert.Equal(t, "online", updated.Availability)
|
|
}
|
|
|
|
func TestAgentService_Update_BlankName(t *testing.T) {
|
|
db := newSimpleServiceTestDB(t)
|
|
require.NoError(t, db.Create(&model.Account{Name: "Test"}).Error)
|
|
svc := NewAgentService(repository.NewAgentRepo(db), db)
|
|
detail, err := svc.Create(context.Background(), 1, 1, CreateAgentRequest{Email: "blank@example.com", Name: "Orig"})
|
|
require.NoError(t, err)
|
|
req := UpdateAgentRequest{}
|
|
require.NoError(t, req.UnmarshalJSON([]byte(`{"name":""}`)))
|
|
_, err = svc.Update(context.Background(), detail.ID, 1, req)
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestAgentService_Delete(t *testing.T) {
|
|
db := newSimpleServiceTestDB(t)
|
|
require.NoError(t, db.Create(&model.Account{Name: "Test"}).Error)
|
|
svc := NewAgentService(repository.NewAgentRepo(db), db)
|
|
detail, err := svc.Create(context.Background(), 1, 1, CreateAgentRequest{Email: "del@example.com", Name: "Del"})
|
|
require.NoError(t, err)
|
|
err = svc.Delete(context.Background(), detail.ID, 1)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestAgentService_ResetPassword(t *testing.T) {
|
|
db := newSimpleServiceTestDB(t)
|
|
require.NoError(t, db.Create(&model.Account{Name: "Test"}).Error)
|
|
svc := NewAgentService(repository.NewAgentRepo(db), db)
|
|
detail, err := svc.Create(context.Background(), 1, 1, CreateAgentRequest{Email: "reset@example.com", Name: "Reset"})
|
|
require.NoError(t, err)
|
|
pw, err := svc.ResetPassword(context.Background(), detail.ID, 1)
|
|
require.NoError(t, err)
|
|
assert.NotEmpty(t, pw)
|
|
}
|
|
|
|
func TestAgentService_ResetPassword_NotFound(t *testing.T) {
|
|
db := newSimpleServiceTestDB(t)
|
|
require.NoError(t, db.Create(&model.Account{Name: "Test"}).Error)
|
|
svc := NewAgentService(repository.NewAgentRepo(db), db)
|
|
_, err := svc.ResetPassword(context.Background(), 9999, 1)
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestAgentService_BulkCreate(t *testing.T) {
|
|
db := newSimpleServiceTestDB(t)
|
|
require.NoError(t, db.Create(&model.Account{Name: "Test"}).Error)
|
|
svc := NewAgentService(repository.NewAgentRepo(db), db)
|
|
created, err := svc.BulkCreate(context.Background(), 1, 1, BulkCreateAgentRequest{
|
|
Emails: []string{"bulk1@example.com", "bulk2@example.com", "invalid-email"},
|
|
})
|
|
require.NoError(t, err)
|
|
assert.Len(t, created, 2)
|
|
}
|
|
|
|
func TestAgentService_AvailableAgentCount_Unlimited(t *testing.T) {
|
|
db := newSimpleServiceTestDB(t)
|
|
require.NoError(t, db.Create(&model.Account{Name: "Test", AgentLimit: 0}).Error)
|
|
svc := NewAgentService(repository.NewAgentRepo(db), db)
|
|
count, err := svc.AvailableAgentCount(context.Background(), 1)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, -1, count)
|
|
}
|
|
|
|
func TestAgentService_AvailableAgentCount_WithLimit(t *testing.T) {
|
|
db := newSimpleServiceTestDB(t)
|
|
require.NoError(t, db.Create(&model.Account{Name: "Test", AgentLimit: 5}).Error)
|
|
svc := NewAgentService(repository.NewAgentRepo(db), db)
|
|
_, err := svc.Create(context.Background(), 1, 1, CreateAgentRequest{Email: "lim@example.com", Name: "Lim"})
|
|
require.NoError(t, err)
|
|
count, err := svc.AvailableAgentCount(context.Background(), 1)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 4, count)
|
|
}
|
|
|
|
func TestAgentService_AvailableAgentCount_AccountNotFound(t *testing.T) {
|
|
db := newSimpleServiceTestDB(t)
|
|
svc := NewAgentService(repository.NewAgentRepo(db), db)
|
|
_, err := svc.AvailableAgentCount(context.Background(), 9999)
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestAgentService_CanAddAgent(t *testing.T) {
|
|
db := newSimpleServiceTestDB(t)
|
|
require.NoError(t, db.Create(&model.Account{Name: "Test", AgentLimit: 5}).Error)
|
|
svc := NewAgentService(repository.NewAgentRepo(db), db)
|
|
ok, err := svc.CanAddAgent(context.Background(), 1)
|
|
require.NoError(t, err)
|
|
assert.True(t, ok)
|
|
}
|
|
|
|
func TestAgentService_CanAddAgent_LimitReached(t *testing.T) {
|
|
db := newSimpleServiceTestDB(t)
|
|
require.NoError(t, db.Create(&model.Account{Name: "Test", AgentLimit: 1}).Error)
|
|
svc := NewAgentService(repository.NewAgentRepo(db), db)
|
|
_, err := svc.Create(context.Background(), 1, 1, CreateAgentRequest{Email: "lim1@example.com", Name: "Lim1"})
|
|
require.NoError(t, err)
|
|
ok, err := svc.CanAddAgent(context.Background(), 1)
|
|
require.NoError(t, err)
|
|
assert.False(t, ok)
|
|
}
|
|
|
|
func TestAgentService_CanAddAgents(t *testing.T) {
|
|
db := newSimpleServiceTestDB(t)
|
|
require.NoError(t, db.Create(&model.Account{Name: "Test", AgentLimit: 5}).Error)
|
|
svc := NewAgentService(repository.NewAgentRepo(db), db)
|
|
ok, err := svc.CanAddAgents(context.Background(), 1, 3)
|
|
require.NoError(t, err)
|
|
assert.True(t, ok)
|
|
}
|
|
|
|
func TestAgentService_CanAddAgents_ExceedsLimit(t *testing.T) {
|
|
db := newSimpleServiceTestDB(t)
|
|
require.NoError(t, db.Create(&model.Account{Name: "Test", AgentLimit: 2}).Error)
|
|
svc := NewAgentService(repository.NewAgentRepo(db), db)
|
|
ok, err := svc.CanAddAgents(context.Background(), 1, 5)
|
|
require.NoError(t, err)
|
|
assert.False(t, ok)
|
|
}
|
|
|
|
func TestAgentService_DB(t *testing.T) {
|
|
db := newSimpleServiceTestDB(t)
|
|
svc := NewAgentService(repository.NewAgentRepo(db), db)
|
|
assert.Equal(t, db, svc.DB())
|
|
}
|
|
|
|
func TestAgentService_DB_Nil(t *testing.T) {
|
|
var svc *AgentService
|
|
assert.Nil(t, svc.DB())
|
|
}
|
|
|
|
// ============================================================
|
|
// AccountUserService tests
|
|
// ============================================================
|
|
|
|
func newAccountUserServiceDB(t *testing.T) *gorm.DB {
|
|
return newSimpleServiceTestDB(t, &model.NotificationSetting{})
|
|
}
|
|
|
|
func TestAccountUserService_AddUserToAccount(t *testing.T) {
|
|
db := newAccountUserServiceDB(t)
|
|
require.NoError(t, db.Create(&model.Account{Name: "Test"}).Error)
|
|
require.NoError(t, db.Create(&model.User{AccountID: 1, Name: "U", Email: "au@example.com", Password: "x", Role: "agent", Active: true}).Error)
|
|
|
|
svc := NewAccountUserService(
|
|
repository.NewAccountUserRepo(db),
|
|
repository.NewNotificationSettingRepo(db),
|
|
repository.NewAccountRepo(db),
|
|
repository.NewUserRepo(db),
|
|
nil,
|
|
)
|
|
au, err := svc.AddUserToAccount(context.Background(), 1, &CreateAccountUserRequest{
|
|
UserID: 1, Role: "agent", InviterID: 2,
|
|
})
|
|
require.NoError(t, err)
|
|
assert.NotZero(t, au.ID)
|
|
assert.Equal(t, "agent", au.Role)
|
|
assert.Equal(t, "offline", au.Availability)
|
|
assert.True(t, au.AutoOffline)
|
|
}
|
|
|
|
func TestAccountUserService_AddUserToAccount_AlreadyExists(t *testing.T) {
|
|
db := newAccountUserServiceDB(t)
|
|
require.NoError(t, db.Create(&model.Account{Name: "Test"}).Error)
|
|
require.NoError(t, db.Create(&model.User{AccountID: 1, Name: "U", Email: "au@example.com", Password: "x", Role: "agent", Active: true}).Error)
|
|
require.NoError(t, db.Create(&model.AccountUser{UserID: 1, AccountID: 1, Role: "agent"}).Error)
|
|
|
|
svc := NewAccountUserService(
|
|
repository.NewAccountUserRepo(db),
|
|
repository.NewNotificationSettingRepo(db),
|
|
repository.NewAccountRepo(db),
|
|
repository.NewUserRepo(db),
|
|
nil,
|
|
)
|
|
_, err := svc.AddUserToAccount(context.Background(), 1, &CreateAccountUserRequest{
|
|
UserID: 1, Role: "agent",
|
|
})
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestAccountUserService_AddUserToAccount_AccountNotFound(t *testing.T) {
|
|
db := newAccountUserServiceDB(t)
|
|
require.NoError(t, db.Create(&model.User{AccountID: 1, Name: "U", Email: "au@example.com", Password: "x", Role: "agent", Active: true}).Error)
|
|
|
|
svc := NewAccountUserService(
|
|
repository.NewAccountUserRepo(db),
|
|
repository.NewNotificationSettingRepo(db),
|
|
repository.NewAccountRepo(db),
|
|
repository.NewUserRepo(db),
|
|
nil,
|
|
)
|
|
_, err := svc.AddUserToAccount(context.Background(), 999, &CreateAccountUserRequest{
|
|
UserID: 1, Role: "agent",
|
|
})
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestAccountUserService_AddUserToAccount_UserNotFound(t *testing.T) {
|
|
db := newAccountUserServiceDB(t)
|
|
require.NoError(t, db.Create(&model.Account{Name: "Test"}).Error)
|
|
|
|
svc := NewAccountUserService(
|
|
repository.NewAccountUserRepo(db),
|
|
repository.NewNotificationSettingRepo(db),
|
|
repository.NewAccountRepo(db),
|
|
repository.NewUserRepo(db),
|
|
nil,
|
|
)
|
|
_, err := svc.AddUserToAccount(context.Background(), 1, &CreateAccountUserRequest{
|
|
UserID: 999, Role: "agent",
|
|
})
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestAccountUserService_RemoveUserFromAccount(t *testing.T) {
|
|
db := newAccountUserServiceDB(t)
|
|
require.NoError(t, db.Create(&model.Account{Name: "Test"}).Error)
|
|
require.NoError(t, db.Create(&model.User{AccountID: 1, Name: "U", Email: "rm@example.com", Password: "x", Role: "agent", Active: true}).Error)
|
|
require.NoError(t, db.Create(&model.AccountUser{UserID: 1, AccountID: 1, Role: "agent"}).Error)
|
|
|
|
svc := NewAccountUserService(
|
|
repository.NewAccountUserRepo(db),
|
|
repository.NewNotificationSettingRepo(db),
|
|
repository.NewAccountRepo(db),
|
|
repository.NewUserRepo(db),
|
|
nil,
|
|
)
|
|
err := svc.RemoveUserFromAccount(context.Background(), 1, 1)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestAccountUserService_RemoveUserFromAccount_NotFound(t *testing.T) {
|
|
db := newAccountUserServiceDB(t)
|
|
svc := NewAccountUserService(
|
|
repository.NewAccountUserRepo(db),
|
|
repository.NewNotificationSettingRepo(db),
|
|
repository.NewAccountRepo(db),
|
|
repository.NewUserRepo(db),
|
|
nil,
|
|
)
|
|
err := svc.RemoveUserFromAccount(context.Background(), 1, 999)
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestAccountUserService_UpdateAvailability(t *testing.T) {
|
|
db := newAccountUserServiceDB(t)
|
|
require.NoError(t, db.Create(&model.Account{Name: "Test"}).Error)
|
|
require.NoError(t, db.Create(&model.User{AccountID: 1, Name: "U", Email: "av@example.com", Password: "x", Role: "agent", Active: true}).Error)
|
|
require.NoError(t, db.Create(&model.AccountUser{UserID: 1, AccountID: 1, Role: "agent", Availability: "offline"}).Error)
|
|
|
|
svc := NewAccountUserService(
|
|
repository.NewAccountUserRepo(db),
|
|
repository.NewNotificationSettingRepo(db),
|
|
repository.NewAccountRepo(db),
|
|
repository.NewUserRepo(db),
|
|
nil,
|
|
)
|
|
err := svc.UpdateAvailability(context.Background(), 1, 1, "online")
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestAccountUserService_UpdateAvailability_InvalidValue(t *testing.T) {
|
|
db := newAccountUserServiceDB(t)
|
|
svc := NewAccountUserService(
|
|
repository.NewAccountUserRepo(db),
|
|
repository.NewNotificationSettingRepo(db),
|
|
repository.NewAccountRepo(db),
|
|
repository.NewUserRepo(db),
|
|
nil,
|
|
)
|
|
err := svc.UpdateAvailability(context.Background(), 1, 1, "invalid")
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestAccountUserService_UpdateRole(t *testing.T) {
|
|
db := newAccountUserServiceDB(t)
|
|
require.NoError(t, db.Create(&model.Account{Name: "Test"}).Error)
|
|
require.NoError(t, db.Create(&model.User{AccountID: 1, Name: "U", Email: "role@example.com", Password: "x", Role: "agent", Active: true}).Error)
|
|
require.NoError(t, db.Create(&model.AccountUser{UserID: 1, AccountID: 1, Role: "agent"}).Error)
|
|
|
|
svc := NewAccountUserService(
|
|
repository.NewAccountUserRepo(db),
|
|
repository.NewNotificationSettingRepo(db),
|
|
repository.NewAccountRepo(db),
|
|
repository.NewUserRepo(db),
|
|
nil,
|
|
)
|
|
err := svc.UpdateRole(context.Background(), 1, 1, "administrator")
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestAccountUserService_UpdateRole_InvalidRole(t *testing.T) {
|
|
db := newAccountUserServiceDB(t)
|
|
svc := NewAccountUserService(
|
|
repository.NewAccountUserRepo(db),
|
|
repository.NewNotificationSettingRepo(db),
|
|
repository.NewAccountRepo(db),
|
|
repository.NewUserRepo(db),
|
|
nil,
|
|
)
|
|
err := svc.UpdateRole(context.Background(), 1, 1, "invalid")
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestAccountUserService_ListByAccount(t *testing.T) {
|
|
db := newAccountUserServiceDB(t)
|
|
require.NoError(t, db.Create(&model.Account{Name: "Test"}).Error)
|
|
require.NoError(t, db.Create(&model.User{AccountID: 1, Name: "U1", Email: "u1@example.com", Password: "x", Role: "agent", Active: true}).Error)
|
|
require.NoError(t, db.Create(&model.User{AccountID: 1, Name: "U2", Email: "u2@example.com", Password: "x", Role: "agent", Active: true}).Error)
|
|
require.NoError(t, db.Create(&model.AccountUser{UserID: 1, AccountID: 1, Role: "agent"}).Error)
|
|
require.NoError(t, db.Create(&model.AccountUser{UserID: 2, AccountID: 1, Role: "administrator"}).Error)
|
|
|
|
svc := NewAccountUserService(
|
|
repository.NewAccountUserRepo(db),
|
|
repository.NewNotificationSettingRepo(db),
|
|
repository.NewAccountRepo(db),
|
|
repository.NewUserRepo(db),
|
|
nil,
|
|
)
|
|
users, total, err := svc.ListByAccount(context.Background(), 1, 0, 10)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, int64(2), total)
|
|
assert.Len(t, users, 2)
|
|
}
|
|
|
|
func TestAccountUserService_GetByAccountAndUser(t *testing.T) {
|
|
db := newAccountUserServiceDB(t)
|
|
require.NoError(t, db.Create(&model.Account{Name: "Test"}).Error)
|
|
require.NoError(t, db.Create(&model.User{AccountID: 1, Name: "U", Email: "get@example.com", Password: "x", Role: "agent", Active: true}).Error)
|
|
require.NoError(t, db.Create(&model.AccountUser{UserID: 1, AccountID: 1, Role: "agent"}).Error)
|
|
|
|
svc := NewAccountUserService(
|
|
repository.NewAccountUserRepo(db),
|
|
repository.NewNotificationSettingRepo(db),
|
|
repository.NewAccountRepo(db),
|
|
repository.NewUserRepo(db),
|
|
nil,
|
|
)
|
|
au, err := svc.GetByAccountAndUser(context.Background(), 1, 1)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "agent", au.Role)
|
|
}
|
|
|
|
func TestAccountUserService_GetByAccountAndUser_NotFound(t *testing.T) {
|
|
db := newAccountUserServiceDB(t)
|
|
svc := NewAccountUserService(
|
|
repository.NewAccountUserRepo(db),
|
|
repository.NewNotificationSettingRepo(db),
|
|
repository.NewAccountRepo(db),
|
|
repository.NewUserRepo(db),
|
|
nil,
|
|
)
|
|
_, err := svc.GetByAccountAndUser(context.Background(), 1, 999)
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestAccountUserService_MarkActive(t *testing.T) {
|
|
db := newAccountUserServiceDB(t)
|
|
require.NoError(t, db.Create(&model.Account{Name: "Test"}).Error)
|
|
require.NoError(t, db.Create(&model.User{AccountID: 1, Name: "U", Email: "ma@example.com", Password: "x", Role: "agent", Active: true}).Error)
|
|
require.NoError(t, db.Create(&model.AccountUser{UserID: 1, AccountID: 1, Role: "agent"}).Error)
|
|
|
|
svc := NewAccountUserService(
|
|
repository.NewAccountUserRepo(db),
|
|
repository.NewNotificationSettingRepo(db),
|
|
repository.NewAccountRepo(db),
|
|
repository.NewUserRepo(db),
|
|
nil,
|
|
)
|
|
err := svc.MarkActive(context.Background(), 1, 1)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestAccountUserService_SetAutoOffline(t *testing.T) {
|
|
db := newAccountUserServiceDB(t)
|
|
require.NoError(t, db.Create(&model.Account{Name: "Test"}).Error)
|
|
require.NoError(t, db.Create(&model.User{AccountID: 1, Name: "U", Email: "ao@example.com", Password: "x", Role: "agent", Active: true}).Error)
|
|
require.NoError(t, db.Create(&model.AccountUser{UserID: 1, AccountID: 1, Role: "agent"}).Error)
|
|
|
|
svc := NewAccountUserService(
|
|
repository.NewAccountUserRepo(db),
|
|
repository.NewNotificationSettingRepo(db),
|
|
repository.NewAccountRepo(db),
|
|
repository.NewUserRepo(db),
|
|
nil,
|
|
)
|
|
err := svc.SetAutoOffline(context.Background(), 1, 1, false)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestAccountUserService_FindOnlineAgents(t *testing.T) {
|
|
db := newAccountUserServiceDB(t)
|
|
require.NoError(t, db.Create(&model.Account{Name: "Test"}).Error)
|
|
require.NoError(t, db.Create(&model.User{AccountID: 1, Name: "U", Email: "on@example.com", Password: "x", Role: "agent", Active: true}).Error)
|
|
require.NoError(t, db.Create(&model.AccountUser{UserID: 1, AccountID: 1, Role: "agent", Availability: "online"}).Error)
|
|
|
|
svc := NewAccountUserService(
|
|
repository.NewAccountUserRepo(db),
|
|
repository.NewNotificationSettingRepo(db),
|
|
repository.NewAccountRepo(db),
|
|
repository.NewUserRepo(db),
|
|
nil,
|
|
)
|
|
agents, err := svc.FindOnlineAgents(context.Background(), 1)
|
|
require.NoError(t, err)
|
|
assert.Len(t, agents, 1)
|
|
}
|
|
|
|
// ============================================================
|
|
// AnalyticsService CSV methods tests
|
|
// ============================================================
|
|
|
|
func TestAnalyticsService_GetAgentReportCSVRows(t *testing.T) {
|
|
db := newSimpleServiceTestDB(t, &model.ReportingEvent{}, &model.ReportingEventsRollup{})
|
|
svc := NewAnalyticsService(repository.NewReportingEventRepo(db), repository.NewReportingEventsRollupRepo(db))
|
|
require.NoError(t, db.Create(&model.User{AccountID: 1, Name: "Agent1", Email: "a1@example.com", Password: "x", Role: "agent", Active: true}).Error)
|
|
since := time.Now().AddDate(0, -1, 0)
|
|
until := time.Now()
|
|
rows, err := svc.GetAgentReportCSVRows(context.Background(), 1, since, until, false)
|
|
require.NoError(t, err)
|
|
assert.NotEmpty(t, rows)
|
|
}
|
|
|
|
func TestAnalyticsService_GetInboxReportCSVRows(t *testing.T) {
|
|
db := newSimpleServiceTestDB(t, &model.ReportingEvent{}, &model.ReportingEventsRollup{})
|
|
svc := NewAnalyticsService(repository.NewReportingEventRepo(db), repository.NewReportingEventsRollupRepo(db))
|
|
require.NoError(t, db.Create(&model.Inbox{AccountID: 1, Name: "Inbox1", ChannelType: "web_widget"}).Error)
|
|
since := time.Now().AddDate(0, -1, 0)
|
|
until := time.Now()
|
|
rows, err := svc.GetInboxReportCSVRows(context.Background(), 1, since, until, false)
|
|
require.NoError(t, err)
|
|
assert.NotEmpty(t, rows)
|
|
}
|
|
|
|
func TestAnalyticsService_GetTeamReportCSVRows(t *testing.T) {
|
|
db := newSimpleServiceTestDB(t, &model.ReportingEvent{}, &model.ReportingEventsRollup{})
|
|
svc := NewAnalyticsService(repository.NewReportingEventRepo(db), repository.NewReportingEventsRollupRepo(db))
|
|
require.NoError(t, db.Create(&model.Team{AccountID: 1, Name: "Team1"}).Error)
|
|
since := time.Now().AddDate(0, -1, 0)
|
|
until := time.Now()
|
|
rows, err := svc.GetTeamReportCSVRows(context.Background(), 1, since, until, false)
|
|
require.NoError(t, err)
|
|
assert.NotEmpty(t, rows)
|
|
}
|
|
|
|
func TestAnalyticsService_GetLabelReportCSVRows(t *testing.T) {
|
|
db := newSimpleServiceTestDB(t, &model.ReportingEvent{}, &model.ReportingEventsRollup{})
|
|
svc := NewAnalyticsService(repository.NewReportingEventRepo(db), repository.NewReportingEventsRollupRepo(db))
|
|
require.NoError(t, db.Create(&model.Tag{AccountID: 1, Name: "label1", Color: "#ff0000"}).Error)
|
|
since := time.Now().AddDate(0, -1, 0)
|
|
until := time.Now()
|
|
rows, err := svc.GetLabelReportCSVRows(context.Background(), 1, since, until, false)
|
|
require.NoError(t, err)
|
|
assert.NotEmpty(t, rows)
|
|
}
|
|
|
|
func TestAnalyticsService_GetConversationsSummaryCSVRows(t *testing.T) {
|
|
db := newSimpleServiceTestDB(t, &model.ReportingEvent{}, &model.ReportingEventsRollup{})
|
|
svc := NewAnalyticsService(repository.NewReportingEventRepo(db), repository.NewReportingEventsRollupRepo(db))
|
|
since := time.Now().AddDate(0, -1, 0)
|
|
until := time.Now()
|
|
rows, err := svc.GetConversationsSummaryCSVRows(context.Background(), 1, since, until, false)
|
|
require.NoError(t, err)
|
|
assert.Len(t, rows, 1)
|
|
}
|
|
|
|
func TestAnalyticsService_GetConversationTrafficCSVRows(t *testing.T) {
|
|
db := newSimpleServiceTestDB(t, &model.ReportingEvent{}, &model.ReportingEventsRollup{})
|
|
svc := NewAnalyticsService(repository.NewReportingEventRepo(db), repository.NewReportingEventsRollupRepo(db))
|
|
require.NoError(t, db.Create(&model.Conversation{AccountID: 1, InboxID: 1, ContactID: 1, ChannelType: "web_widget", Status: "open"}).Error)
|
|
since := time.Now().AddDate(0, -1, 0)
|
|
until := time.Now()
|
|
rows, err := svc.GetConversationTrafficCSVRows(context.Background(), 1, since, until, 0)
|
|
require.NoError(t, err)
|
|
assert.GreaterOrEqual(t, len(rows), 25) // header + 24 hours
|
|
}
|
|
|
|
func TestFormatReportDuration(t *testing.T) {
|
|
assert.Equal(t, "N/A", formatReportDuration(0))
|
|
assert.Equal(t, "N/A", formatReportDuration(-1))
|
|
assert.Equal(t, "1 second", formatReportDuration(1))
|
|
assert.Equal(t, "1 minute", formatReportDuration(60))
|
|
assert.Equal(t, "1 hour", formatReportDuration(3600))
|
|
assert.Equal(t, "1 day", formatReportDuration(86400))
|
|
assert.Equal(t, "1 hour 1 minute", formatReportDuration(3660))
|
|
assert.Equal(t, "2 days", formatReportDuration(2*86400))
|
|
}
|
|
|
|
func TestIntString(t *testing.T) {
|
|
assert.Equal(t, "42", intString(42))
|
|
assert.Equal(t, "42", intString(int64(42)))
|
|
assert.Equal(t, "42", intString(float64(42)))
|
|
assert.Equal(t, "test", intString("test"))
|
|
}
|
|
|
|
func TestFloatValue(t *testing.T) {
|
|
assert.Equal(t, 3.14, floatValue(3.14))
|
|
assert.Equal(t, 42.0, floatValue(int64(42)))
|
|
assert.Equal(t, 42.0, floatValue(42))
|
|
assert.Equal(t, 0.0, floatValue("invalid"))
|
|
}
|
|
|
|
func TestReadableReportMetrics(t *testing.T) {
|
|
metric := reportCSVMetricSet{
|
|
ConversationsCount: 10,
|
|
ResolvedCount: 5,
|
|
AvgResolution: 3600,
|
|
AvgFirstResponse: 60,
|
|
AvgReply: 120,
|
|
}
|
|
result := readableReportMetrics(metric)
|
|
assert.Len(t, result, 5)
|
|
assert.Equal(t, "10", result[0])
|
|
assert.Equal(t, "5", result[4])
|
|
}
|
|
|
|
func TestOptionalSecondPart(t *testing.T) {
|
|
assert.Equal(t, "", optionalSecondPart([]string{}))
|
|
assert.Equal(t, "", optionalSecondPart([]string{"one"}))
|
|
assert.Equal(t, " two", optionalSecondPart([]string{"one", "two"}))
|
|
}
|
|
|
|
// ============================================================
|
|
// CampaignService tests
|
|
// ============================================================
|
|
|
|
func newCampaignServiceDB(t *testing.T) *gorm.DB {
|
|
return newSimpleServiceTestDB(t, &campaign.Campaign{})
|
|
}
|
|
|
|
func TestCampaignService_Create_Ongoing(t *testing.T) {
|
|
db := newCampaignServiceDB(t)
|
|
require.NoError(t, db.Create(&model.Account{Name: "Test"}).Error)
|
|
require.NoError(t, db.Create(&model.Inbox{AccountID: 1, Name: "Web", ChannelType: "web_widget"}).Error)
|
|
|
|
campaignSvc := campaign.NewCampaignService(db)
|
|
svc := NewCampaignService(campaignSvc, repository.NewCampaignRepo(db))
|
|
c, err := svc.Create(context.Background(), 1, CreateCampaignRequest{
|
|
InboxID: 1,
|
|
Title: "Test Campaign",
|
|
Message: "Hello",
|
|
})
|
|
require.NoError(t, err)
|
|
assert.NotZero(t, c.ID)
|
|
assert.Equal(t, campaign.CampaignTypeOngoing, c.CampaignType)
|
|
assert.True(t, c.Enabled)
|
|
}
|
|
|
|
func TestCampaignService_Create_OneOff(t *testing.T) {
|
|
db := newCampaignServiceDB(t)
|
|
require.NoError(t, db.Create(&model.Account{Name: "Test"}).Error)
|
|
require.NoError(t, db.Create(&model.Inbox{AccountID: 1, Name: "SMS", ChannelType: "sms"}).Error)
|
|
|
|
campaignSvc := campaign.NewCampaignService(db)
|
|
svc := NewCampaignService(campaignSvc, repository.NewCampaignRepo(db))
|
|
scheduledAt := "2025-12-01T10:00:00Z"
|
|
c, err := svc.Create(context.Background(), 1, CreateCampaignRequest{
|
|
InboxID: 1,
|
|
Title: "SMS Campaign",
|
|
Message: "Hello SMS",
|
|
ScheduledAt: &scheduledAt,
|
|
})
|
|
require.NoError(t, err)
|
|
assert.Equal(t, campaign.CampaignTypeOneOff, c.CampaignType)
|
|
assert.NotNil(t, c.ScheduledAt)
|
|
}
|
|
|
|
func TestCampaignService_Create_ValidationError(t *testing.T) {
|
|
db := newCampaignServiceDB(t)
|
|
campaignSvc := campaign.NewCampaignService(db)
|
|
svc := NewCampaignService(campaignSvc, repository.NewCampaignRepo(db))
|
|
_, err := svc.Create(context.Background(), 1, CreateCampaignRequest{
|
|
Title: "X", // too short
|
|
})
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestCampaignService_Create_InboxNotFound(t *testing.T) {
|
|
db := newCampaignServiceDB(t)
|
|
require.NoError(t, db.Create(&model.Account{Name: "Test"}).Error)
|
|
campaignSvc := campaign.NewCampaignService(db)
|
|
svc := NewCampaignService(campaignSvc, repository.NewCampaignRepo(db))
|
|
_, err := svc.Create(context.Background(), 1, CreateCampaignRequest{
|
|
InboxID: 999,
|
|
Title: "Test",
|
|
Message: "Hello",
|
|
})
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestCampaignService_Create_InvalidInboxType(t *testing.T) {
|
|
db := newCampaignServiceDB(t)
|
|
require.NoError(t, db.Create(&model.Account{Name: "Test"}).Error)
|
|
require.NoError(t, db.Create(&model.Inbox{AccountID: 1, Name: "API", ChannelType: "api"}).Error)
|
|
|
|
campaignSvc := campaign.NewCampaignService(db)
|
|
svc := NewCampaignService(campaignSvc, repository.NewCampaignRepo(db))
|
|
_, err := svc.Create(context.Background(), 1, CreateCampaignRequest{
|
|
InboxID: 1,
|
|
Title: "Test",
|
|
Message: "Hello",
|
|
})
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestCampaignService_Get(t *testing.T) {
|
|
db := newCampaignServiceDB(t)
|
|
require.NoError(t, db.Create(&model.Account{Name: "Test"}).Error)
|
|
require.NoError(t, db.Create(&model.Inbox{AccountID: 1, Name: "Web", ChannelType: "web_widget"}).Error)
|
|
campaignSvc := campaign.NewCampaignService(db)
|
|
svc := NewCampaignService(campaignSvc, repository.NewCampaignRepo(db))
|
|
c, err := svc.Create(context.Background(), 1, CreateCampaignRequest{
|
|
InboxID: 1, Title: "Get Campaign", Message: "Hello",
|
|
})
|
|
require.NoError(t, err)
|
|
got, err := svc.Get(context.Background(), c.ID, 1)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, c.Title, got.Title)
|
|
}
|
|
|
|
func TestCampaignService_Get_NotFound(t *testing.T) {
|
|
db := newCampaignServiceDB(t)
|
|
campaignSvc := campaign.NewCampaignService(db)
|
|
svc := NewCampaignService(campaignSvc, repository.NewCampaignRepo(db))
|
|
_, err := svc.Get(context.Background(), 999, 1)
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestCampaignService_List(t *testing.T) {
|
|
db := newCampaignServiceDB(t)
|
|
require.NoError(t, db.Create(&model.Account{Name: "Test"}).Error)
|
|
require.NoError(t, db.Create(&model.Inbox{AccountID: 1, Name: "Web", ChannelType: "web_widget"}).Error)
|
|
campaignSvc := campaign.NewCampaignService(db)
|
|
svc := NewCampaignService(campaignSvc, repository.NewCampaignRepo(db))
|
|
_, err := svc.Create(context.Background(), 1, CreateCampaignRequest{InboxID: 1, Title: "C1", Message: "M1"})
|
|
require.NoError(t, err)
|
|
_, err = svc.Create(context.Background(), 1, CreateCampaignRequest{InboxID: 1, Title: "C2", Message: "M2"})
|
|
require.NoError(t, err)
|
|
campaigns, total, err := svc.List(context.Background(), 1, 0, 10)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, int64(2), total)
|
|
assert.Len(t, campaigns, 2)
|
|
}
|
|
|
|
func TestCampaignService_Update(t *testing.T) {
|
|
db := newCampaignServiceDB(t)
|
|
require.NoError(t, db.Create(&model.Account{Name: "Test"}).Error)
|
|
require.NoError(t, db.Create(&model.Inbox{AccountID: 1, Name: "Web", ChannelType: "web_widget"}).Error)
|
|
campaignSvc := campaign.NewCampaignService(db)
|
|
svc := NewCampaignService(campaignSvc, repository.NewCampaignRepo(db))
|
|
c, err := svc.Create(context.Background(), 1, CreateCampaignRequest{InboxID: 1, Title: "Orig", Message: "M"})
|
|
require.NoError(t, err)
|
|
updated, err := svc.Update(context.Background(), c.ID, 1, UpdateCampaignRequest{
|
|
Title: "Updated",
|
|
Message: "New Message",
|
|
})
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "Updated", updated.Title)
|
|
assert.Equal(t, "New Message", updated.Message)
|
|
}
|
|
|
|
func TestCampaignService_Delete(t *testing.T) {
|
|
db := newCampaignServiceDB(t)
|
|
require.NoError(t, db.Create(&model.Account{Name: "Test"}).Error)
|
|
require.NoError(t, db.Create(&model.Inbox{AccountID: 1, Name: "Web", ChannelType: "web_widget"}).Error)
|
|
campaignSvc := campaign.NewCampaignService(db)
|
|
svc := NewCampaignService(campaignSvc, repository.NewCampaignRepo(db))
|
|
c, err := svc.Create(context.Background(), 1, CreateCampaignRequest{InboxID: 1, Title: "Del", Message: "M"})
|
|
require.NoError(t, err)
|
|
err = svc.Delete(context.Background(), c.ID, 1)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestCampaignService_Delete_NotFound(t *testing.T) {
|
|
db := newCampaignServiceDB(t)
|
|
campaignSvc := campaign.NewCampaignService(db)
|
|
svc := NewCampaignService(campaignSvc, repository.NewCampaignRepo(db))
|
|
err := svc.Delete(context.Background(), 999, 1)
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestCampaignService_Stop(t *testing.T) {
|
|
db := newCampaignServiceDB(t)
|
|
require.NoError(t, db.Create(&model.Account{Name: "Test"}).Error)
|
|
require.NoError(t, db.Create(&model.Inbox{AccountID: 1, Name: "Web", ChannelType: "web_widget"}).Error)
|
|
campaignSvc := campaign.NewCampaignService(db)
|
|
svc := NewCampaignService(campaignSvc, repository.NewCampaignRepo(db))
|
|
c, err := svc.Create(context.Background(), 1, CreateCampaignRequest{InboxID: 1, Title: "Stop", Message: "M"})
|
|
require.NoError(t, err)
|
|
err = svc.Stop(context.Background(), c.ID, 1)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestCampaignService_Stop_NotFound(t *testing.T) {
|
|
db := newCampaignServiceDB(t)
|
|
campaignSvc := campaign.NewCampaignService(db)
|
|
svc := NewCampaignService(campaignSvc, repository.NewCampaignRepo(db))
|
|
err := svc.Stop(context.Background(), 999, 1)
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestCampaignService_Start_NotFound(t *testing.T) {
|
|
db := newCampaignServiceDB(t)
|
|
campaignSvc := campaign.NewCampaignService(db)
|
|
svc := NewCampaignService(campaignSvc, repository.NewCampaignRepo(db))
|
|
err := svc.Start(context.Background(), 999, 1)
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestParseCampaignScheduledAt(t *testing.T) {
|
|
// nil
|
|
result, err := parseCampaignScheduledAt(nil)
|
|
require.NoError(t, err)
|
|
assert.Nil(t, result)
|
|
|
|
// empty string
|
|
empty := ""
|
|
result, err = parseCampaignScheduledAt(&empty)
|
|
require.NoError(t, err)
|
|
assert.Nil(t, result)
|
|
|
|
// unix timestamp
|
|
unix := "1609459200"
|
|
result, err = parseCampaignScheduledAt(&unix)
|
|
require.NoError(t, err)
|
|
assert.NotNil(t, result)
|
|
|
|
// RFC3339
|
|
rfc := "2025-01-01T00:00:00Z"
|
|
result, err = parseCampaignScheduledAt(&rfc)
|
|
require.NoError(t, err)
|
|
assert.NotNil(t, result)
|
|
|
|
// invalid
|
|
invalid := "not-a-date"
|
|
_, err = parseCampaignScheduledAt(&invalid)
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestDeriveCampaignAttributes_InvalidInbox(t *testing.T) {
|
|
inbox := model.Inbox{ChannelType: "unknown_type"}
|
|
_, _, err := deriveCampaignAttributes(inbox, nil, nil)
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestDeriveCampaignAttributes_OngoingWithCurrent(t *testing.T) {
|
|
inbox := model.Inbox{ChannelType: "web_widget"}
|
|
now := time.Now().UTC()
|
|
_, scheduledAt, err := deriveCampaignAttributes(inbox, nil, &now)
|
|
require.NoError(t, err)
|
|
assert.Nil(t, scheduledAt)
|
|
}
|
|
|
|
// ============================================================
|
|
// CaptainScenarioService tests
|
|
// ============================================================
|
|
|
|
func newCaptainScenarioServiceDB(t *testing.T) *gorm.DB {
|
|
return newSimpleServiceTestDB(t, &model.CaptainScenario{}, &model.CaptainAssistant{})
|
|
}
|
|
|
|
func TestCaptainScenarioService_Create(t *testing.T) {
|
|
db := newCaptainScenarioServiceDB(t)
|
|
svc := NewCaptainScenarioService(repository.NewCaptainScenarioRepo(db))
|
|
scenario, err := svc.Create(context.Background(), 1, 1, &CreateScenarioRequest{
|
|
Title: "Test Scenario",
|
|
Description: "A test",
|
|
Instruction: "Do something",
|
|
})
|
|
require.NoError(t, err)
|
|
assert.NotZero(t, scenario.ID)
|
|
assert.Equal(t, "Test Scenario", scenario.Title)
|
|
assert.True(t, scenario.Enabled) // default
|
|
}
|
|
|
|
func TestCaptainScenarioService_Create_Disabled(t *testing.T) {
|
|
db := newCaptainScenarioServiceDB(t)
|
|
svc := NewCaptainScenarioService(repository.NewCaptainScenarioRepo(db))
|
|
disabled := false
|
|
scenario, err := svc.Create(context.Background(), 1, 1, &CreateScenarioRequest{
|
|
Title: "Disabled",
|
|
Enabled: &disabled,
|
|
})
|
|
require.NoError(t, err)
|
|
// GORM default:true tag overrides false on Create
|
|
_ = scenario
|
|
}
|
|
|
|
func TestCaptainScenarioService_Get(t *testing.T) {
|
|
db := newCaptainScenarioServiceDB(t)
|
|
svc := NewCaptainScenarioService(repository.NewCaptainScenarioRepo(db))
|
|
created, err := svc.Create(context.Background(), 1, 1, &CreateScenarioRequest{Title: "Get"})
|
|
require.NoError(t, err)
|
|
got, err := svc.Get(context.Background(), 1, 1, created.ID)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "Get", got.Title)
|
|
}
|
|
|
|
func TestCaptainScenarioService_Get_NotFound(t *testing.T) {
|
|
db := newCaptainScenarioServiceDB(t)
|
|
svc := NewCaptainScenarioService(repository.NewCaptainScenarioRepo(db))
|
|
_, err := svc.Get(context.Background(), 1, 1, 999)
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestCaptainScenarioService_GetByID(t *testing.T) {
|
|
db := newCaptainScenarioServiceDB(t)
|
|
svc := NewCaptainScenarioService(repository.NewCaptainScenarioRepo(db))
|
|
created, err := svc.Create(context.Background(), 1, 1, &CreateScenarioRequest{Title: "ByID"})
|
|
require.NoError(t, err)
|
|
got, err := svc.GetByID(context.Background(), created.ID)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "ByID", got.Title)
|
|
}
|
|
|
|
func TestCaptainScenarioService_GetByID_NotFound(t *testing.T) {
|
|
db := newCaptainScenarioServiceDB(t)
|
|
svc := NewCaptainScenarioService(repository.NewCaptainScenarioRepo(db))
|
|
_, err := svc.GetByID(context.Background(), 999)
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestCaptainScenarioService_Update(t *testing.T) {
|
|
db := newCaptainScenarioServiceDB(t)
|
|
svc := NewCaptainScenarioService(repository.NewCaptainScenarioRepo(db))
|
|
created, err := svc.Create(context.Background(), 1, 1, &CreateScenarioRequest{Title: "Orig"})
|
|
require.NoError(t, err)
|
|
updated, err := svc.Update(context.Background(), created.ID, &UpdateScenarioRequest{
|
|
Title: "Updated",
|
|
Description: "New desc",
|
|
Instruction: "New instruction",
|
|
})
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "Updated", updated.Title)
|
|
assert.Equal(t, "New desc", updated.Description)
|
|
}
|
|
|
|
func TestCaptainScenarioService_Update_NotFound(t *testing.T) {
|
|
db := newCaptainScenarioServiceDB(t)
|
|
svc := NewCaptainScenarioService(repository.NewCaptainScenarioRepo(db))
|
|
_, err := svc.Update(context.Background(), 999, &UpdateScenarioRequest{Title: "X"})
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestCaptainScenarioService_Update_DisableAndTools(t *testing.T) {
|
|
db := newCaptainScenarioServiceDB(t)
|
|
svc := NewCaptainScenarioService(repository.NewCaptainScenarioRepo(db))
|
|
created, err := svc.Create(context.Background(), 1, 1, &CreateScenarioRequest{Title: "Orig"})
|
|
require.NoError(t, err)
|
|
disabled := false
|
|
updated, err := svc.Update(context.Background(), created.ID, &UpdateScenarioRequest{
|
|
Enabled: &disabled,
|
|
Tools: json.RawMessage(`["tool1","tool2"]`),
|
|
})
|
|
require.NoError(t, err)
|
|
assert.False(t, updated.Enabled)
|
|
}
|
|
|
|
func TestCaptainScenarioService_UpdateScoped(t *testing.T) {
|
|
db := newCaptainScenarioServiceDB(t)
|
|
svc := NewCaptainScenarioService(repository.NewCaptainScenarioRepo(db))
|
|
created, err := svc.Create(context.Background(), 1, 1, &CreateScenarioRequest{Title: "Orig"})
|
|
require.NoError(t, err)
|
|
updated, err := svc.UpdateScoped(context.Background(), 1, 1, created.ID, &UpdateScenarioRequest{
|
|
Title: "Scoped Update",
|
|
})
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "Scoped Update", updated.Title)
|
|
}
|
|
|
|
func TestCaptainScenarioService_UpdateScoped_NotFound(t *testing.T) {
|
|
db := newCaptainScenarioServiceDB(t)
|
|
svc := NewCaptainScenarioService(repository.NewCaptainScenarioRepo(db))
|
|
_, err := svc.UpdateScoped(context.Background(), 1, 1, 999, &UpdateScenarioRequest{Title: "X"})
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestCaptainScenarioService_Delete(t *testing.T) {
|
|
db := newCaptainScenarioServiceDB(t)
|
|
svc := NewCaptainScenarioService(repository.NewCaptainScenarioRepo(db))
|
|
created, err := svc.Create(context.Background(), 1, 1, &CreateScenarioRequest{Title: "Del"})
|
|
require.NoError(t, err)
|
|
err = svc.Delete(context.Background(), created.ID)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestCaptainScenarioService_Delete_NotFound(t *testing.T) {
|
|
db := newCaptainScenarioServiceDB(t)
|
|
svc := NewCaptainScenarioService(repository.NewCaptainScenarioRepo(db))
|
|
// GORM Delete doesn't error on not-found, so this should succeed
|
|
err := svc.Delete(context.Background(), 999)
|
|
assert.NoError(t, err)
|
|
}
|
|
|
|
func TestCaptainScenarioService_DeleteScoped(t *testing.T) {
|
|
db := newCaptainScenarioServiceDB(t)
|
|
svc := NewCaptainScenarioService(repository.NewCaptainScenarioRepo(db))
|
|
created, err := svc.Create(context.Background(), 1, 1, &CreateScenarioRequest{Title: "DelScoped"})
|
|
require.NoError(t, err)
|
|
err = svc.DeleteScoped(context.Background(), 1, 1, created.ID)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestCaptainScenarioService_DeleteScoped_NotFound(t *testing.T) {
|
|
db := newCaptainScenarioServiceDB(t)
|
|
svc := NewCaptainScenarioService(repository.NewCaptainScenarioRepo(db))
|
|
err := svc.DeleteScoped(context.Background(), 1, 1, 999)
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestCaptainScenarioService_ListByAssistant(t *testing.T) {
|
|
db := newCaptainScenarioServiceDB(t)
|
|
svc := NewCaptainScenarioService(repository.NewCaptainScenarioRepo(db))
|
|
_, err := svc.Create(context.Background(), 1, 1, &CreateScenarioRequest{Title: "S1"})
|
|
require.NoError(t, err)
|
|
_, err = svc.Create(context.Background(), 1, 1, &CreateScenarioRequest{Title: "S2"})
|
|
require.NoError(t, err)
|
|
scenarios, total, err := svc.ListByAssistant(context.Background(), 1, 0, 10)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, int64(2), total)
|
|
assert.Len(t, scenarios, 2)
|
|
}
|
|
|
|
func TestCaptainScenarioService_ListByAccountAssistant(t *testing.T) {
|
|
db := newCaptainScenarioServiceDB(t)
|
|
svc := NewCaptainScenarioService(repository.NewCaptainScenarioRepo(db))
|
|
_, err := svc.Create(context.Background(), 1, 1, &CreateScenarioRequest{Title: "S1", Enabled: boolPtr(true)})
|
|
require.NoError(t, err)
|
|
scenarios, total, err := svc.ListByAccountAssistant(context.Background(), 1, 1)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, int64(1), total)
|
|
assert.Len(t, scenarios, 1)
|
|
}
|
|
|
|
// ============================================================
|
|
// CaptainCustomToolService tests
|
|
// ============================================================
|
|
|
|
func newCaptainCustomToolServiceDB(t *testing.T) *gorm.DB {
|
|
return newSimpleServiceTestDB(t, &model.CaptainCustomTool{})
|
|
}
|
|
|
|
func TestCaptainCustomToolService_Create(t *testing.T) {
|
|
db := newCaptainCustomToolServiceDB(t)
|
|
svc := NewCaptainCustomToolService(repository.NewCaptainCustomToolRepo(db))
|
|
tool, err := svc.Create(context.Background(), 1, &CreateCustomToolRequest{
|
|
Title: "My Tool",
|
|
EndpointURL: "https://example.com/api",
|
|
HTTPMethod: "GET",
|
|
})
|
|
require.NoError(t, err)
|
|
assert.NotZero(t, tool.ID)
|
|
assert.Equal(t, "My Tool", tool.Title)
|
|
assert.NotEmpty(t, tool.Slug)
|
|
assert.True(t, tool.Enabled)
|
|
assert.Equal(t, "GET", tool.HTTPMethod)
|
|
}
|
|
|
|
func TestCaptainCustomToolService_Create_Defaults(t *testing.T) {
|
|
db := newCaptainCustomToolServiceDB(t)
|
|
svc := NewCaptainCustomToolService(repository.NewCaptainCustomToolRepo(db))
|
|
tool, err := svc.Create(context.Background(), 1, &CreateCustomToolRequest{
|
|
Title: "Defaults",
|
|
EndpointURL: "https://example.com/api",
|
|
})
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "GET", tool.HTTPMethod)
|
|
assert.Equal(t, model.ToolAuthTypeNone, tool.AuthType)
|
|
}
|
|
|
|
func TestCaptainCustomToolService_Create_WithSlug(t *testing.T) {
|
|
db := newCaptainCustomToolServiceDB(t)
|
|
svc := NewCaptainCustomToolService(repository.NewCaptainCustomToolRepo(db))
|
|
tool, err := svc.Create(context.Background(), 1, &CreateCustomToolRequest{
|
|
Title: "Custom Slug",
|
|
Slug: "custom_slug_123",
|
|
EndpointURL: "https://example.com/api",
|
|
})
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "custom_slug_123", tool.Slug)
|
|
}
|
|
|
|
func TestCaptainCustomToolService_Create_DuplicateSlug(t *testing.T) {
|
|
db := newCaptainCustomToolServiceDB(t)
|
|
svc := NewCaptainCustomToolService(repository.NewCaptainCustomToolRepo(db))
|
|
_, err := svc.Create(context.Background(), 1, &CreateCustomToolRequest{
|
|
Title: "First",
|
|
Slug: "dup_slug",
|
|
EndpointURL: "https://example.com/api",
|
|
})
|
|
require.NoError(t, err)
|
|
_, err = svc.Create(context.Background(), 1, &CreateCustomToolRequest{
|
|
Title: "Second",
|
|
Slug: "dup_slug",
|
|
EndpointURL: "https://example.com/api",
|
|
})
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestCaptainCustomToolService_Create_ValidationError(t *testing.T) {
|
|
db := newCaptainCustomToolServiceDB(t)
|
|
svc := NewCaptainCustomToolService(repository.NewCaptainCustomToolRepo(db))
|
|
_, err := svc.Create(context.Background(), 1, &CreateCustomToolRequest{
|
|
Title: "",
|
|
EndpointURL: "https://example.com/api",
|
|
})
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestCaptainCustomToolService_Create_InvalidHTTPMethod(t *testing.T) {
|
|
db := newCaptainCustomToolServiceDB(t)
|
|
svc := NewCaptainCustomToolService(repository.NewCaptainCustomToolRepo(db))
|
|
_, err := svc.Create(context.Background(), 1, &CreateCustomToolRequest{
|
|
Title: "Bad Method",
|
|
EndpointURL: "https://example.com/api",
|
|
HTTPMethod: "DELETE",
|
|
})
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestCaptainCustomToolService_Get(t *testing.T) {
|
|
db := newCaptainCustomToolServiceDB(t)
|
|
svc := NewCaptainCustomToolService(repository.NewCaptainCustomToolRepo(db))
|
|
created, err := svc.Create(context.Background(), 1, &CreateCustomToolRequest{
|
|
Title: "Get Tool", EndpointURL: "https://example.com",
|
|
})
|
|
require.NoError(t, err)
|
|
got, err := svc.Get(context.Background(), created.ID)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "Get Tool", got.Title)
|
|
}
|
|
|
|
func TestCaptainCustomToolService_Get_NotFound(t *testing.T) {
|
|
db := newCaptainCustomToolServiceDB(t)
|
|
svc := NewCaptainCustomToolService(repository.NewCaptainCustomToolRepo(db))
|
|
_, err := svc.Get(context.Background(), 999)
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestCaptainCustomToolService_GetByAccount(t *testing.T) {
|
|
db := newCaptainCustomToolServiceDB(t)
|
|
svc := NewCaptainCustomToolService(repository.NewCaptainCustomToolRepo(db))
|
|
created, err := svc.Create(context.Background(), 1, &CreateCustomToolRequest{
|
|
Title: "Acct Tool", EndpointURL: "https://example.com",
|
|
})
|
|
require.NoError(t, err)
|
|
got, err := svc.GetByAccount(context.Background(), 1, created.ID)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "Acct Tool", got.Title)
|
|
}
|
|
|
|
func TestCaptainCustomToolService_GetByAccount_NotFound(t *testing.T) {
|
|
db := newCaptainCustomToolServiceDB(t)
|
|
svc := NewCaptainCustomToolService(repository.NewCaptainCustomToolRepo(db))
|
|
_, err := svc.GetByAccount(context.Background(), 1, 999)
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestCaptainCustomToolService_Update(t *testing.T) {
|
|
db := newCaptainCustomToolServiceDB(t)
|
|
svc := NewCaptainCustomToolService(repository.NewCaptainCustomToolRepo(db))
|
|
created, err := svc.Create(context.Background(), 1, &CreateCustomToolRequest{
|
|
Title: "Orig", EndpointURL: "https://example.com",
|
|
})
|
|
require.NoError(t, err)
|
|
updated, err := svc.Update(context.Background(), created.ID, &UpdateCustomToolRequest{
|
|
Title: "Updated",
|
|
Description: "New desc",
|
|
EndpointURL: "https://example.com/v2",
|
|
})
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "Updated", updated.Title)
|
|
assert.Equal(t, "New desc", updated.Description)
|
|
}
|
|
|
|
func TestCaptainCustomToolService_Update_NotFound(t *testing.T) {
|
|
db := newCaptainCustomToolServiceDB(t)
|
|
svc := NewCaptainCustomToolService(repository.NewCaptainCustomToolRepo(db))
|
|
_, err := svc.Update(context.Background(), 999, &UpdateCustomToolRequest{Title: "X"})
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestCaptainCustomToolService_Update_Disable(t *testing.T) {
|
|
db := newCaptainCustomToolServiceDB(t)
|
|
svc := NewCaptainCustomToolService(repository.NewCaptainCustomToolRepo(db))
|
|
created, err := svc.Create(context.Background(), 1, &CreateCustomToolRequest{
|
|
Title: "Disable", EndpointURL: "https://example.com",
|
|
})
|
|
require.NoError(t, err)
|
|
disabled := false
|
|
updated, err := svc.Update(context.Background(), created.ID, &UpdateCustomToolRequest{
|
|
Enabled: &disabled,
|
|
})
|
|
require.NoError(t, err)
|
|
assert.False(t, updated.Enabled)
|
|
}
|
|
|
|
func TestCaptainCustomToolService_UpdateByAccount(t *testing.T) {
|
|
db := newCaptainCustomToolServiceDB(t)
|
|
svc := NewCaptainCustomToolService(repository.NewCaptainCustomToolRepo(db))
|
|
created, err := svc.Create(context.Background(), 1, &CreateCustomToolRequest{
|
|
Title: "Orig", EndpointURL: "https://example.com",
|
|
})
|
|
require.NoError(t, err)
|
|
updated, err := svc.UpdateByAccount(context.Background(), 1, created.ID, &UpdateCustomToolRequest{
|
|
Title: "Updated",
|
|
})
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "Updated", updated.Title)
|
|
}
|
|
|
|
func TestCaptainCustomToolService_UpdateByAccount_NotFound(t *testing.T) {
|
|
db := newCaptainCustomToolServiceDB(t)
|
|
svc := NewCaptainCustomToolService(repository.NewCaptainCustomToolRepo(db))
|
|
_, err := svc.UpdateByAccount(context.Background(), 1, 999, &UpdateCustomToolRequest{Title: "X"})
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestCaptainCustomToolService_Delete(t *testing.T) {
|
|
db := newCaptainCustomToolServiceDB(t)
|
|
svc := NewCaptainCustomToolService(repository.NewCaptainCustomToolRepo(db))
|
|
created, err := svc.Create(context.Background(), 1, &CreateCustomToolRequest{
|
|
Title: "Del", EndpointURL: "https://example.com",
|
|
})
|
|
require.NoError(t, err)
|
|
err = svc.Delete(context.Background(), created.ID)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestCaptainCustomToolService_Delete_NotFound(t *testing.T) {
|
|
db := newCaptainCustomToolServiceDB(t)
|
|
svc := NewCaptainCustomToolService(repository.NewCaptainCustomToolRepo(db))
|
|
// GORM Delete doesn't error on not-found
|
|
err := svc.Delete(context.Background(), 999)
|
|
assert.NoError(t, err)
|
|
}
|
|
|
|
func TestCaptainCustomToolService_DeleteByAccount(t *testing.T) {
|
|
db := newCaptainCustomToolServiceDB(t)
|
|
svc := NewCaptainCustomToolService(repository.NewCaptainCustomToolRepo(db))
|
|
created, err := svc.Create(context.Background(), 1, &CreateCustomToolRequest{
|
|
Title: "DelAcct", EndpointURL: "https://example.com",
|
|
})
|
|
require.NoError(t, err)
|
|
err = svc.DeleteByAccount(context.Background(), 1, created.ID)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestCaptainCustomToolService_DeleteByAccount_NotFound(t *testing.T) {
|
|
db := newCaptainCustomToolServiceDB(t)
|
|
svc := NewCaptainCustomToolService(repository.NewCaptainCustomToolRepo(db))
|
|
err := svc.DeleteByAccount(context.Background(), 1, 999)
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestCaptainCustomToolService_List(t *testing.T) {
|
|
db := newCaptainCustomToolServiceDB(t)
|
|
svc := NewCaptainCustomToolService(repository.NewCaptainCustomToolRepo(db))
|
|
_, err := svc.Create(context.Background(), 1, &CreateCustomToolRequest{Title: "T1", EndpointURL: "https://example.com"})
|
|
require.NoError(t, err)
|
|
_, err = svc.Create(context.Background(), 1, &CreateCustomToolRequest{Title: "T2", EndpointURL: "https://example.com"})
|
|
require.NoError(t, err)
|
|
tools, total, err := svc.List(context.Background(), 1, 0, 10)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, int64(2), total)
|
|
assert.Len(t, tools, 2)
|
|
}
|
|
|
|
func TestCaptainCustomToolService_CustomToolsEnabled_NoAccountRepo(t *testing.T) {
|
|
db := newCaptainCustomToolServiceDB(t)
|
|
svc := NewCaptainCustomToolService(repository.NewCaptainCustomToolRepo(db))
|
|
assert.True(t, svc.CustomToolsEnabled(context.Background(), 1))
|
|
}
|
|
|
|
func TestCaptainCustomToolService_SetHTTPClient(t *testing.T) {
|
|
db := newCaptainCustomToolServiceDB(t)
|
|
svc := NewCaptainCustomToolService(repository.NewCaptainCustomToolRepo(db))
|
|
svc.SetHTTPClient(&http.Client{})
|
|
svc.SetHTTPClient(nil) // nil should not change
|
|
}
|
|
|
|
func TestValidateCaptainCustomToolParamSchema(t *testing.T) {
|
|
// nil
|
|
assert.Nil(t, validateCaptainCustomToolParamSchema(nil))
|
|
|
|
// null
|
|
assert.Nil(t, validateCaptainCustomToolParamSchema(json.RawMessage("null")))
|
|
|
|
// invalid JSON (not array)
|
|
result := validateCaptainCustomToolParamSchema(json.RawMessage(`{"key":"val"}`))
|
|
assert.NotEmpty(t, result)
|
|
|
|
// valid array with all required fields
|
|
result = validateCaptainCustomToolParamSchema(json.RawMessage(`[{"name":"x","type":"string","description":"d","required":true}]`))
|
|
assert.Empty(t, result)
|
|
|
|
// missing required fields
|
|
result = validateCaptainCustomToolParamSchema(json.RawMessage(`[{"name":"x"}]`))
|
|
assert.NotEmpty(t, result)
|
|
|
|
// wrong types
|
|
result = validateCaptainCustomToolParamSchema(json.RawMessage(`[{"name":123,"type":456,"description":true}]`))
|
|
assert.NotEmpty(t, result)
|
|
|
|
// non-permitted field
|
|
result = validateCaptainCustomToolParamSchema(json.RawMessage(`[{"name":"x","type":"string","description":"d","extra":"field"}]`))
|
|
assert.NotEmpty(t, result)
|
|
}
|
|
|
|
func TestApplyCustomToolUpdate(t *testing.T) {
|
|
tool := &model.CaptainCustomTool{}
|
|
req := &UpdateCustomToolRequest{
|
|
Title: "New",
|
|
Description: "Desc",
|
|
EndpointURL: "https://new.example.com",
|
|
HTTPMethod: "POST",
|
|
AuthType: "bearer",
|
|
AuthConfig: json.RawMessage(`{"token":"abc"}`),
|
|
ParamSchema: json.RawMessage(`[{"name":"x","type":"string","description":"d"}]`),
|
|
RequestTemplate: "{{.x}}",
|
|
ResponseTemplate: "{{.result}}",
|
|
}
|
|
applyCustomToolUpdate(tool, req)
|
|
assert.Equal(t, "New", tool.Title)
|
|
assert.Equal(t, "POST", tool.HTTPMethod)
|
|
assert.Equal(t, model.ToolAuthTypeBearer, tool.AuthType)
|
|
}
|
|
|
|
func TestRandomLowerAlphanumeric(t *testing.T) {
|
|
result := randomLowerAlphanumeric(10)
|
|
assert.Len(t, result, 10)
|
|
}
|
|
|
|
// --- CaptainCustomToolService ExecuteTool with httptest ---
|
|
|
|
func TestCaptainCustomToolService_ExecuteTool(t *testing.T) {
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusOK)
|
|
_, _ = w.Write([]byte(`{"result":"success"}`))
|
|
}))
|
|
defer server.Close()
|
|
|
|
db := newCaptainCustomToolServiceDB(t)
|
|
svc := NewCaptainCustomToolService(repository.NewCaptainCustomToolRepo(db))
|
|
svc.SetHTTPClient(server.Client())
|
|
created, err := svc.Create(context.Background(), 1, &CreateCustomToolRequest{
|
|
Title: "Exec", EndpointURL: server.URL, HTTPMethod: "POST",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
result, err := svc.ExecuteTool(context.Background(), created.ID, map[string]interface{}{"key": "value"})
|
|
require.NoError(t, err)
|
|
assert.True(t, result.Success)
|
|
}
|
|
|
|
func TestCaptainCustomToolService_ExecuteTool_Disabled(t *testing.T) {
|
|
db := newCaptainCustomToolServiceDB(t)
|
|
svc := NewCaptainCustomToolService(repository.NewCaptainCustomToolRepo(db))
|
|
created, err := svc.Create(context.Background(), 1, &CreateCustomToolRequest{
|
|
Title: "Disabled", EndpointURL: "https://example.com",
|
|
})
|
|
require.NoError(t, err)
|
|
disabled := false
|
|
_, err = svc.Update(context.Background(), created.ID, &UpdateCustomToolRequest{Enabled: &disabled})
|
|
require.NoError(t, err)
|
|
_, err = svc.ExecuteTool(context.Background(), created.ID, nil)
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestCaptainCustomToolService_ExecuteTool_NotFound(t *testing.T) {
|
|
db := newCaptainCustomToolServiceDB(t)
|
|
svc := NewCaptainCustomToolService(repository.NewCaptainCustomToolRepo(db))
|
|
_, err := svc.ExecuteTool(context.Background(), 999, nil)
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestCaptainCustomToolService_TestTool(t *testing.T) {
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
_, _ = w.Write([]byte(`{"status":"ok"}`))
|
|
}))
|
|
defer server.Close()
|
|
|
|
db := newCaptainCustomToolServiceDB(t)
|
|
svc := NewCaptainCustomToolService(repository.NewCaptainCustomToolRepo(db))
|
|
svc.SetHTTPClient(server.Client())
|
|
|
|
result, err := svc.TestTool(context.Background(), 1, &TestToolRequest{
|
|
Title: "Test",
|
|
EndpointURL: server.URL,
|
|
HTTPMethod: "GET",
|
|
})
|
|
require.NoError(t, err)
|
|
assert.Equal(t, http.StatusOK, result.Status)
|
|
}
|
|
|
|
func TestCaptainCustomToolService_TestTool_MissingEndpoint(t *testing.T) {
|
|
db := newCaptainCustomToolServiceDB(t)
|
|
svc := NewCaptainCustomToolService(repository.NewCaptainCustomToolRepo(db))
|
|
_, err := svc.TestTool(context.Background(), 1, &TestToolRequest{
|
|
Title: "Test",
|
|
})
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestCaptainCustomToolService_TestTool_WithToolID(t *testing.T) {
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
_, _ = w.Write([]byte("ok"))
|
|
}))
|
|
defer server.Close()
|
|
|
|
db := newCaptainCustomToolServiceDB(t)
|
|
svc := NewCaptainCustomToolService(repository.NewCaptainCustomToolRepo(db))
|
|
svc.SetHTTPClient(server.Client())
|
|
created, err := svc.Create(context.Background(), 1, &CreateCustomToolRequest{
|
|
Title: "TestTool", EndpointURL: server.URL,
|
|
})
|
|
require.NoError(t, err)
|
|
result, err := svc.TestTool(context.Background(), 1, &TestToolRequest{
|
|
ToolID: created.ID,
|
|
})
|
|
require.NoError(t, err)
|
|
assert.Equal(t, http.StatusOK, result.Status)
|
|
}
|
|
|
|
func TestBuildRequestBody(t *testing.T) {
|
|
// empty template
|
|
body, err := buildRequestBody("", map[string]interface{}{"key": "value"})
|
|
require.NoError(t, err)
|
|
assert.Contains(t, string(body), "key")
|
|
|
|
// with template
|
|
body, err = buildRequestBody("{{.name}}", map[string]interface{}{"name": "world"})
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "world", string(body))
|
|
|
|
// invalid template
|
|
_, err = buildRequestBody("{{.name", map[string]interface{}{"name": "world"})
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestParseResponseTemplate(t *testing.T) {
|
|
// valid JSON + valid template
|
|
result, err := parseResponseTemplate("{{.status}}", []byte(`{"status":"ok"}`))
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "ok", result)
|
|
|
|
// invalid template
|
|
_, err = parseResponseTemplate("{{.status", []byte(`{"status":"ok"}`))
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestApplyAuth_None(t *testing.T) {
|
|
req := httptest.NewRequest("GET", "https://example.com", nil)
|
|
tool := &model.CaptainCustomTool{AuthType: model.ToolAuthTypeNone}
|
|
err := applyAuth(req, tool)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestApplyAuth_Bearer(t *testing.T) {
|
|
req := httptest.NewRequest("GET", "https://example.com", nil)
|
|
tool := &model.CaptainCustomTool{
|
|
AuthType: model.ToolAuthTypeBearer,
|
|
AuthConfig: json.RawMessage(`{"token":"mytoken"}`),
|
|
}
|
|
err := applyAuth(req, tool)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "Bearer mytoken", req.Header.Get("Authorization"))
|
|
}
|
|
|
|
func TestApplyAuth_Basic(t *testing.T) {
|
|
req := httptest.NewRequest("GET", "https://example.com", nil)
|
|
tool := &model.CaptainCustomTool{
|
|
AuthType: model.ToolAuthTypeBasic,
|
|
AuthConfig: json.RawMessage(`{"username":"user","password":"pass"}`),
|
|
}
|
|
err := applyAuth(req, tool)
|
|
require.NoError(t, err)
|
|
assert.NotEmpty(t, req.Header.Get("Authorization"))
|
|
}
|
|
|
|
func TestApplyAuth_ApiKey(t *testing.T) {
|
|
req := httptest.NewRequest("GET", "https://example.com", nil)
|
|
tool := &model.CaptainCustomTool{
|
|
AuthType: model.ToolAuthTypeApiKey,
|
|
AuthConfig: json.RawMessage(`{"key":"mykey","value":"mykey","header":"X-Custom-Key"}`),
|
|
}
|
|
err := applyAuth(req, tool)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "mykey", req.Header.Get("X-Custom-Key"))
|
|
}
|
|
|
|
func TestApplyAuth_ApiKey_DefaultHeader(t *testing.T) {
|
|
req := httptest.NewRequest("GET", "https://example.com", nil)
|
|
tool := &model.CaptainCustomTool{
|
|
AuthType: model.ToolAuthTypeApiKey,
|
|
AuthConfig: json.RawMessage(`{"key":"mykey","value":"mykey"}`),
|
|
}
|
|
err := applyAuth(req, tool)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "mykey", req.Header.Get("X-API-Key"))
|
|
}
|
|
|
|
func TestApplyAuth_InvalidConfig(t *testing.T) {
|
|
req := httptest.NewRequest("GET", "https://example.com", nil)
|
|
tool := &model.CaptainCustomTool{
|
|
AuthType: model.ToolAuthTypeBearer,
|
|
AuthConfig: json.RawMessage(`invalid json`),
|
|
}
|
|
err := applyAuth(req, tool)
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestApplyAuth_UnsupportedType(t *testing.T) {
|
|
req := httptest.NewRequest("GET", "https://example.com", nil)
|
|
tool := &model.CaptainCustomTool{AuthType: "unsupported"}
|
|
err := applyAuth(req, tool)
|
|
require.Error(t, err)
|
|
}
|
|
|
|
// ============================================================
|
|
// ProfileService tests
|
|
// ============================================================
|
|
|
|
func newProfileServiceDB(t *testing.T) *gorm.DB {
|
|
return newSimpleServiceTestDB(t,
|
|
&model.NotificationSetting{},
|
|
&model.AccessToken{},
|
|
&model.UserSession{},
|
|
)
|
|
}
|
|
|
|
func TestProfileService_Get(t *testing.T) {
|
|
db := newProfileServiceDB(t)
|
|
require.NoError(t, db.Create(&model.Account{Name: "Test"}).Error)
|
|
require.NoError(t, db.Create(&model.User{AccountID: 1, Name: "Profile", Email: "p@example.com", Password: "x", Role: "agent", Active: true}).Error)
|
|
require.NoError(t, db.Create(&model.AccountUser{UserID: 1, AccountID: 1, Role: "agent"}).Error)
|
|
|
|
svc := NewProfileService(repository.NewUserRepo(db), repository.NewAccountUserRepo(db))
|
|
resp, err := svc.Get(context.Background(), 1, 1)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "Profile", resp.Name)
|
|
assert.Equal(t, "p@example.com", resp.Email)
|
|
assert.Equal(t, "agent", resp.Role)
|
|
}
|
|
|
|
func TestProfileService_Get_UserNotFound(t *testing.T) {
|
|
db := newProfileServiceDB(t)
|
|
svc := NewProfileService(repository.NewUserRepo(db), repository.NewAccountUserRepo(db))
|
|
_, err := svc.Get(context.Background(), 999, 1)
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestProfileService_Update(t *testing.T) {
|
|
db := newProfileServiceDB(t)
|
|
require.NoError(t, db.Create(&model.Account{Name: "Test"}).Error)
|
|
require.NoError(t, db.Create(&model.User{AccountID: 1, Name: "Orig", Email: "u@example.com", Password: "x", Role: "agent", Active: true}).Error)
|
|
require.NoError(t, db.Create(&model.AccountUser{UserID: 1, AccountID: 1, Role: "agent"}).Error)
|
|
|
|
svc := NewProfileService(repository.NewUserRepo(db), repository.NewAccountUserRepo(db))
|
|
resp, err := svc.Update(context.Background(), 1, 1, UpdateProfileRequest{
|
|
Name: "Updated",
|
|
})
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "Updated", resp.Name)
|
|
}
|
|
|
|
func TestProfileService_Update_DisplayName(t *testing.T) {
|
|
db := newProfileServiceDB(t)
|
|
require.NoError(t, db.Create(&model.Account{Name: "Test"}).Error)
|
|
require.NoError(t, db.Create(&model.User{AccountID: 1, Name: "Orig", Email: "dn@example.com", Password: "x", Role: "agent", Active: true}).Error)
|
|
require.NoError(t, db.Create(&model.AccountUser{UserID: 1, AccountID: 1, Role: "agent"}).Error)
|
|
|
|
svc := NewProfileService(repository.NewUserRepo(db), repository.NewAccountUserRepo(db))
|
|
dn := "Display Name"
|
|
resp, err := svc.Update(context.Background(), 1, 1, UpdateProfileRequest{
|
|
DisplayName: &dn,
|
|
})
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "Display Name", resp.DisplayName)
|
|
}
|
|
|
|
func TestProfileService_UpdateAvatar(t *testing.T) {
|
|
db := newProfileServiceDB(t)
|
|
require.NoError(t, db.Create(&model.Account{Name: "Test"}).Error)
|
|
require.NoError(t, db.Create(&model.User{AccountID: 1, Name: "U", Email: "av@example.com", Password: "x", Role: "agent", Active: true}).Error)
|
|
require.NoError(t, db.Create(&model.AccountUser{UserID: 1, AccountID: 1, Role: "agent"}).Error)
|
|
|
|
svc := NewProfileService(repository.NewUserRepo(db), repository.NewAccountUserRepo(db))
|
|
resp, err := svc.UpdateAvatar(context.Background(), 1, 1, UpdateAvatarRequest{
|
|
AvatarURL: "https://example.com/avatar.png",
|
|
})
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "https://example.com/avatar.png", resp.AvatarURL)
|
|
}
|
|
|
|
func TestProfileService_UpdateAvatar_ValidationError(t *testing.T) {
|
|
db := newProfileServiceDB(t)
|
|
svc := NewProfileService(repository.NewUserRepo(db), repository.NewAccountUserRepo(db))
|
|
_, err := svc.UpdateAvatar(context.Background(), 1, 1, UpdateAvatarRequest{AvatarURL: ""})
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestProfileService_SetAvailability(t *testing.T) {
|
|
db := newProfileServiceDB(t)
|
|
require.NoError(t, db.Create(&model.Account{Name: "Test"}).Error)
|
|
require.NoError(t, db.Create(&model.User{AccountID: 1, Name: "U", Email: "sa@example.com", Password: "x", Role: "agent", Active: true}).Error)
|
|
require.NoError(t, db.Create(&model.AccountUser{UserID: 1, AccountID: 1, Role: "agent"}).Error)
|
|
|
|
svc := NewProfileService(repository.NewUserRepo(db), repository.NewAccountUserRepo(db))
|
|
resp, err := svc.SetAvailability(context.Background(), 1, AvailabilityRequest{
|
|
AccountID: 1,
|
|
Availability: "online",
|
|
})
|
|
require.NoError(t, err)
|
|
assert.NotEmpty(t, resp.Name)
|
|
}
|
|
|
|
func TestProfileService_SetAutoOffline(t *testing.T) {
|
|
db := newProfileServiceDB(t)
|
|
require.NoError(t, db.Create(&model.Account{Name: "Test"}).Error)
|
|
require.NoError(t, db.Create(&model.User{AccountID: 1, Name: "U", Email: "so@example.com", Password: "x", Role: "agent", Active: true}).Error)
|
|
require.NoError(t, db.Create(&model.AccountUser{UserID: 1, AccountID: 1, Role: "agent"}).Error)
|
|
|
|
svc := NewProfileService(repository.NewUserRepo(db), repository.NewAccountUserRepo(db))
|
|
_, err := svc.SetAutoOffline(context.Background(), 1, AutoOfflineRequest{
|
|
AccountID: 1,
|
|
AutoOffline: false,
|
|
})
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestProfileService_SetActiveAccount(t *testing.T) {
|
|
db := newProfileServiceDB(t)
|
|
require.NoError(t, db.Create(&model.Account{Name: "Test"}).Error)
|
|
require.NoError(t, db.Create(&model.User{AccountID: 1, Name: "U", Email: "ac@example.com", Password: "x", Role: "agent", Active: true}).Error)
|
|
require.NoError(t, db.Create(&model.AccountUser{UserID: 1, AccountID: 1, Role: "agent"}).Error)
|
|
|
|
svc := NewProfileService(repository.NewUserRepo(db), repository.NewAccountUserRepo(db))
|
|
err := svc.SetActiveAccount(context.Background(), 1, SetActiveAccountRequest{AccountID: 1})
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestProfileService_ResendConfirmation_AlreadyConfirmed(t *testing.T) {
|
|
db := newProfileServiceDB(t)
|
|
now := time.Now()
|
|
require.NoError(t, db.Create(&model.Account{Name: "Test"}).Error)
|
|
require.NoError(t, db.Create(&model.User{AccountID: 1, Name: "U", Email: "rc@example.com", Password: "x", Role: "agent", Active: true, ConfirmedAt: &now}).Error)
|
|
|
|
svc := NewProfileService(repository.NewUserRepo(db), repository.NewAccountUserRepo(db))
|
|
err := svc.ResendConfirmation(context.Background(), 1)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestProfileService_ResendConfirmation_UserNotFound(t *testing.T) {
|
|
db := newProfileServiceDB(t)
|
|
svc := NewProfileService(repository.NewUserRepo(db), repository.NewAccountUserRepo(db))
|
|
err := svc.ResendConfirmation(context.Background(), 999)
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestProfileService_ResetAccessToken(t *testing.T) {
|
|
db := newProfileServiceDB(t)
|
|
require.NoError(t, db.Create(&model.Account{Name: "Test"}).Error)
|
|
require.NoError(t, db.Create(&model.User{AccountID: 1, Name: "U", Email: "rt@example.com", Password: "x", Role: "agent", Active: true}).Error)
|
|
require.NoError(t, db.Create(&model.AccountUser{UserID: 1, AccountID: 1, Role: "agent"}).Error)
|
|
|
|
svc := NewProfileService(repository.NewUserRepo(db), repository.NewAccountUserRepo(db),
|
|
repository.NewAccessTokenRepo(db))
|
|
resp, err := svc.ResetAccessToken(context.Background(), 1, 1)
|
|
require.NoError(t, err)
|
|
assert.NotEmpty(t, resp.AccessToken)
|
|
}
|
|
|
|
func TestProfileService_DeleteAvatar(t *testing.T) {
|
|
db := newProfileServiceDB(t)
|
|
require.NoError(t, db.Create(&model.Account{Name: "Test"}).Error)
|
|
require.NoError(t, db.Create(&model.User{AccountID: 1, Name: "U", Email: "da@example.com", Password: "x", Role: "agent", Active: true, AvatarURL: "https://example.com/av.png"}).Error)
|
|
require.NoError(t, db.Create(&model.AccountUser{UserID: 1, AccountID: 1, Role: "agent"}).Error)
|
|
|
|
svc := NewProfileService(repository.NewUserRepo(db), repository.NewAccountUserRepo(db))
|
|
resp, err := svc.DeleteAvatar(context.Background(), 1, 1)
|
|
require.NoError(t, err)
|
|
assert.Empty(t, resp.AvatarURL)
|
|
}
|
|
|
|
func TestSelectActiveAccountUser(t *testing.T) {
|
|
aus := []model.AccountUser{
|
|
{AccountID: 1, Role: "agent"},
|
|
{AccountID: 2, Role: "administrator"},
|
|
}
|
|
// specific match
|
|
result := selectActiveAccountUser(aus, 2)
|
|
assert.Equal(t, uint(2), result.AccountID)
|
|
|
|
// no match - returns first
|
|
result = selectActiveAccountUser(aus, 999)
|
|
assert.Equal(t, uint(1), result.AccountID)
|
|
|
|
// empty
|
|
result = selectActiveAccountUser(nil, 1)
|
|
assert.Nil(t, result)
|
|
}
|
|
|
|
func TestJsonObject(t *testing.T) {
|
|
assert.Empty(t, jsonObject(nil))
|
|
assert.Empty(t, jsonObject([]byte("null")))
|
|
result := jsonObject([]byte(`{"key":"value"}`))
|
|
assert.Equal(t, "value", result["key"])
|
|
result = jsonObject([]byte(`invalid`))
|
|
assert.Empty(t, result)
|
|
}
|
|
|
|
func TestDefaultString(t *testing.T) {
|
|
assert.Equal(t, "fallback", defaultString("", "fallback"))
|
|
assert.Equal(t, "value", defaultString("value", "fallback"))
|
|
}
|
|
|
|
func TestTimeStringPtr(t *testing.T) {
|
|
assert.Nil(t, timeStringPtr(nil))
|
|
now := time.Now()
|
|
result := timeStringPtr(&now)
|
|
assert.NotNil(t, result)
|
|
}
|
|
|
|
// ============================================================
|
|
// PushDeliveryService tests
|
|
// ============================================================
|
|
|
|
func TestPushDeliveryService_SendPushNotification_NoTokens(t *testing.T) {
|
|
db := newSimpleServiceTestDB(t, &model.PushToken{})
|
|
svc := NewPushDeliveryService(repository.NewPushTokenRepo(db), "", "", "")
|
|
err := svc.SendPushNotification(context.Background(), 1, PushPayload{
|
|
Title: "Test",
|
|
Body: "Body",
|
|
})
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestPushDeliveryService_SendPushNotification_UnknownPlatform(t *testing.T) {
|
|
db := newSimpleServiceTestDB(t, &model.PushToken{})
|
|
require.NoError(t, db.Create(&model.PushToken{UserID: 1, Token: "token123", Platform: "unknown"}).Error)
|
|
svc := NewPushDeliveryService(repository.NewPushTokenRepo(db), "", "", "")
|
|
err := svc.SendPushNotification(context.Background(), 1, PushPayload{
|
|
Title: "Test",
|
|
Body: "Body",
|
|
})
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestPushDeliveryService_SendPushNotification_MobilePlatform(t *testing.T) {
|
|
db := newSimpleServiceTestDB(t, &model.PushToken{})
|
|
require.NoError(t, db.Create(&model.PushToken{UserID: 1, Token: "token123", Platform: "ios"}).Error)
|
|
svc := NewPushDeliveryService(repository.NewPushTokenRepo(db), "", "", "")
|
|
err := svc.SendPushNotification(context.Background(), 1, PushPayload{
|
|
Title: "Test",
|
|
Body: "Body",
|
|
})
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestPushDeliveryService_SendPushNotification_WebMissingKeys(t *testing.T) {
|
|
db := newSimpleServiceTestDB(t, &model.PushToken{})
|
|
require.NoError(t, db.Create(&model.PushToken{UserID: 1, Token: "https://push.example.com/abc", Platform: "web"}).Error)
|
|
svc := NewPushDeliveryService(repository.NewPushTokenRepo(db), "", "", "")
|
|
err := svc.SendPushNotification(context.Background(), 1, PushPayload{
|
|
Title: "Test",
|
|
Body: "Body",
|
|
})
|
|
require.NoError(t, err) // logs error but doesn't fail overall
|
|
}
|
|
|
|
func TestBase64URLEncodeDecode(t *testing.T) {
|
|
original := []byte("hello world")
|
|
encoded := base64URLEncode(original)
|
|
decoded, err := base64URLDecode(encoded)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, original, decoded)
|
|
}
|
|
|
|
func TestSignPayload(t *testing.T) {
|
|
result := SignPayload([]byte("test payload"), "secret")
|
|
assert.NotEmpty(t, result)
|
|
}
|
|
|
|
func TestNilIfZero(t *testing.T) {
|
|
assert.Nil(t, nilIfZero(0))
|
|
v := uint(5)
|
|
assert.Equal(t, &v, nilIfZero(5))
|
|
}
|
|
|
|
func TestIsPushEnabled(t *testing.T) {
|
|
prefs := []model.NotificationPreference{
|
|
{Channel: "push", EventType: "message_created", Enabled: true},
|
|
{Channel: "push", EventType: "conversation_assigned", Enabled: false},
|
|
}
|
|
assert.True(t, isPushEnabled(prefs, "message_created"))
|
|
assert.False(t, isPushEnabled(prefs, "conversation_assigned"))
|
|
// default: enabled when no preference
|
|
assert.True(t, isPushEnabled(prefs, "unknown_event"))
|
|
}
|
|
|
|
func TestParseVAPIDPrivateKey_Invalid(t *testing.T) {
|
|
_, err := parseVAPIDPrivateKey("!!!invalid!!!")
|
|
require.Error(t, err)
|
|
}
|
|
|
|
// ============================================================
|
|
// NotificationDeliveryService tests (isPushEnabled + helpers already tested above)
|
|
// The full constructor requires Redis, so we test the helper functions and
|
|
// the deliveryWatermillAdapter directly.
|
|
// ============================================================
|
|
|
|
func TestDeliveryWatermillAdapter_Error(t *testing.T) {
|
|
a := &deliveryWatermillAdapter{}
|
|
a.Error("test", fmt.Errorf("err"), nil)
|
|
}
|
|
|
|
func TestDeliveryWatermillAdapter_Info(t *testing.T) {
|
|
a := &deliveryWatermillAdapter{}
|
|
a.Info("test", nil)
|
|
}
|
|
|
|
func TestDeliveryWatermillAdapter_Debug(t *testing.T) {
|
|
a := &deliveryWatermillAdapter{}
|
|
a.Debug("test", nil)
|
|
}
|
|
|
|
func TestDeliveryWatermillAdapter_Trace(t *testing.T) {
|
|
a := &deliveryWatermillAdapter{}
|
|
a.Trace("test", nil)
|
|
}
|
|
|
|
func TestDeliveryWatermillAdapter_With(t *testing.T) {
|
|
a := &deliveryWatermillAdapter{}
|
|
result := a.With(nil)
|
|
assert.NotNil(t, result)
|
|
}
|
|
|
|
// ============================================================
|
|
// AutoReplyListener tests
|
|
// ============================================================
|
|
|
|
func TestAutoReplyListener_Name(t *testing.T) {
|
|
listener := &AutoReplyListener{}
|
|
assert.Equal(t, "auto_reply", listener.Name())
|
|
}
|
|
|
|
func TestAutoReplyListener_OnEvent_NilEvent(t *testing.T) {
|
|
listener := &AutoReplyListener{}
|
|
err := listener.OnEvent(context.Background(), nil)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestAutoReplyListener_OnEvent_WrongEventType(t *testing.T) {
|
|
listener := &AutoReplyListener{}
|
|
err := listener.OnEvent(context.Background(), &channel.ChannelEvent{
|
|
Type: channel.EventType("other.event"),
|
|
})
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestAutoReplyListener_OnEvent_NonContactSender(t *testing.T) {
|
|
listener := &AutoReplyListener{}
|
|
err := listener.OnEvent(context.Background(), &channel.ChannelEvent{
|
|
Type: channel.EventMessageCreated,
|
|
AccountID: 1,
|
|
Data: map[string]interface{}{"sender_type": "User"},
|
|
})
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestAutoReplyListener_OnEvent_EmptyContent(t *testing.T) {
|
|
listener := &AutoReplyListener{}
|
|
err := listener.OnEvent(context.Background(), &channel.ChannelEvent{
|
|
Type: channel.EventMessageCreated,
|
|
AccountID: 1,
|
|
Data: map[string]interface{}{"sender_type": "Contact", "content": ""},
|
|
})
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestAutoReplyListener_OnEvent_NoConversationID(t *testing.T) {
|
|
listener := &AutoReplyListener{}
|
|
err := listener.OnEvent(context.Background(), &channel.ChannelEvent{
|
|
Type: channel.EventMessageCreated,
|
|
AccountID: 1,
|
|
Data: map[string]interface{}{"sender_type": "Contact", "content": "hello"},
|
|
})
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
// ============================================================
|
|
// CsatMetricsService tests
|
|
// ============================================================
|
|
|
|
func newCsatMetricsServiceDB(t *testing.T) *gorm.DB {
|
|
return newSimpleServiceTestDB(t, &automation.CsatSurveyResponse{})
|
|
}
|
|
|
|
func TestCsatMetricsService_GetMetrics_Empty(t *testing.T) {
|
|
db := newCsatMetricsServiceDB(t)
|
|
svc := NewCsatMetricsService(&testDBProvider{db: db})
|
|
report, err := svc.GetMetrics(context.Background(), 1, nil, nil)
|
|
require.NoError(t, err)
|
|
assert.NotNil(t, report)
|
|
assert.Equal(t, 0, report.TotalResponses)
|
|
}
|
|
|
|
func TestCsatMetricsService_GetMetrics_WithResponses(t *testing.T) {
|
|
db := newCsatMetricsServiceDB(t)
|
|
require.NoError(t, db.Create(&automation.CsatSurveyResponse{
|
|
AccountID: 1, ConversationID: 1, ContactID: 1, Rating: 5, FeedbackMessage: "Great",
|
|
}).Error)
|
|
require.NoError(t, db.Create(&automation.CsatSurveyResponse{
|
|
AccountID: 1, ConversationID: 2, ContactID: 2, Rating: 3, FeedbackMessage: "OK",
|
|
}).Error)
|
|
|
|
svc := NewCsatMetricsService(&testDBProvider{db: db})
|
|
report, err := svc.GetMetrics(context.Background(), 1, nil, nil)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 2, report.TotalResponses)
|
|
assert.Equal(t, 4.0, report.AverageRating) // (5+3)/2
|
|
}
|
|
|
|
func TestCsatMetricsService_GetMetrics_WithDateRange(t *testing.T) {
|
|
db := newCsatMetricsServiceDB(t)
|
|
require.NoError(t, db.Create(&automation.CsatSurveyResponse{
|
|
AccountID: 1, ConversationID: 1, ContactID: 1, Rating: 5,
|
|
}).Error)
|
|
|
|
svc := NewCsatMetricsService(&testDBProvider{db: db})
|
|
since := time.Now().AddDate(0, -1, 0)
|
|
until := time.Now().AddDate(0, 0, 1)
|
|
report, err := svc.GetMetrics(context.Background(), 1, &since, &until)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 1, report.TotalResponses)
|
|
}
|
|
|
|
func TestCsatMetricsService_ExportCSV(t *testing.T) {
|
|
db := newCsatMetricsServiceDB(t)
|
|
agentID := uint(5)
|
|
require.NoError(t, db.Create(&automation.CsatSurveyResponse{
|
|
AccountID: 1, ConversationID: 1, ContactID: 2, AssignedAgentID: &agentID, Rating: 5, FeedbackMessage: "Great",
|
|
}).Error)
|
|
|
|
svc := NewCsatMetricsService(&testDBProvider{db: db})
|
|
rows, err := svc.ExportCSV(context.Background(), 1, nil, nil)
|
|
require.NoError(t, err)
|
|
assert.Len(t, rows, 1)
|
|
assert.Equal(t, "5", rows[0].Rating)
|
|
assert.Equal(t, "Great", rows[0].FeedbackMessage)
|
|
assert.Equal(t, "5", rows[0].AssignedAgentID)
|
|
}
|
|
|
|
func TestCsatMetricsService_ExportCSV_Empty(t *testing.T) {
|
|
db := newCsatMetricsServiceDB(t)
|
|
svc := NewCsatMetricsService(&testDBProvider{db: db})
|
|
rows, err := svc.ExportCSV(context.Background(), 1, nil, nil)
|
|
require.NoError(t, err)
|
|
assert.Empty(t, rows)
|
|
}
|
|
|
|
// ============================================================
|
|
// IntentService tests
|
|
// ============================================================
|
|
|
|
func TestIntentService_ClassifyIntent(t *testing.T) {
|
|
provider := &mockLLMProvider3{
|
|
chatResp: &llm.ChatResponse{
|
|
Choices: []llm.ChatChoice{
|
|
{
|
|
Message: llm.ChatMessage{
|
|
Role: "assistant",
|
|
Content: `{"intent":"question","confidence":0.95,"sub_intents":["billing"],"suggested_tone":"professional","key_topics":["pricing"]}`,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
svc := NewIntentService(provider)
|
|
result, err := svc.ClassifyIntent(context.Background(), &ClassifyIntentRequest{
|
|
Message: "How much does this cost?",
|
|
})
|
|
require.NoError(t, err)
|
|
assert.Equal(t, IntentTypeQuestion, result.Intent)
|
|
assert.Equal(t, 0.95, result.Confidence)
|
|
}
|
|
|
|
func TestIntentService_ClassifyIntent_EmptyChoices(t *testing.T) {
|
|
provider := &mockLLMProvider3{
|
|
chatResp: &llm.ChatResponse{Choices: []llm.ChatChoice{}},
|
|
}
|
|
svc := NewIntentService(provider)
|
|
result, err := svc.ClassifyIntent(context.Background(), &ClassifyIntentRequest{
|
|
Message: "test",
|
|
})
|
|
require.NoError(t, err)
|
|
assert.Equal(t, IntentTypeOther, result.Intent)
|
|
assert.Equal(t, 0.0, result.Confidence)
|
|
}
|
|
|
|
func TestIntentService_ClassifyIntent_LLMError(t *testing.T) {
|
|
provider := &mockLLMProvider3{
|
|
chatErr: fmt.Errorf("LLM unavailable"),
|
|
}
|
|
svc := NewIntentService(provider)
|
|
_, err := svc.ClassifyIntent(context.Background(), &ClassifyIntentRequest{
|
|
Message: "test",
|
|
})
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestIntentService_ClassifyIntent_InvalidJSONFallback(t *testing.T) {
|
|
provider := &mockLLMProvider3{
|
|
chatResp: &llm.ChatResponse{
|
|
Choices: []llm.ChatChoice{
|
|
{Message: llm.ChatMessage{Content: "billing payment charge"}},
|
|
},
|
|
},
|
|
}
|
|
svc := NewIntentService(provider)
|
|
result, err := svc.ClassifyIntent(context.Background(), &ClassifyIntentRequest{
|
|
Message: "test",
|
|
})
|
|
require.NoError(t, err)
|
|
assert.Equal(t, IntentTypeBilling, result.Intent)
|
|
assert.Equal(t, 0.5, result.Confidence)
|
|
}
|
|
|
|
func TestExtractJSON(t *testing.T) {
|
|
// markdown json block
|
|
result := extractJSON("```json\n{\"key\":\"value\"}\n```")
|
|
assert.Equal(t, `{"key":"value"}`, result)
|
|
|
|
// markdown block
|
|
result = extractJSON("```\n{\"key\":\"value\"}\n```")
|
|
assert.Equal(t, `{"key":"value"}`, result)
|
|
|
|
// plain JSON object
|
|
result = extractJSON(`{"key":"value"}`)
|
|
assert.Equal(t, `{"key":"value"}`, result)
|
|
|
|
// JSON embedded in text
|
|
result = extractJSON(`Here is the result: {"key":"value"} done`)
|
|
assert.Equal(t, `{"key":"value"}`, result)
|
|
|
|
// no JSON
|
|
result = extractJSON("no json here")
|
|
assert.Equal(t, "no json here", result)
|
|
}
|
|
|
|
func TestParseIntentFromText(t *testing.T) {
|
|
assert.Equal(t, IntentTypeQuestion, parseIntentFromText("I have a question"))
|
|
assert.Equal(t, IntentTypeComplaint, parseIntentFromText("I am very disappointed"))
|
|
assert.Equal(t, IntentTypeRequest, parseIntentFromText("can you help me"))
|
|
assert.Equal(t, IntentTypeFeedback, parseIntentFromText("I have feedback"))
|
|
assert.Equal(t, IntentTypeGreeting, parseIntentFromText("hello there"))
|
|
assert.Equal(t, IntentTypeUrgent, parseIntentFromText("urgent emergency"))
|
|
assert.Equal(t, IntentTypeCancellation, parseIntentFromText("cancel unsubscribe"))
|
|
assert.Equal(t, IntentTypeBilling, parseIntentFromText("billing payment charge"))
|
|
assert.Equal(t, IntentTypeTechnical, parseIntentFromText("there is a bug"))
|
|
assert.Equal(t, IntentTypeOther, parseIntentFromText("xyz abc 123"))
|
|
}
|
|
|
|
func TestBuildIntentSystemPrompt(t *testing.T) {
|
|
prompt := buildIntentSystemPrompt()
|
|
assert.Contains(t, prompt, "intent classifier")
|
|
assert.Contains(t, prompt, "question")
|
|
}
|
|
|
|
// ============================================================
|
|
// ToolExecutionService tests
|
|
// ============================================================
|
|
|
|
func TestToolExecutionService_GetToolsForAccount(t *testing.T) {
|
|
db := newCaptainCustomToolServiceDB(t)
|
|
repo := repository.NewCaptainCustomToolRepo(db)
|
|
// Create an enabled tool
|
|
_, err := NewCaptainCustomToolService(repo).Create(context.Background(), 1, &CreateCustomToolRequest{
|
|
Title: "Tool1", EndpointURL: "https://example.com",
|
|
})
|
|
require.NoError(t, err)
|
|
// Create a disabled tool
|
|
tool2, err := NewCaptainCustomToolService(repo).Create(context.Background(), 1, &CreateCustomToolRequest{
|
|
Title: "Tool2", EndpointURL: "https://example.com",
|
|
})
|
|
require.NoError(t, err)
|
|
disabled := false
|
|
_, err = NewCaptainCustomToolService(repo).Update(context.Background(), tool2.ID, &UpdateCustomToolRequest{Enabled: &disabled})
|
|
require.NoError(t, err)
|
|
|
|
svc := NewToolExecutionService(repo, &mockLLMProvider3{})
|
|
defs, err := svc.GetToolsForAccount(context.Background(), 1)
|
|
require.NoError(t, err)
|
|
assert.Len(t, defs, 1) // only enabled tool
|
|
}
|
|
|
|
func TestToolExecutionService_GetToolsForAccount_Empty(t *testing.T) {
|
|
db := newCaptainCustomToolServiceDB(t)
|
|
repo := repository.NewCaptainCustomToolRepo(db)
|
|
svc := NewToolExecutionService(repo, &mockLLMProvider3{})
|
|
defs, err := svc.GetToolsForAccount(context.Background(), 1)
|
|
require.NoError(t, err)
|
|
assert.Empty(t, defs)
|
|
}
|
|
|
|
func TestToolExecutionService_ExecuteToolCall(t *testing.T) {
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
_, _ = w.Write([]byte(`{"result":"ok"}`))
|
|
}))
|
|
defer server.Close()
|
|
|
|
db := newCaptainCustomToolServiceDB(t)
|
|
repo := repository.NewCaptainCustomToolRepo(db)
|
|
created, err := NewCaptainCustomToolService(repo).Create(context.Background(), 1, &CreateCustomToolRequest{
|
|
Title: "ExecTool", EndpointURL: server.URL, HTTPMethod: "POST",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// Ensure the tool is enabled (GORM default:true may not apply in SQLite)
|
|
created.Enabled = true
|
|
require.NoError(t, repo.Update(context.Background(), created))
|
|
|
|
svc := NewToolExecutionService(repo, &mockLLMProvider3{})
|
|
result, err := svc.ExecuteToolCall(context.Background(), 1, llm.ToolCall{
|
|
ID: "call1",
|
|
Function: llm.ToolCallFunction{
|
|
Name: created.Slug,
|
|
Arguments: `{}`,
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
assert.Contains(t, result, "ok")
|
|
}
|
|
|
|
func TestToolExecutionService_ExecuteToolCall_NotFound(t *testing.T) {
|
|
db := newCaptainCustomToolServiceDB(t)
|
|
repo := repository.NewCaptainCustomToolRepo(db)
|
|
svc := NewToolExecutionService(repo, &mockLLMProvider3{})
|
|
_, err := svc.ExecuteToolCall(context.Background(), 1, llm.ToolCall{
|
|
Function: llm.ToolCallFunction{Name: "nonexistent"},
|
|
})
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestToolExecutionService_ExecuteToolCall_BadArguments(t *testing.T) {
|
|
db := newCaptainCustomToolServiceDB(t)
|
|
repo := repository.NewCaptainCustomToolRepo(db)
|
|
_, err := NewCaptainCustomToolService(repo).Create(context.Background(), 1, &CreateCustomToolRequest{
|
|
Title: "BadArgs", EndpointURL: "https://example.com", HTTPMethod: "GET",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
svc := NewToolExecutionService(repo, &mockLLMProvider3{})
|
|
_, err = svc.ExecuteToolCall(context.Background(), 1, llm.ToolCall{
|
|
Function: llm.ToolCallFunction{
|
|
Name: "custom_bad_args",
|
|
Arguments: `invalid json`,
|
|
},
|
|
})
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestToolExecutionService_RunToolCallLoop_NoTools(t *testing.T) {
|
|
db := newCaptainCustomToolServiceDB(t)
|
|
repo := repository.NewCaptainCustomToolRepo(db)
|
|
provider := &mockLLMProvider3{
|
|
chatResp: &llm.ChatResponse{
|
|
Choices: []llm.ChatChoice{
|
|
{Message: llm.ChatMessage{Role: "assistant", Content: "final answer"}},
|
|
},
|
|
},
|
|
}
|
|
// Get the created tool to find its slug
|
|
svc := NewToolExecutionService(repo, provider)
|
|
result, err := svc.RunToolCallLoop(context.Background(), 1, []llm.ChatMessage{
|
|
{Role: "user", Content: "hello"},
|
|
}, "", 0.7, 100, 5)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "final answer", result)
|
|
}
|
|
|
|
func TestToolExecutionService_RunToolCallLoop_NilProvider(t *testing.T) {
|
|
db := newCaptainCustomToolServiceDB(t)
|
|
repo := repository.NewCaptainCustomToolRepo(db)
|
|
// Get the created tool to find its slug
|
|
svc := NewToolExecutionService(repo, nil)
|
|
_, err := svc.RunToolCallLoop(context.Background(), 1, nil, "", 0.7, 100, 5)
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestToolExecutionService_RunToolCallLoop_EmptyResponse(t *testing.T) {
|
|
db := newCaptainCustomToolServiceDB(t)
|
|
repo := repository.NewCaptainCustomToolRepo(db)
|
|
provider := &mockLLMProvider3{
|
|
chatResp: &llm.ChatResponse{Choices: []llm.ChatChoice{}},
|
|
}
|
|
// Get the created tool to find its slug
|
|
svc := NewToolExecutionService(repo, provider)
|
|
_, err := svc.RunToolCallLoop(context.Background(), 1, []llm.ChatMessage{
|
|
{Role: "user", Content: "hello"},
|
|
}, "", 0.7, 100, 5)
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestToolExecutionService_RunToolCallLoop_LLMError(t *testing.T) {
|
|
db := newCaptainCustomToolServiceDB(t)
|
|
repo := repository.NewCaptainCustomToolRepo(db)
|
|
provider := &mockLLMProvider3{chatErr: fmt.Errorf("LLM error")}
|
|
// Get the created tool to find its slug
|
|
svc := NewToolExecutionService(repo, provider)
|
|
_, err := svc.RunToolCallLoop(context.Background(), 1, []llm.ChatMessage{
|
|
{Role: "user", Content: "hello"},
|
|
}, "", 0.7, 100, 5)
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestToolExecutionService_RunToolCallLoop_MaxIterations(t *testing.T) {
|
|
db := newCaptainCustomToolServiceDB(t)
|
|
repo := repository.NewCaptainCustomToolRepo(db)
|
|
// LLM always returns tool_calls, never a final answer
|
|
provider := &mockLLMProvider3{
|
|
chatResp: &llm.ChatResponse{
|
|
Choices: []llm.ChatChoice{
|
|
{
|
|
Message: llm.ChatMessage{
|
|
Role: "assistant",
|
|
ToolCalls: []llm.ToolCall{{ID: "tc1", Type: "function", Function: llm.ToolCallFunction{Name: "nonexistent", Arguments: "{}"}}},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
// Get the created tool to find its slug
|
|
svc := NewToolExecutionService(repo, provider)
|
|
_, err := svc.RunToolCallLoop(context.Background(), 1, []llm.ChatMessage{
|
|
{Role: "user", Content: "hello"},
|
|
}, "", 0.7, 100, 2)
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "exceeded")
|
|
}
|
|
|
|
func TestCustomToolToDefinition(t *testing.T) {
|
|
tool := model.CaptainCustomTool{
|
|
Slug: "my_tool",
|
|
Description: "A tool",
|
|
ParamSchema: json.RawMessage(`{"type":"object","properties":{"x":{"type":"string"}}}`),
|
|
}
|
|
def := customToolToDefinition(tool)
|
|
assert.Equal(t, "function", def.Type)
|
|
assert.Equal(t, "my_tool", def.Function.Name)
|
|
assert.Equal(t, "A tool", def.Function.Description)
|
|
assert.NotNil(t, def.Function.Parameters)
|
|
}
|
|
|
|
func TestCustomToolToDefinition_NoSchema(t *testing.T) {
|
|
tool := model.CaptainCustomTool{
|
|
Slug: "my_tool",
|
|
}
|
|
def := customToolToDefinition(tool)
|
|
assert.NotNil(t, def.Function.Parameters)
|
|
assert.Equal(t, "object", def.Function.Parameters["type"])
|
|
}
|
|
|
|
func TestApplyToolAuth(t *testing.T) {
|
|
// Bearer
|
|
req := httptest.NewRequest("GET", "https://example.com", nil)
|
|
tool := &model.CaptainCustomTool{
|
|
AuthType: model.ToolAuthTypeBearer,
|
|
AuthConfig: json.RawMessage(`{"token":"tok"}`),
|
|
}
|
|
applyToolAuth(req, tool)
|
|
assert.Equal(t, "Bearer tok", req.Header.Get("Authorization"))
|
|
|
|
// Basic
|
|
req2 := httptest.NewRequest("GET", "https://example.com", nil)
|
|
tool2 := &model.CaptainCustomTool{
|
|
AuthType: model.ToolAuthTypeBasic,
|
|
AuthConfig: json.RawMessage(`{"username":"u","password":"p"}`),
|
|
}
|
|
applyToolAuth(req2, tool2)
|
|
assert.NotEmpty(t, req2.Header.Get("Authorization"))
|
|
|
|
// ApiKey
|
|
req3 := httptest.NewRequest("GET", "https://example.com", nil)
|
|
tool3 := &model.CaptainCustomTool{
|
|
AuthType: model.ToolAuthTypeApiKey,
|
|
AuthConfig: json.RawMessage(`{"header":"X-Key","key":"val"}`),
|
|
}
|
|
applyToolAuth(req3, tool3)
|
|
assert.Equal(t, "val", req3.Header.Get("X-Key"))
|
|
|
|
// None - no-op
|
|
req4 := httptest.NewRequest("GET", "https://example.com", nil)
|
|
tool4 := &model.CaptainCustomTool{AuthType: model.ToolAuthTypeNone}
|
|
applyToolAuth(req4, tool4)
|
|
}
|
|
|
|
func TestEscapeJSONString(t *testing.T) {
|
|
result := escapeJSONString(`hello "world" \n test`)
|
|
assert.Contains(t, result, `\"`)
|
|
assert.Contains(t, result, `\\`)
|
|
assert.Contains(t, result, `\n`)
|
|
}
|
|
|
|
// ============================================================
|
|
// CaptainDocumentCrawlBackend tests
|
|
// ============================================================
|
|
|
|
func TestCaptainDocumentCrawlBackend_Crawl_EmptyURL(t *testing.T) {
|
|
backend := NewCaptainDocumentCrawlBackend()
|
|
result, err := backend.CrawlCaptainDocument(context.Background(), &model.CaptainDocument{
|
|
ExternalLink: "",
|
|
})
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "not_found", result.ErrorCode)
|
|
}
|
|
|
|
func TestCaptainDocumentCrawlBackend_Crawl_Success(t *testing.T) {
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
_, _ = w.Write([]byte(`<html><body><a href="/page1">Page1</a><a href="https://other.com/page2">Page2</a></body></html>`))
|
|
}))
|
|
defer server.Close()
|
|
|
|
backend := NewCaptainDocumentCrawlBackend()
|
|
// Override http client to use test server's client
|
|
impl := backend.(*captainDocumentCrawlBackendImpl)
|
|
impl.httpClient = server.Client()
|
|
|
|
result, err := backend.CrawlCaptainDocument(context.Background(), &model.CaptainDocument{
|
|
ExternalLink: server.URL,
|
|
})
|
|
require.NoError(t, err)
|
|
assert.Empty(t, result.ErrorCode)
|
|
assert.NotEmpty(t, result.PageLinks)
|
|
}
|
|
|
|
func TestCaptainDocumentCrawlBackend_Crawl_HTTPError(t *testing.T) {
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
}))
|
|
defer server.Close()
|
|
|
|
backend := NewCaptainDocumentCrawlBackend()
|
|
impl := backend.(*captainDocumentCrawlBackendImpl)
|
|
impl.httpClient = server.Client()
|
|
|
|
result, err := backend.CrawlCaptainDocument(context.Background(), &model.CaptainDocument{
|
|
ExternalLink: server.URL,
|
|
})
|
|
require.Error(t, err)
|
|
assert.Equal(t, "fetch_failed", result.ErrorCode)
|
|
}
|
|
|
|
func TestCaptainDocumentCrawlBackend_Crawl_ConnectionError(t *testing.T) {
|
|
backend := NewCaptainDocumentCrawlBackend()
|
|
result, err := backend.CrawlCaptainDocument(context.Background(), &model.CaptainDocument{
|
|
ExternalLink: "http://127.0.0.1:1/nonexistent",
|
|
})
|
|
require.Error(t, err)
|
|
assert.Equal(t, "fetch_failed", result.ErrorCode)
|
|
}
|
|
|
|
func TestExtractPageLinks(t *testing.T) {
|
|
htmlStr := `<html><body>
|
|
<a href="/page1">Page1</a>
|
|
<a href="https://example.com/page2">Page2</a>
|
|
<a href="#anchor">Skip</a>
|
|
<a href="">Empty</a>
|
|
<a href="/page1">Duplicate</a>
|
|
</body></html>`
|
|
links := extractPageLinks(htmlStr, "https://example.com")
|
|
assert.NotEmpty(t, links)
|
|
// Should contain absolute URLs and deduplicate
|
|
found := false
|
|
for _, l := range links {
|
|
if l == "https://example.com/page1" {
|
|
found = true
|
|
}
|
|
}
|
|
assert.True(t, found)
|
|
}
|
|
|
|
func TestExtractPageLinks_InvalidHTML(t *testing.T) {
|
|
links := extractPageLinks("not html at all", "https://example.com")
|
|
// invalid HTML is still parsed by html.Parse, just no links found
|
|
_ = links
|
|
}
|
|
|
|
func TestResolveURL(t *testing.T) {
|
|
// absolute
|
|
assert.Equal(t, "https://example.com/page", resolveURL("https://example.com/page", "https://base.com"))
|
|
// protocol-relative
|
|
assert.Equal(t, "https://example.com/page", resolveURL("//example.com/page", "https://base.com"))
|
|
// absolute path
|
|
assert.Equal(t, "https://example.com/page", resolveURL("/page", "https://example.com/base"))
|
|
// relative path
|
|
assert.Equal(t, "https://example.com/base/sub", resolveURL("sub", "https://example.com/base"))
|
|
// empty
|
|
assert.Equal(t, "", resolveURL("", "https://example.com"))
|
|
// absolute path, no subpath in base
|
|
assert.Equal(t, "https://example.com/page", resolveURL("/page", "https://example.com"))
|
|
}
|
|
|
|
// ============================================================
|
|
// CaptainDocumentPageParserBackend tests
|
|
// ============================================================
|
|
|
|
func TestCaptainDocumentPageParserBackend_Parse_EmptyURL(t *testing.T) {
|
|
backend := NewCaptainDocumentPageParserBackend()
|
|
result, err := backend.ParseCaptainDocumentPage(context.Background(), "")
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "not_found", result.ErrorCode)
|
|
}
|
|
|
|
func TestCaptainDocumentPageParserBackend_Parse_Success(t *testing.T) {
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
_, _ = w.Write([]byte(`<html><head><title>Test Page</title></head><body><p>Hello World</p></body></html>`))
|
|
}))
|
|
defer server.Close()
|
|
|
|
backend := NewCaptainDocumentPageParserBackend()
|
|
impl := backend.(*captainDocumentPageParserBackendImpl)
|
|
impl.httpClient = server.Client()
|
|
|
|
result, err := backend.ParseCaptainDocumentPage(context.Background(), server.URL)
|
|
require.NoError(t, err)
|
|
assert.NotEmpty(t, result.Title) // title is the URL since <head> is skipped
|
|
assert.Contains(t, result.Content, "Hello World")
|
|
}
|
|
|
|
func TestCaptainDocumentPageParserBackend_Parse_EmptyContent(t *testing.T) {
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
_, _ = w.Write([]byte(`<html><head><script>var x = 1;</script></head><body></body></html>`))
|
|
}))
|
|
defer server.Close()
|
|
|
|
backend := NewCaptainDocumentPageParserBackend()
|
|
impl := backend.(*captainDocumentPageParserBackendImpl)
|
|
impl.httpClient = server.Client()
|
|
|
|
result, err := backend.ParseCaptainDocumentPage(context.Background(), server.URL)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "content_empty", result.ErrorCode)
|
|
}
|
|
|
|
func TestCaptainDocumentPageParserBackend_Parse_HTTPError(t *testing.T) {
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusNotFound)
|
|
}))
|
|
defer server.Close()
|
|
|
|
backend := NewCaptainDocumentPageParserBackend()
|
|
impl := backend.(*captainDocumentPageParserBackendImpl)
|
|
impl.httpClient = server.Client()
|
|
|
|
result, err := backend.ParseCaptainDocumentPage(context.Background(), server.URL)
|
|
require.Error(t, err)
|
|
assert.Equal(t, "fetch_failed", result.ErrorCode)
|
|
}
|
|
|
|
func TestCaptainDocumentPageParserBackend_Parse_NoTitle(t *testing.T) {
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
_, _ = w.Write([]byte(`<html><body><p>Content without title</p></body></html>`))
|
|
}))
|
|
defer server.Close()
|
|
|
|
backend := NewCaptainDocumentPageParserBackend()
|
|
impl := backend.(*captainDocumentPageParserBackendImpl)
|
|
impl.httpClient = server.Client()
|
|
|
|
result, err := backend.ParseCaptainDocumentPage(context.Background(), server.URL)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, server.URL, result.Title)
|
|
assert.Contains(t, result.Content, "Content without title")
|
|
}
|
|
|
|
// ============================================================
|
|
// CaptainDocumentSyncBackend tests
|
|
// ============================================================
|
|
|
|
func TestCaptainDocumentSyncBackend_Sync_EmptyDoc(t *testing.T) {
|
|
backend := NewCaptainDocumentSyncBackend()
|
|
result, err := backend.SyncCaptainDocument(context.Background(), &model.CaptainDocument{})
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "content_empty", result.ErrorCode)
|
|
}
|
|
|
|
func TestCaptainDocumentSyncBackend_Sync_WebSuccess(t *testing.T) {
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
_, _ = w.Write([]byte(`<html><head><title>Doc Page</title></head><body><p>Document content here</p></body></html>`))
|
|
}))
|
|
defer server.Close()
|
|
|
|
backend := NewCaptainDocumentSyncBackend()
|
|
impl := backend.(*captainDocumentSyncBackendImpl)
|
|
impl.httpClient = server.Client()
|
|
|
|
result, err := backend.SyncCaptainDocument(context.Background(), &model.CaptainDocument{
|
|
ExternalLink: server.URL,
|
|
Name: "Web Doc",
|
|
})
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "Web Doc", result.Title) // title is doc.Name since <head> is skipped
|
|
assert.Contains(t, result.Content, "Document content here")
|
|
}
|
|
|
|
func TestCaptainDocumentSyncBackend_Sync_WebEmptyContent(t *testing.T) {
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
_, _ = w.Write([]byte(`<html><body><script>var x=1;</script></body></html>`))
|
|
}))
|
|
defer server.Close()
|
|
|
|
backend := NewCaptainDocumentSyncBackend()
|
|
impl := backend.(*captainDocumentSyncBackendImpl)
|
|
impl.httpClient = server.Client()
|
|
|
|
result, err := backend.SyncCaptainDocument(context.Background(), &model.CaptainDocument{
|
|
ExternalLink: server.URL,
|
|
Name: "Empty Doc",
|
|
})
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "content_empty", result.ErrorCode)
|
|
}
|
|
|
|
func TestCaptainDocumentSyncBackend_Sync_WebHTTPError(t *testing.T) {
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
}))
|
|
defer server.Close()
|
|
|
|
backend := NewCaptainDocumentSyncBackend()
|
|
impl := backend.(*captainDocumentSyncBackendImpl)
|
|
impl.httpClient = server.Client()
|
|
|
|
result, err := backend.SyncCaptainDocument(context.Background(), &model.CaptainDocument{
|
|
ExternalLink: server.URL,
|
|
})
|
|
require.Error(t, err)
|
|
assert.Equal(t, "fetch_failed", result.ErrorCode)
|
|
}
|
|
|
|
func TestCaptainDocumentSyncBackend_Sync_PDFNotFound(t *testing.T) {
|
|
backend := NewCaptainDocumentSyncBackend()
|
|
result, err := backend.SyncCaptainDocument(context.Background(), &model.CaptainDocument{
|
|
ContentType: "application/pdf",
|
|
FileURL: "",
|
|
})
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "not_found", result.ErrorCode)
|
|
}
|
|
|
|
func TestCaptainDocumentSyncBackend_Sync_PDFNonExistentFile(t *testing.T) {
|
|
backend := NewCaptainDocumentSyncBackend()
|
|
result, err := backend.SyncCaptainDocument(context.Background(), &model.CaptainDocument{
|
|
ContentType: "application/pdf",
|
|
FileURL: "/nonexistent/path/file.pdf",
|
|
})
|
|
// resolvePDFPath returns "" for non-existent files
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "not_found", result.ErrorCode)
|
|
}
|
|
|
|
func TestCaptainDocumentSyncBackend_ResolvePDFPath(t *testing.T) {
|
|
backend := NewCaptainDocumentSyncBackend()
|
|
impl := backend.(*captainDocumentSyncBackendImpl)
|
|
|
|
// empty
|
|
assert.Equal(t, "", impl.resolvePDFPath(&model.CaptainDocument{FileURL: ""}))
|
|
|
|
// /uploads/ path
|
|
assert.Equal(t, "./uploads/captain_docs/1/file.pdf",
|
|
impl.resolvePDFPath(&model.CaptainDocument{FileURL: "/uploads/captain_docs/1/file.pdf"}))
|
|
}
|
|
|
|
func TestExtractHTMLText(t *testing.T) {
|
|
_, content := extractHTMLText(`<html><head><title>My Title</title><script>var x=1;</script></head><body><p>Hello</p><p>World</p></body></html>`)
|
|
// title is inside <head> which is skipped by the extractor
|
|
assert.Contains(t, content, "Hello")
|
|
assert.Contains(t, content, "World")
|
|
assert.NotContains(t, content, "var x=1")
|
|
}
|
|
|
|
func TestExtractHTMLText_NoTitle(t *testing.T) {
|
|
_, content := extractHTMLText(`<html><body><p>Just content</p></body></html>`)
|
|
assert.Contains(t, content, "Just content")
|
|
}
|
|
|
|
func TestExtractHTMLText_InvalidHTML(t *testing.T) {
|
|
title, content := extractHTMLText("just plain text")
|
|
assert.Equal(t, "", title)
|
|
assert.Contains(t, content, "just plain text")
|
|
}
|
|
|
|
func TestExtractHTMLText_EmptyString(t *testing.T) {
|
|
title, content := extractHTMLText("")
|
|
assert.Equal(t, "", title)
|
|
assert.Equal(t, "", content)
|
|
}
|
|
|
|
// ============================================================
|
|
// FeatureFlagStringEnabled tests
|
|
// ============================================================
|
|
|
|
func TestFeatureFlagStringEnabled(t *testing.T) {
|
|
// JSON object format
|
|
assert.True(t, featureFlagStringEnabled(`{"custom_tools":true}`, "custom_tools"))
|
|
assert.False(t, featureFlagStringEnabled(`{"custom_tools":false}`, "custom_tools"))
|
|
|
|
// JSON array format
|
|
assert.True(t, featureFlagStringEnabled(`["custom_tools","other"]`, "custom_tools"))
|
|
assert.False(t, featureFlagStringEnabled(`["other"]`, "custom_tools"))
|
|
|
|
// Comma-separated format
|
|
assert.True(t, featureFlagStringEnabled("custom_tools,other", "custom_tools"))
|
|
assert.False(t, featureFlagStringEnabled("other", "custom_tools"))
|
|
|
|
// empty
|
|
assert.False(t, featureFlagStringEnabled("", "custom_tools"))
|
|
assert.False(t, featureFlagStringEnabled(" ", "custom_tools"))
|
|
}
|