Files

249 lines
14 KiB
Go

package autoassignment
import (
"context"
"testing"
"time"
"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/model"
"github.com/gochat/gochat/internal/repository"
)
func setupCapacityAssignmentDB(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)
require.NoError(t, db.AutoMigrate(
&model.Account{},
&model.User{},
&model.AccountUser{},
&model.Inbox{},
&model.InboxMember{},
&model.Contact{},
&model.Conversation{},
&model.AssignmentPolicy{},
&model.InboxAssignmentPolicy{},
&model.AgentCapacityPolicy{},
&model.InboxCapacityLimit{},
&model.Tag{},
&model.ConversationLabel{},
))
return db
}
func TestAssignmentService_SkipsAgentsAtInboxCapacity(t *testing.T) {
_, rdb := setupTestRedis(t)
db := setupCapacityAssignmentDB(t)
svc := NewAssignmentService(db, rdb)
ctx := context.Background()
account := &model.Account{Name: "Capacity Account", FeatureFlags: `{"advanced_assignment":true}`}
require.NoError(t, db.Create(account).Error)
inbox := &model.Inbox{AccountID: account.ID, Name: "Capacity Inbox", ChannelType: string(model.InboxChannelTypeWebWidget), EnableAutoAssignment: true}
require.NoError(t, db.Create(inbox).Error)
contact := &model.Contact{AccountID: account.ID, Name: "Capacity Contact"}
require.NoError(t, db.Create(contact).Error)
agentAtCapacity := createCapacityUser(t, db, account.ID, "full@test.com")
agentWithCapacity := createCapacityUser(t, db, account.ID, "available@test.com")
require.NoError(t, db.Create(&model.InboxMember{InboxID: inbox.ID, UserID: agentAtCapacity.ID}).Error)
require.NoError(t, db.Create(&model.InboxMember{InboxID: inbox.ID, UserID: agentWithCapacity.ID}).Error)
policy := &model.AgentCapacityPolicy{AccountID: account.ID, 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 = ?", account.ID, agentAtCapacity.ID).
Update("agent_capacity_policy_id", policy.ID).Error)
require.NoError(t, db.Create(&model.InboxCapacityLimit{AgentCapacityPolicyID: policy.ID, InboxID: inbox.ID, ConversationLimit: 1}).Error)
now := time.Now().Unix()
existing := &model.Conversation{AccountID: account.ID, InboxID: inbox.ID, ContactID: contact.ID, AssigneeID: &agentAtCapacity.ID, Status: string(model.ConversationStatusOpen), ChannelType: "web_widget", Channel: "web_widget", LastActivityAt: &now}
unassigned := &model.Conversation{AccountID: account.ID, InboxID: inbox.ID, ContactID: contact.ID, Status: string(model.ConversationStatusOpen), ChannelType: "web_widget", Channel: "web_widget", LastActivityAt: &now}
require.NoError(t, db.Create(existing).Error)
require.NoError(t, db.Create(unassigned).Error)
agentID, err := svc.AssignConversation(ctx, unassigned.ID, inbox.ID, account.ID)
require.NoError(t, err)
assert.Equal(t, agentWithCapacity.ID, agentID)
}
func TestAssignmentServiceIgnoresCapacityWhenAdvancedAssignmentIsDisabled(t *testing.T) {
_, rdb := setupTestRedis(t)
db := setupCapacityAssignmentDB(t)
account := &model.Account{Name: "Default Assignment"}
require.NoError(t, db.Create(account).Error)
inbox := &model.Inbox{AccountID: account.ID, Name: "Inbox", ChannelType: string(model.InboxChannelTypeWebWidget), EnableAutoAssignment: true}
require.NoError(t, db.Create(inbox).Error)
agent := createCapacityUser(t, db, account.ID, "default@test.com")
require.NoError(t, db.Create(&model.InboxMember{InboxID: inbox.ID, UserID: agent.ID}).Error)
policy := &model.AgentCapacityPolicy{AccountID: account.ID, Name: "Excluded", ExclusionRules: []byte(`{"exclude_older_than_hours":1}`)}
require.NoError(t, db.Create(policy).Error)
require.NoError(t, db.Model(&model.AccountUser{}).Where("account_id = ? AND user_id = ?", account.ID, agent.ID).Update("agent_capacity_policy_id", policy.ID).Error)
require.NoError(t, db.Create(&model.InboxCapacityLimit{AgentCapacityPolicyID: policy.ID, InboxID: inbox.ID, ConversationLimit: 0}).Error)
contact := &model.Contact{AccountID: account.ID, Name: "Contact"}
require.NoError(t, db.Create(contact).Error)
old := time.Now().Add(-2 * time.Hour).Unix()
conversation := &model.Conversation{AccountID: account.ID, InboxID: inbox.ID, ContactID: contact.ID, Status: string(model.ConversationStatusOpen), LastActivityAt: &old}
require.NoError(t, db.Create(conversation).Error)
assigned, err := NewAssignmentService(db, rdb).AssignConversation(context.Background(), conversation.ID, inbox.ID, account.ID)
require.NoError(t, err)
require.Equal(t, agent.ID, assigned)
}
func TestAssignmentServiceAppliesCapacityPolicyExclusions(t *testing.T) {
_, rdb := setupTestRedis(t)
db := setupCapacityAssignmentDB(t)
account := &model.Account{Name: "Exclusions", FeatureFlags: `{"advanced_assignment":true}`}
require.NoError(t, db.Create(account).Error)
inbox := &model.Inbox{AccountID: account.ID, Name: "Inbox", ChannelType: string(model.InboxChannelTypeWebWidget), EnableAutoAssignment: true}
require.NoError(t, db.Create(inbox).Error)
agent := createCapacityUser(t, db, account.ID, "exclusions@test.com")
require.NoError(t, db.Create(&model.InboxMember{InboxID: inbox.ID, UserID: agent.ID}).Error)
capacityPolicy := &model.AgentCapacityPolicy{
AccountID: account.ID,
Name: "Exclude stale spam",
ExclusionRules: []byte(`{"excluded_labels":["spam"],"exclude_older_than_hours":24}`),
}
require.NoError(t, db.Create(capacityPolicy).Error)
require.NoError(t, db.Model(&model.AccountUser{}).Where("account_id = ? AND user_id = ?", account.ID, agent.ID).Update("agent_capacity_policy_id", capacityPolicy.ID).Error)
require.NoError(t, db.Create(&model.InboxCapacityLimit{AgentCapacityPolicyID: capacityPolicy.ID, InboxID: inbox.ID, ConversationLimit: 10}).Error)
contact := &model.Contact{AccountID: account.ID, Name: "Contact"}
require.NoError(t, db.Create(contact).Error)
now := time.Now().Unix()
old := time.Now().Add(-25 * time.Hour).Unix()
recent := &model.Conversation{AccountID: account.ID, InboxID: inbox.ID, ContactID: contact.ID, Status: string(model.ConversationStatusOpen), LastActivityAt: &now}
labeled := &model.Conversation{AccountID: account.ID, InboxID: inbox.ID, ContactID: contact.ID, Status: string(model.ConversationStatusOpen), LastActivityAt: &now}
stale := &model.Conversation{AccountID: account.ID, InboxID: inbox.ID, ContactID: contact.ID, Status: string(model.ConversationStatusOpen), LastActivityAt: &old}
unknownAge := &model.Conversation{AccountID: account.ID, InboxID: inbox.ID, ContactID: contact.ID, Status: string(model.ConversationStatusOpen)}
require.NoError(t, db.Create([]*model.Conversation{recent, labeled, stale, unknownAge}).Error)
tag := &model.Tag{AccountID: account.ID, Name: "spam"}
require.NoError(t, db.Create(tag).Error)
require.NoError(t, db.Create(&model.ConversationLabel{AccountID: account.ID, ConversationID: labeled.ID, TagID: tag.ID}).Error)
assigned, err := NewAssignmentService(db, rdb).AssignUnassignedConversations(context.Background(), inbox.ID, account.ID)
require.NoError(t, err)
require.Equal(t, []uint{recent.ID}, assigned)
for _, excluded := range []*model.Conversation{labeled, stale, unknownAge} {
require.NoError(t, db.First(excluded, excluded.ID).Error)
require.Nil(t, excluded.AssigneeID)
}
}
func TestAssignmentServiceAppliesEachAgentsCapacityPolicyExclusions(t *testing.T) {
_, rdb := setupTestRedis(t)
db := setupCapacityAssignmentDB(t)
account := &model.Account{Name: "Per-agent exclusions", FeatureFlags: `{"advanced_assignment":true}`}
require.NoError(t, db.Create(account).Error)
inbox := &model.Inbox{AccountID: account.ID, Name: "Inbox", ChannelType: string(model.InboxChannelTypeWebWidget), EnableAutoAssignment: true}
require.NoError(t, db.Create(inbox).Error)
spamAgent := createCapacityUser(t, db, account.ID, "spam-policy@test.com")
vipAgent := createCapacityUser(t, db, account.ID, "vip-policy@test.com")
require.NoError(t, db.Create([]*model.InboxMember{{InboxID: inbox.ID, UserID: spamAgent.ID}, {InboxID: inbox.ID, UserID: vipAgent.ID}}).Error)
spamPolicy := &model.AgentCapacityPolicy{AccountID: account.ID, Name: "Exclude spam", ExclusionRules: []byte(`{"excluded_labels":["spam"]}`)}
vipPolicy := &model.AgentCapacityPolicy{AccountID: account.ID, Name: "Exclude vip", ExclusionRules: []byte(`{"excluded_labels":["vip"]}`)}
require.NoError(t, db.Create(spamPolicy).Error)
require.NoError(t, db.Create(vipPolicy).Error)
require.NoError(t, db.Model(&model.AccountUser{}).Where("account_id = ? AND user_id = ?", account.ID, spamAgent.ID).Update("agent_capacity_policy_id", spamPolicy.ID).Error)
require.NoError(t, db.Model(&model.AccountUser{}).Where("account_id = ? AND user_id = ?", account.ID, vipAgent.ID).Update("agent_capacity_policy_id", vipPolicy.ID).Error)
require.NoError(t, db.Create([]*model.InboxCapacityLimit{
{AgentCapacityPolicyID: spamPolicy.ID, InboxID: inbox.ID, ConversationLimit: 10},
{AgentCapacityPolicyID: vipPolicy.ID, InboxID: inbox.ID, ConversationLimit: 10},
}).Error)
contact := &model.Contact{AccountID: account.ID, Name: "Contact"}
require.NoError(t, db.Create(contact).Error)
now := time.Now().Unix()
spam := &model.Conversation{AccountID: account.ID, InboxID: inbox.ID, ContactID: contact.ID, Status: string(model.ConversationStatusOpen), LastActivityAt: &now}
vip := &model.Conversation{AccountID: account.ID, InboxID: inbox.ID, ContactID: contact.ID, Status: string(model.ConversationStatusOpen), LastActivityAt: &now}
require.NoError(t, db.Create([]*model.Conversation{spam, vip}).Error)
spamTag := &model.Tag{AccountID: account.ID, Name: "spam"}
vipTag := &model.Tag{AccountID: account.ID, Name: "vip"}
require.NoError(t, db.Create([]*model.Tag{spamTag, vipTag}).Error)
require.NoError(t, db.Create([]*model.ConversationLabel{
{AccountID: account.ID, ConversationID: spam.ID, TagID: spamTag.ID},
{AccountID: account.ID, ConversationID: vip.ID, TagID: vipTag.ID},
}).Error)
assigned, err := NewAssignmentService(db, rdb).AssignUnassignedConversations(context.Background(), inbox.ID, account.ID)
require.NoError(t, err)
require.Equal(t, []uint{spam.ID, vip.ID}, assigned)
require.NoError(t, db.First(spam, spam.ID).Error)
require.NoError(t, db.First(vip, vip.ID).Error)
require.Equal(t, vipAgent.ID, *spam.AssigneeID)
require.Equal(t, spamAgent.ID, *vip.AssigneeID)
}
func TestAssignmentEligibilityRejectsSoftDeletedUserAndAccountUser(t *testing.T) {
for _, deleted := range []string{"user", "account user"} {
t.Run(deleted, func(t *testing.T) {
db := setupCapacityAssignmentDB(t)
account := &model.Account{Name: deleted}
require.NoError(t, db.Create(account).Error)
inbox := &model.Inbox{AccountID: account.ID, Name: "Inbox", ChannelType: string(model.InboxChannelTypeWebWidget), EnableAutoAssignment: true}
require.NoError(t, db.Create(inbox).Error)
agent := createCapacityUser(t, db, account.ID, deleted+"@test.com")
require.NoError(t, db.Create(&model.InboxMember{InboxID: inbox.ID, UserID: agent.ID}).Error)
contact := &model.Contact{AccountID: account.ID, Name: "Contact"}
require.NoError(t, db.Create(contact).Error)
conversation := &model.Conversation{AccountID: account.ID, InboxID: inbox.ID, ContactID: contact.ID, Status: string(model.ConversationStatusOpen)}
require.NoError(t, db.Create(conversation).Error)
if deleted == "user" {
require.NoError(t, db.Delete(agent).Error)
} else {
require.NoError(t, db.Where("account_id = ? AND user_id = ?", account.ID, agent.ID).Delete(&model.AccountUser{}).Error)
}
agents, err := NewAssignmentService(db, nil).getEligibleAgents(context.Background(), inbox.ID, account.ID, nil, false)
require.NoError(t, err)
require.Empty(t, agents)
assigned, err := repository.AutoAssignConversation(context.Background(), db, account.ID, inbox.ID, conversation.ID, agent.ID)
require.NoError(t, err)
require.False(t, assigned)
})
}
}
func TestLongestWaitingSortsUnknownActivityLast(t *testing.T) {
testLongestWaitingSortsUnknownActivityLast(t, setupCapacityAssignmentDB(t))
}
func testLongestWaitingSortsUnknownActivityLast(t *testing.T, db *gorm.DB) {
t.Helper()
account := &model.Account{Name: "Longest waiting"}
require.NoError(t, db.Create(account).Error)
inbox := &model.Inbox{AccountID: account.ID, Name: "Inbox", ChannelType: string(model.InboxChannelTypeWebWidget)}
require.NoError(t, db.Create(inbox).Error)
contact := &model.Contact{AccountID: account.ID, Name: "Contact"}
require.NoError(t, db.Create(contact).Error)
olderActivity := time.Now().Add(-2 * time.Hour).Unix()
newerActivity := time.Now().Add(-time.Hour).Unix()
unknown := &model.Conversation{AccountID: account.ID, InboxID: inbox.ID, ContactID: contact.ID, Status: string(model.ConversationStatusOpen)}
newer := &model.Conversation{AccountID: account.ID, InboxID: inbox.ID, ContactID: contact.ID, Status: string(model.ConversationStatusOpen), LastActivityAt: &newerActivity}
older := &model.Conversation{AccountID: account.ID, InboxID: inbox.ID, ContactID: contact.ID, Status: string(model.ConversationStatusOpen), LastActivityAt: &olderActivity}
require.NoError(t, db.Create([]*model.Conversation{unknown, newer, older}).Error)
conversations, err := NewAssignmentService(db, nil).findUnassignedConversations(
context.Background(), inbox.ID, account.ID, &model.AssignmentPolicy{ConversationPriority: 1},
)
require.NoError(t, err)
require.Equal(t, []uint{older.ID, newer.ID, unknown.ID}, []uint{conversations[0].ID, conversations[1].ID, conversations[2].ID})
}
func createCapacityUser(t *testing.T, db *gorm.DB, accountID uint, email string) *model.User {
t.Helper()
user := &model.User{AccountID: accountID, Name: email, Email: email, Password: "hashed", Active: true, Available: true}
require.NoError(t, db.Create(user).Error)
require.NoError(t, db.Create(&model.AccountUser{AccountID: accountID, UserID: user.ID, Role: "agent", Availability: "online"}).Error)
return user
}