Reorganize repo: backend/, deploy/, docs/ layout + AGENTS.md
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.
This commit is contained in:
@@ -0,0 +1,555 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user