Restructure the monorepo into clear top-level directories: - backend/: Go module root (cmd, internal, pkg, configs, migrations, docs/swagger, scripts, tests, go.mod, Makefile, .air.toml) - deploy/: Docker (Dockerfile, docker-compose*), quickstart, fluentd - docs/: project documentation + reports/ (moved from repo root) - AGENTS.md: new AI coding-agent guide at repo root Update all references to the new layout: - Dockerfile: COPY backend/go.mod, COPY backend/ (context = repo root) - docker-compose files: context ../.., dockerfile deploy/docker/Dockerfile, env_file ../../.env, volume mounts ../../backend:/app - deploy/quickstart/compose.yaml: dockerfile deploy/docker/Dockerfile - CI: working-directory: backend for go commands, file deploy/docker/Dockerfile, coverage path backend/coverage.out, health_check backend/scripts/ - backend/Makefile: docker target uses -f ../deploy/docker/Dockerfile ../ - README: architecture tree, quickstart, config paths updated Move root stray scripts (rename_models.*, run_m11_tests.sh, verify_build.sh, gorm_bool_main.go) to backend/scripts/legacy/. All moves via git mv to preserve history. Build, vet, SQLite tests, and docker compose config verified.
556 lines
21 KiB
Go
556 lines
21 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"gorm.io/driver/sqlite"
|
|
"gorm.io/gorm"
|
|
"gorm.io/gorm/logger"
|
|
|
|
"github.com/gochat/gochat/internal/channel"
|
|
"github.com/gochat/gochat/internal/model"
|
|
"github.com/gochat/gochat/internal/repository"
|
|
)
|
|
|
|
// setupAssignmentTestDB creates a SQLite in-memory DB with all models needed
|
|
// for assignment tests: Account, User, AccountUser, Inbox, InboxMember,
|
|
// Contact, Conversation, Team, TeamMember.
|
|
func setupAssignmentTestDB(t *testing.T) *gorm.DB {
|
|
t.Helper()
|
|
db, err := gorm.Open(sqlite.Open("file::memory:"), &gorm.Config{
|
|
Logger: logger.Default.LogMode(logger.Silent),
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
models := []interface{}{
|
|
&model.Account{},
|
|
&model.User{},
|
|
&model.AccountUser{},
|
|
&model.Inbox{},
|
|
&model.InboxMember{},
|
|
&model.Contact{},
|
|
&model.Conversation{},
|
|
&model.Team{},
|
|
&model.TeamMember{},
|
|
&model.AgentCapacityPolicy{},
|
|
&model.InboxCapacityLimit{},
|
|
}
|
|
require.NoError(t, db.AutoMigrate(models...))
|
|
return db
|
|
}
|
|
|
|
// setupAssignmentService creates a ConversationService with all repos needed
|
|
// for deep assignment logic testing.
|
|
func setupAssignmentService(t *testing.T) (*ConversationService, *gorm.DB) {
|
|
t.Helper()
|
|
db := setupAssignmentTestDB(t)
|
|
convRepo := repository.NewConversationRepo(db)
|
|
msgRepo := repository.NewMessageRepo(db)
|
|
dispatcher := channel.NewDispatcher()
|
|
inboxMemberRepo := repository.NewInboxMemberRepo(db)
|
|
inboxMemberSvc := NewInboxMemberService(inboxMemberRepo)
|
|
accountUserRepo := repository.NewAccountUserRepo(db)
|
|
teamRepo := repository.NewTeamRepo(db)
|
|
teamMemberRepo := repository.NewTeamMemberRepo(db)
|
|
svc := NewConversationService(convRepo, msgRepo, dispatcher, inboxMemberSvc, accountUserRepo, teamRepo, teamMemberRepo)
|
|
return svc, db
|
|
}
|
|
|
|
// ========== Helper functions for creating test data ==========
|
|
|
|
func createAssignmentAccount(t *testing.T, db *gorm.DB) *model.Account {
|
|
t.Helper()
|
|
acc := &model.Account{Name: "Test Account"}
|
|
require.NoError(t, db.Create(acc).Error)
|
|
return acc
|
|
}
|
|
|
|
func createAssignmentUser(t *testing.T, db *gorm.DB, name, email string) *model.User {
|
|
t.Helper()
|
|
u := &model.User{Name: name, Email: email, AccountID: 1, Password: "hashed"}
|
|
require.NoError(t, db.Create(u).Error)
|
|
return u
|
|
}
|
|
|
|
func createAssignmentAccountUser(t *testing.T, db *gorm.DB, accountID, userID uint, role, availability string) *model.AccountUser {
|
|
t.Helper()
|
|
au := &model.AccountUser{AccountID: accountID, UserID: userID, Role: role, Availability: availability}
|
|
require.NoError(t, db.Create(au).Error)
|
|
return au
|
|
}
|
|
|
|
func createAssignmentInbox(t *testing.T, db *gorm.DB, accountID uint) *model.Inbox {
|
|
t.Helper()
|
|
inbox := &model.Inbox{AccountID: accountID, Name: "Test Inbox", ChannelType: string(model.InboxChannelTypeWebWidget)}
|
|
require.NoError(t, db.Create(inbox).Error)
|
|
return inbox
|
|
}
|
|
|
|
func createAssignmentInboxMember(t *testing.T, db *gorm.DB, inboxID, userID uint) {
|
|
t.Helper()
|
|
im := &model.InboxMember{InboxID: inboxID, UserID: userID}
|
|
require.NoError(t, db.Create(im).Error)
|
|
}
|
|
|
|
func createAssignmentContact(t *testing.T, db *gorm.DB, accountID uint) *model.Contact {
|
|
t.Helper()
|
|
c := &model.Contact{AccountID: accountID, Name: "Test Contact"}
|
|
require.NoError(t, db.Create(c).Error)
|
|
return c
|
|
}
|
|
|
|
func createAssignmentConversation(t *testing.T, db *gorm.DB, accountID, inboxID, contactID uint) *model.Conversation {
|
|
t.Helper()
|
|
conv := &model.Conversation{
|
|
AccountID: accountID,
|
|
InboxID: inboxID,
|
|
ContactID: contactID,
|
|
Status: "open",
|
|
ChannelType: "web_widget",
|
|
Channel: "web_widget",
|
|
}
|
|
require.NoError(t, db.Create(conv).Error)
|
|
return conv
|
|
}
|
|
|
|
func createAssignmentCapacityLimit(t *testing.T, db *gorm.DB, accountID, inboxID, userID uint, conversationLimit int) *model.AgentCapacityPolicy {
|
|
t.Helper()
|
|
policy := &model.AgentCapacityPolicy{AccountID: accountID, Name: "Capacity", AssignmentLogic: "round_robin", ExclusionRules: []byte(`{}`)}
|
|
require.NoError(t, db.Create(policy).Error)
|
|
require.NoError(t, db.Model(&model.AccountUser{}).
|
|
Where("account_id = ? AND user_id = ?", accountID, userID).
|
|
Update("agent_capacity_policy_id", policy.ID).Error)
|
|
require.NoError(t, db.Create(&model.InboxCapacityLimit{
|
|
AgentCapacityPolicyID: policy.ID,
|
|
InboxID: inboxID,
|
|
ConversationLimit: conversationLimit,
|
|
}).Error)
|
|
return policy
|
|
}
|
|
|
|
func createAssignmentTeam(t *testing.T, db *gorm.DB, accountID uint, name string, allowAutoAssignment bool) *model.Team {
|
|
t.Helper()
|
|
team := &model.Team{AccountID: accountID, Name: name, AllowAutoAssignment: true} // SQLite default:true workaround — create as true first
|
|
require.NoError(t, db.Create(team).Error)
|
|
if !allowAutoAssignment {
|
|
require.NoError(t, db.Model(team).Update("allow_auto_assignment", false).Error)
|
|
team.AllowAutoAssignment = false
|
|
}
|
|
return team
|
|
}
|
|
|
|
func createAssignmentTeamMember(t *testing.T, db *gorm.DB, teamID, userID uint, availability string) *model.TeamMember {
|
|
t.Helper()
|
|
tm := &model.TeamMember{TeamID: teamID, UserID: userID, AvailabilityStatus: availability}
|
|
require.NoError(t, db.Create(tm).Error)
|
|
return tm
|
|
}
|
|
|
|
// ========== AssignAgent Authorization Tests ==========
|
|
|
|
func TestAssignAgent_Authorization_RejectsNonAccountMember(t *testing.T) {
|
|
svc, db := setupAssignmentService(t)
|
|
ctx := context.Background()
|
|
|
|
account := createAssignmentAccount(t, db)
|
|
inbox := createAssignmentInbox(t, db, account.ID)
|
|
contact := createAssignmentContact(t, db, account.ID)
|
|
conv := createAssignmentConversation(t, db, account.ID, inbox.ID, contact.ID)
|
|
|
|
// Create a user who is NOT an agent/admin in this account
|
|
outsider := createAssignmentUser(t, db, "Outsider", "outsider@test.com")
|
|
|
|
_, err := svc.AssignAgent(ctx, account.ID, conv.ID, outsider.ID)
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "not an agent or administrator")
|
|
}
|
|
|
|
func TestAssignAgent_Authorization_AcceptsAgent(t *testing.T) {
|
|
svc, db := setupAssignmentService(t)
|
|
ctx := context.Background()
|
|
|
|
account := createAssignmentAccount(t, db)
|
|
inbox := createAssignmentInbox(t, db, account.ID)
|
|
contact := createAssignmentContact(t, db, account.ID)
|
|
conv := createAssignmentConversation(t, db, account.ID, inbox.ID, contact.ID)
|
|
|
|
// Create an agent who IS a member of this account AND inbox
|
|
agent := createAssignmentUser(t, db, "Agent1", "agent1@test.com")
|
|
createAssignmentAccountUser(t, db, account.ID, agent.ID, "agent", "online")
|
|
createAssignmentInboxMember(t, db, inbox.ID, agent.ID)
|
|
|
|
result, err := svc.AssignAgent(ctx, account.ID, conv.ID, agent.ID)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, agent.ID, *result.AssigneeID)
|
|
}
|
|
|
|
func TestAssignAgent_Authorization_AcceptsAdministrator(t *testing.T) {
|
|
svc, db := setupAssignmentService(t)
|
|
ctx := context.Background()
|
|
|
|
account := createAssignmentAccount(t, db)
|
|
inbox := createAssignmentInbox(t, db, account.ID)
|
|
contact := createAssignmentContact(t, db, account.ID)
|
|
conv := createAssignmentConversation(t, db, account.ID, inbox.ID, contact.ID)
|
|
|
|
// Create an admin who IS a member of this account AND inbox
|
|
admin := createAssignmentUser(t, db, "Admin1", "admin1@test.com")
|
|
createAssignmentAccountUser(t, db, account.ID, admin.ID, "administrator", "online")
|
|
createAssignmentInboxMember(t, db, inbox.ID, admin.ID)
|
|
|
|
result, err := svc.AssignAgent(ctx, account.ID, conv.ID, admin.ID)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, admin.ID, *result.AssigneeID)
|
|
}
|
|
|
|
func TestAssignAgent_RejectsInboxNonMember(t *testing.T) {
|
|
svc, db := setupAssignmentService(t)
|
|
ctx := context.Background()
|
|
|
|
account := createAssignmentAccount(t, db)
|
|
inbox := createAssignmentInbox(t, db, account.ID)
|
|
contact := createAssignmentContact(t, db, account.ID)
|
|
conv := createAssignmentConversation(t, db, account.ID, inbox.ID, contact.ID)
|
|
|
|
// Create an agent in the account but NOT in the inbox
|
|
agent := createAssignmentUser(t, db, "NoInboxAgent", "noinbox@test.com")
|
|
createAssignmentAccountUser(t, db, account.ID, agent.ID, "agent", "online")
|
|
// Do NOT add to inbox members
|
|
|
|
_, err := svc.AssignAgent(ctx, account.ID, conv.ID, agent.ID)
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "not a member of the conversation's inbox")
|
|
}
|
|
|
|
func TestAssignAgent_RejectsAgentAtInboxCapacity(t *testing.T) {
|
|
svc, db := setupAssignmentService(t)
|
|
ctx := context.Background()
|
|
|
|
account := createAssignmentAccount(t, db)
|
|
inbox := createAssignmentInbox(t, db, account.ID)
|
|
contact := createAssignmentContact(t, db, account.ID)
|
|
existing := createAssignmentConversation(t, db, account.ID, inbox.ID, contact.ID)
|
|
conv := createAssignmentConversation(t, db, account.ID, inbox.ID, contact.ID)
|
|
|
|
agent := createAssignmentUser(t, db, "CapacityAgent", "capacity@test.com")
|
|
createAssignmentAccountUser(t, db, account.ID, agent.ID, "agent", "online")
|
|
createAssignmentInboxMember(t, db, inbox.ID, agent.ID)
|
|
createAssignmentCapacityLimit(t, db, account.ID, inbox.ID, agent.ID, 1)
|
|
require.NoError(t, db.Model(existing).Update("assignee_id", agent.ID).Error)
|
|
|
|
_, err := svc.AssignAgent(ctx, account.ID, conv.ID, agent.ID)
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "reached capacity")
|
|
}
|
|
|
|
func TestAssignAgent_IgnoresResolvedConversationsForCapacity(t *testing.T) {
|
|
svc, db := setupAssignmentService(t)
|
|
ctx := context.Background()
|
|
|
|
account := createAssignmentAccount(t, db)
|
|
inbox := createAssignmentInbox(t, db, account.ID)
|
|
contact := createAssignmentContact(t, db, account.ID)
|
|
resolved := createAssignmentConversation(t, db, account.ID, inbox.ID, contact.ID)
|
|
conv := createAssignmentConversation(t, db, account.ID, inbox.ID, contact.ID)
|
|
|
|
agent := createAssignmentUser(t, db, "CapacityAgent2", "capacity2@test.com")
|
|
createAssignmentAccountUser(t, db, account.ID, agent.ID, "agent", "online")
|
|
createAssignmentInboxMember(t, db, inbox.ID, agent.ID)
|
|
createAssignmentCapacityLimit(t, db, account.ID, inbox.ID, agent.ID, 1)
|
|
require.NoError(t, db.Model(resolved).Updates(map[string]any{"assignee_id": agent.ID, "status": model.ConversationStatusResolved}).Error)
|
|
|
|
result, err := svc.AssignAgent(ctx, account.ID, conv.ID, agent.ID)
|
|
assert.NoError(t, err)
|
|
require.NotNil(t, result.AssigneeID)
|
|
assert.Equal(t, agent.ID, *result.AssigneeID)
|
|
}
|
|
|
|
func TestAssignAgent_Unassign_SetsNilAssigneeID(t *testing.T) {
|
|
svc, db := setupAssignmentService(t)
|
|
ctx := context.Background()
|
|
|
|
account := createAssignmentAccount(t, db)
|
|
inbox := createAssignmentInbox(t, db, account.ID)
|
|
contact := createAssignmentContact(t, db, account.ID)
|
|
conv := createAssignmentConversation(t, db, account.ID, inbox.ID, contact.ID)
|
|
|
|
// First assign an agent
|
|
agent := createAssignmentUser(t, db, "Agent1", "agent1@test.com")
|
|
createAssignmentAccountUser(t, db, account.ID, agent.ID, "agent", "online")
|
|
createAssignmentInboxMember(t, db, inbox.ID, agent.ID)
|
|
|
|
result, err := svc.AssignAgent(ctx, account.ID, conv.ID, agent.ID)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, result.AssigneeID)
|
|
|
|
// Then unassign (assigneeID = 0)
|
|
result, err = svc.AssignAgent(ctx, account.ID, conv.ID, 0)
|
|
assert.NoError(t, err)
|
|
assert.Nil(t, result.AssigneeID)
|
|
}
|
|
|
|
// ========== AssignTeam Validation Tests ==========
|
|
|
|
func TestAssignTeam_RejectsTeamNotInAccount(t *testing.T) {
|
|
svc, db := setupAssignmentService(t)
|
|
ctx := context.Background()
|
|
|
|
// Create two accounts
|
|
account1 := createAssignmentAccount(t, db)
|
|
account2 := createAssignmentAccount(t, db)
|
|
|
|
inbox := createAssignmentInbox(t, db, account1.ID)
|
|
contact := createAssignmentContact(t, db, account1.ID)
|
|
conv := createAssignmentConversation(t, db, account1.ID, inbox.ID, contact.ID)
|
|
|
|
// Create a team in account2 (wrong account!)
|
|
team2 := createAssignmentTeam(t, db, account2.ID, "Wrong Team", true)
|
|
|
|
_, err := svc.AssignTeam(ctx, account1.ID, conv.ID, nil, &team2.ID)
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "team not found in this account")
|
|
}
|
|
|
|
func TestAssignTeam_RejectsTeamWithoutAutoAssignment(t *testing.T) {
|
|
svc, db := setupAssignmentService(t)
|
|
ctx := context.Background()
|
|
|
|
account := createAssignmentAccount(t, db)
|
|
inbox := createAssignmentInbox(t, db, account.ID)
|
|
contact := createAssignmentContact(t, db, account.ID)
|
|
conv := createAssignmentConversation(t, db, account.ID, inbox.ID, contact.ID)
|
|
|
|
// Create a team with AllowAutoAssignment = false
|
|
team := createAssignmentTeam(t, db, account.ID, "NoAuto Team", false)
|
|
|
|
// Try to assign team without specifying an agent — should be rejected
|
|
_, err := svc.AssignTeam(ctx, account.ID, conv.ID, nil, &team.ID)
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "does not allow auto-assignment")
|
|
}
|
|
|
|
func TestAssignTeam_AllowsTeamWithAutoAssignment(t *testing.T) {
|
|
svc, db := setupAssignmentService(t)
|
|
ctx := context.Background()
|
|
|
|
account := createAssignmentAccount(t, db)
|
|
inbox := createAssignmentInbox(t, db, account.ID)
|
|
contact := createAssignmentContact(t, db, account.ID)
|
|
conv := createAssignmentConversation(t, db, account.ID, inbox.ID, contact.ID)
|
|
|
|
// Create a team with AllowAutoAssignment = true
|
|
team := createAssignmentTeam(t, db, account.ID, "Auto Team", true)
|
|
|
|
result, err := svc.AssignTeam(ctx, account.ID, conv.ID, nil, &team.ID)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, team.ID, *result.TeamID)
|
|
}
|
|
|
|
func TestAssignTeam_WithAgentAndTeam(t *testing.T) {
|
|
svc, db := setupAssignmentService(t)
|
|
ctx := context.Background()
|
|
|
|
account := createAssignmentAccount(t, db)
|
|
inbox := createAssignmentInbox(t, db, account.ID)
|
|
contact := createAssignmentContact(t, db, account.ID)
|
|
conv := createAssignmentConversation(t, db, account.ID, inbox.ID, contact.ID)
|
|
|
|
team := createAssignmentTeam(t, db, account.ID, "Team1", true)
|
|
agent := createAssignmentUser(t, db, "Agent1", "agent1@test.com")
|
|
createAssignmentAccountUser(t, db, account.ID, agent.ID, "agent", "online")
|
|
createAssignmentInboxMember(t, db, inbox.ID, agent.ID)
|
|
|
|
result, err := svc.AssignTeam(ctx, account.ID, conv.ID, &agent.ID, &team.ID)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, team.ID, *result.TeamID)
|
|
assert.Equal(t, agent.ID, *result.AssigneeID)
|
|
}
|
|
|
|
func TestAssignTeam_RejectsAgentNotInAccount(t *testing.T) {
|
|
svc, db := setupAssignmentService(t)
|
|
ctx := context.Background()
|
|
|
|
account := createAssignmentAccount(t, db)
|
|
inbox := createAssignmentInbox(t, db, account.ID)
|
|
contact := createAssignmentContact(t, db, account.ID)
|
|
conv := createAssignmentConversation(t, db, account.ID, inbox.ID, contact.ID)
|
|
|
|
team := createAssignmentTeam(t, db, account.ID, "Team1", true)
|
|
outsideAgent := createAssignmentUser(t, db, "Outside", "outside@test.com")
|
|
// NOT adding AccountUser for this agent
|
|
|
|
_, err := svc.AssignTeam(ctx, account.ID, conv.ID, &outsideAgent.ID, &team.ID)
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "not an agent or administrator")
|
|
}
|
|
|
|
func TestAssignTeam_Overflow_FallbackToAccountOnlineAgent(t *testing.T) {
|
|
svc, db := setupAssignmentService(t)
|
|
ctx := context.Background()
|
|
|
|
account := createAssignmentAccount(t, db)
|
|
inbox := createAssignmentInbox(t, db, account.ID)
|
|
contact := createAssignmentContact(t, db, account.ID)
|
|
conv := createAssignmentConversation(t, db, account.ID, inbox.ID, contact.ID)
|
|
|
|
// Create a team
|
|
team := createAssignmentTeam(t, db, account.ID, "Overflow Team", true)
|
|
|
|
// Add 2 team members — both OFFLINE
|
|
member1 := createAssignmentUser(t, db, "Member1", "member1@test.com")
|
|
member2 := createAssignmentUser(t, db, "Member2", "member2@test.com")
|
|
createAssignmentAccountUser(t, db, account.ID, member1.ID, "agent", "offline")
|
|
createAssignmentAccountUser(t, db, account.ID, member2.ID, "agent", "offline")
|
|
createAssignmentTeamMember(t, db, team.ID, member1.ID, "offline")
|
|
createAssignmentTeamMember(t, db, team.ID, member2.ID, "offline")
|
|
|
|
// Add an account-level online agent (NOT in the team) for overflow fallback
|
|
fallbackAgent := createAssignmentUser(t, db, "Fallback", "fallback@test.com")
|
|
createAssignmentAccountUser(t, db, account.ID, fallbackAgent.ID, "agent", "online")
|
|
createAssignmentInboxMember(t, db, inbox.ID, fallbackAgent.ID)
|
|
|
|
// Assign team without specifying agent — should overflow to fallback
|
|
result, err := svc.AssignTeam(ctx, account.ID, conv.ID, nil, &team.ID)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, team.ID, *result.TeamID)
|
|
// Overflow should assign the fallback agent
|
|
assert.NotNil(t, result.AssigneeID)
|
|
assert.Equal(t, fallbackAgent.ID, *result.AssigneeID)
|
|
}
|
|
|
|
func TestAssignTeam_Overflow_NoOnlineAgents_NoFallback(t *testing.T) {
|
|
svc, db := setupAssignmentService(t)
|
|
ctx := context.Background()
|
|
|
|
account := createAssignmentAccount(t, db)
|
|
inbox := createAssignmentInbox(t, db, account.ID)
|
|
contact := createAssignmentContact(t, db, account.ID)
|
|
conv := createAssignmentConversation(t, db, account.ID, inbox.ID, contact.ID)
|
|
|
|
// Create a team
|
|
team := createAssignmentTeam(t, db, account.ID, "NoFallback Team", true)
|
|
|
|
// Add team members — all OFFLINE
|
|
member1 := createAssignmentUser(t, db, "Member1", "member1@test.com")
|
|
createAssignmentAccountUser(t, db, account.ID, member1.ID, "agent", "offline")
|
|
createAssignmentTeamMember(t, db, team.ID, member1.ID, "offline")
|
|
|
|
// No online agents in the account at all
|
|
|
|
// Assign team without agent — overflow should NOT assign anyone
|
|
result, err := svc.AssignTeam(ctx, account.ID, conv.ID, nil, &team.ID)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, team.ID, *result.TeamID)
|
|
// No overflow agent available, so AssigneeID should remain nil
|
|
assert.Nil(t, result.AssigneeID)
|
|
}
|
|
|
|
func TestAssignTeam_OnlineTeamMember_NoOverflow(t *testing.T) {
|
|
svc, db := setupAssignmentService(t)
|
|
ctx := context.Background()
|
|
|
|
account := createAssignmentAccount(t, db)
|
|
inbox := createAssignmentInbox(t, db, account.ID)
|
|
contact := createAssignmentContact(t, db, account.ID)
|
|
conv := createAssignmentConversation(t, db, account.ID, inbox.ID, contact.ID)
|
|
|
|
// Create a team with online members
|
|
team := createAssignmentTeam(t, db, account.ID, "Online Team", true)
|
|
|
|
onlineMember := createAssignmentUser(t, db, "OnlineMember", "online@test.com")
|
|
createAssignmentAccountUser(t, db, account.ID, onlineMember.ID, "agent", "online")
|
|
createAssignmentTeamMember(t, db, team.ID, onlineMember.ID, "online")
|
|
|
|
// Assign team without agent — no overflow needed (member is online)
|
|
result, err := svc.AssignTeam(ctx, account.ID, conv.ID, nil, &team.ID)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, team.ID, *result.TeamID)
|
|
// No overflow triggered since team has online members
|
|
assert.Nil(t, result.AssigneeID)
|
|
}
|
|
|
|
// ========== AccountUserRepo Tests ==========
|
|
|
|
func TestAccountUserRepo_IsAgentOrAdmin(t *testing.T) {
|
|
db := setupAssignmentTestDB(t)
|
|
repo := repository.NewAccountUserRepo(db)
|
|
ctx := context.Background()
|
|
|
|
account := createAssignmentAccount(t, db)
|
|
user := createAssignmentUser(t, db, "TestUser", "test@test.com")
|
|
|
|
// User not in account yet
|
|
isMember, err := repo.IsAgentOrAdmin(ctx, account.ID, user.ID)
|
|
assert.NoError(t, err)
|
|
assert.False(t, isMember)
|
|
|
|
// Add as agent
|
|
createAssignmentAccountUser(t, db, account.ID, user.ID, "agent", "online")
|
|
isMember, err = repo.IsAgentOrAdmin(ctx, account.ID, user.ID)
|
|
assert.NoError(t, err)
|
|
assert.True(t, isMember)
|
|
}
|
|
|
|
func TestAccountUserRepo_IsAdministrator(t *testing.T) {
|
|
db := setupAssignmentTestDB(t)
|
|
repo := repository.NewAccountUserRepo(db)
|
|
ctx := context.Background()
|
|
|
|
account := createAssignmentAccount(t, db)
|
|
agentUser := createAssignmentUser(t, db, "Agent", "agent@test.com")
|
|
adminUser := createAssignmentUser(t, db, "Admin", "admin@test.com")
|
|
|
|
createAssignmentAccountUser(t, db, account.ID, agentUser.ID, "agent", "online")
|
|
createAssignmentAccountUser(t, db, account.ID, adminUser.ID, "administrator", "online")
|
|
|
|
isAdmin, err := repo.IsAdministrator(ctx, account.ID, agentUser.ID)
|
|
assert.NoError(t, err)
|
|
assert.False(t, isAdmin)
|
|
|
|
isAdmin, err = repo.IsAdministrator(ctx, account.ID, adminUser.ID)
|
|
assert.NoError(t, err)
|
|
assert.True(t, isAdmin)
|
|
}
|
|
|
|
func TestAccountUserRepo_FindOnlineAgentsByAccount(t *testing.T) {
|
|
db := setupAssignmentTestDB(t)
|
|
repo := repository.NewAccountUserRepo(db)
|
|
ctx := context.Background()
|
|
|
|
account := createAssignmentAccount(t, db)
|
|
onlineAgent := createAssignmentUser(t, db, "Online1", "online1@test.com")
|
|
offlineAgent := createAssignmentUser(t, db, "Offline1", "offline1@test.com")
|
|
|
|
createAssignmentAccountUser(t, db, account.ID, onlineAgent.ID, "agent", "online")
|
|
createAssignmentAccountUser(t, db, account.ID, offlineAgent.ID, "agent", "offline")
|
|
|
|
onlineAgents, err := repo.FindOnlineAgentsByAccount(ctx, account.ID)
|
|
assert.NoError(t, err)
|
|
assert.Len(t, onlineAgents, 1)
|
|
assert.Equal(t, onlineAgent.ID, onlineAgents[0].UserID)
|
|
}
|
|
|
|
func TestAccountUserRepo_FindByAccountAndUser(t *testing.T) {
|
|
db := setupAssignmentTestDB(t)
|
|
repo := repository.NewAccountUserRepo(db)
|
|
ctx := context.Background()
|
|
|
|
account := createAssignmentAccount(t, db)
|
|
user := createAssignmentUser(t, db, "TestUser", "test@test.com")
|
|
|
|
// Not in account yet
|
|
au, err := repo.FindByAccountAndUser(ctx, account.ID, user.ID)
|
|
assert.NoError(t, err)
|
|
assert.Nil(t, au)
|
|
|
|
// Add to account
|
|
createAssignmentAccountUser(t, db, account.ID, user.ID, "agent", "online")
|
|
au, err = repo.FindByAccountAndUser(ctx, account.ID, user.ID)
|
|
assert.NoError(t, err)
|
|
assert.NotNil(t, au)
|
|
assert.Equal(t, "agent", au.Role)
|
|
}
|