188 lines
9.2 KiB
Go
188 lines
9.2 KiB
Go
package autoassignment
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/alicebob/miniredis/v2"
|
|
"github.com/gochat/gochat/internal/channel"
|
|
"github.com/gochat/gochat/internal/model"
|
|
"github.com/redis/go-redis/v9"
|
|
"github.com/stretchr/testify/require"
|
|
"gorm.io/driver/sqlite"
|
|
"gorm.io/gorm"
|
|
"gorm.io/gorm/logger"
|
|
)
|
|
|
|
func setupFullAADB_Cov9(t *testing.T) (*gorm.DB, *redis.Client) {
|
|
t.Helper()
|
|
db, err := gorm.Open(sqlite.Open(fmt.Sprintf("file:%s?mode=memory&cache=shared", strings.ReplaceAll(t.Name(), "/", "_"))), &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.ContactInbox{},
|
|
&model.Conversation{}, &model.Message{},
|
|
&AssignmentPolicy{}, &InboxAssignmentPolicy{},
|
|
&model.AgentCapacityPolicy{}, &model.InboxCapacityLimit{},
|
|
))
|
|
mr := miniredis.RunT(t)
|
|
rdb := redis.NewClient(&redis.Options{Addr: mr.Addr()})
|
|
t.Cleanup(func() { _ = rdb.Close() })
|
|
return db, rdb
|
|
}
|
|
|
|
func seedAssignableConversation_Cov9(t *testing.T, db *gorm.DB) (*model.Account, *model.User, *model.Inbox, *model.Conversation) {
|
|
t.Helper()
|
|
acc := &model.Account{Name: "cov9-account"}
|
|
require.NoError(t, db.Create(acc).Error)
|
|
agent := &model.User{AccountID: acc.ID, Name: "agent", Email: "agent-cov9@example.com", Password: "p", Active: true, Available: true}
|
|
require.NoError(t, db.Create(agent).Error)
|
|
require.NoError(t, db.Create(&model.AccountUser{AccountID: acc.ID, UserID: agent.ID, Role: "agent"}).Error)
|
|
inbox := &model.Inbox{AccountID: acc.ID, Name: "inbox", ChannelType: "web_widget", EnableAutoAssignment: true}
|
|
require.NoError(t, db.Create(inbox).Error)
|
|
require.NoError(t, db.Create(&model.InboxMember{InboxID: inbox.ID, UserID: agent.ID, Role: "agent", AvailabilityStatus: "online"}).Error)
|
|
contact := &model.Contact{AccountID: acc.ID, Name: "contact", Email: "contact-cov9@example.com"}
|
|
require.NoError(t, db.Create(contact).Error)
|
|
conv := &model.Conversation{AccountID: acc.ID, InboxID: inbox.ID, ContactID: contact.ID, Status: string(model.ConversationStatusOpen)}
|
|
require.NoError(t, db.Create(conv).Error)
|
|
return acc, agent, inbox, conv
|
|
}
|
|
|
|
func TestEffectivePolicyLimitWindow_ActiveInactive_Cov9(t *testing.T) {
|
|
activeAccount := &AssignmentPolicy{Policy: PolicyLongestWaiting, FairDistributionLimit: 11, FairDistributionWindow: 111, Active: true}
|
|
inactiveAccount := &AssignmentPolicy{Policy: PolicyLowestLoad, FairDistributionLimit: 22, FairDistributionWindow: 222, Active: false}
|
|
activeInbox := &InboxAssignmentPolicy{Policy: PolicyLowestLoad, FairDistributionLimit: 33, FairDistributionWindow: 333, Active: true}
|
|
inactiveInbox := &InboxAssignmentPolicy{Policy: PolicyLowestLoad, FairDistributionLimit: 44, FairDistributionWindow: 444, Active: false}
|
|
|
|
require.Equal(t, PolicyRoundRobin, EffectivePolicy(nil, nil))
|
|
require.Equal(t, PolicyLongestWaiting, EffectivePolicy(activeAccount, nil))
|
|
require.Equal(t, PolicyRoundRobin, EffectivePolicy(inactiveAccount, nil))
|
|
require.Equal(t, PolicyLowestLoad, EffectivePolicy(activeAccount, activeInbox))
|
|
require.Equal(t, PolicyLongestWaiting, EffectivePolicy(activeAccount, inactiveInbox))
|
|
|
|
require.Equal(t, 5, EffectiveLimit(nil, nil))
|
|
require.Equal(t, 11, EffectiveLimit(activeAccount, nil))
|
|
require.Equal(t, 22, EffectiveLimit(inactiveAccount, nil))
|
|
require.Equal(t, 33, EffectiveLimit(activeAccount, activeInbox))
|
|
require.Equal(t, 11, EffectiveLimit(activeAccount, inactiveInbox))
|
|
|
|
require.Equal(t, 300, EffectiveWindow(nil, nil))
|
|
require.Equal(t, 111, EffectiveWindow(activeAccount, nil))
|
|
require.Equal(t, 222, EffectiveWindow(inactiveAccount, nil))
|
|
require.Equal(t, 333, EffectiveWindow(activeAccount, activeInbox))
|
|
require.Equal(t, 111, EffectiveWindow(activeAccount, inactiveInbox))
|
|
}
|
|
|
|
func TestAssignmentService_AssignConversation_Success_Cov9(t *testing.T) {
|
|
db, rdb := setupFullAADB_Cov9(t)
|
|
acc, agent, inbox, conv := seedAssignableConversation_Cov9(t, db)
|
|
svc := NewAssignmentService(db, rdb)
|
|
agentID, err := svc.AssignConversation(context.Background(), conv.ID, inbox.ID, acc.ID)
|
|
require.NoError(t, err)
|
|
require.Equal(t, agent.ID, agentID)
|
|
|
|
var updated model.Conversation
|
|
require.NoError(t, db.First(&updated, conv.ID).Error)
|
|
require.NotNil(t, updated.AssigneeID)
|
|
require.Equal(t, agent.ID, *updated.AssigneeID)
|
|
}
|
|
|
|
func TestAssignmentService_AssignUnassignedConversations_Success_Cov9(t *testing.T) {
|
|
db, rdb := setupFullAADB_Cov9(t)
|
|
acc, _, inbox, conv := seedAssignableConversation_Cov9(t, db)
|
|
svc := NewAssignmentService(db, rdb)
|
|
ids, err := svc.AssignUnassignedConversations(context.Background(), inbox.ID, acc.ID)
|
|
require.NoError(t, err)
|
|
require.Contains(t, ids, conv.ID)
|
|
}
|
|
|
|
func TestAssignmentService_AssignConversation_NoEligibleAgents_Cov9(t *testing.T) {
|
|
db, rdb := setupFullAADB_Cov9(t)
|
|
acc, agent, inbox, conv := seedAssignableConversation_Cov9(t, db)
|
|
require.NoError(t, db.Model(agent).Update("available", false).Error)
|
|
svc := NewAssignmentService(db, rdb)
|
|
agentID, err := svc.AssignConversation(context.Background(), conv.ID, inbox.ID, acc.ID)
|
|
require.NoError(t, err)
|
|
require.Zero(t, agentID)
|
|
}
|
|
|
|
func TestAssignmentService_GetAndPolicyHelpers_Cov9(t *testing.T) {
|
|
db, rdb := setupFullAADB_Cov9(t)
|
|
acc, _, inbox, conv := seedAssignableConversation_Cov9(t, db)
|
|
svc := NewAssignmentService(db, rdb)
|
|
policy := &AssignmentPolicy{AccountID: acc.ID, Policy: PolicyLowestLoad, FairDistributionLimit: 7, FairDistributionWindow: 77, Active: true}
|
|
require.NoError(t, db.Create(policy).Error)
|
|
inboxPolicy := &InboxAssignmentPolicy{AccountID: acc.ID, InboxID: inbox.ID, Policy: PolicyLongestWaiting, FairDistributionLimit: 8, FairDistributionWindow: 88, Active: true}
|
|
require.NoError(t, db.Create(inboxPolicy).Error)
|
|
|
|
loadedInbox, err := svc.getInbox(context.Background(), inbox.ID)
|
|
require.NoError(t, err)
|
|
require.Equal(t, inbox.ID, loadedInbox.ID)
|
|
convs, err := svc.findUnassignedConversations(context.Background(), inbox.ID, acc.ID)
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, convs)
|
|
_ = conv
|
|
require.Equal(t, PolicyLowestLoad, svc.getAccountPolicy(context.Background(), acc.ID).Policy)
|
|
require.Equal(t, PolicyLongestWaiting, svc.getInboxPolicy(context.Background(), inbox.ID).Policy)
|
|
}
|
|
|
|
func TestAssignmentService_AgentHasInboxCapacity_Limited_Cov9(t *testing.T) {
|
|
db, rdb := setupFullAADB_Cov9(t)
|
|
acc, agent, inbox, conv := seedAssignableConversation_Cov9(t, db)
|
|
policy := &model.AgentCapacityPolicy{AccountID: acc.ID, Name: "cap"}
|
|
require.NoError(t, db.Create(policy).Error)
|
|
require.NoError(t, db.Create(&model.InboxCapacityLimit{AgentCapacityPolicyID: policy.ID, InboxID: inbox.ID, ConversationLimit: 1}).Error)
|
|
require.NoError(t, db.Model(&model.AccountUser{}).Where("account_id = ? AND user_id = ?", acc.ID, agent.ID).Update("agent_capacity_policy_id", policy.ID).Error)
|
|
agentID := agent.ID
|
|
require.NoError(t, db.Model(&model.Conversation{}).Where("id = ?", conv.ID).Update("assignee_id", agentID).Error)
|
|
|
|
svc := NewAssignmentService(db, rdb)
|
|
ok, err := svc.agentHasInboxCapacity(context.Background(), acc.ID, inbox.ID, agent.ID, 0)
|
|
require.NoError(t, err)
|
|
require.False(t, ok)
|
|
ok, err = svc.agentHasInboxCapacity(context.Background(), acc.ID, inbox.ID, agent.ID, conv.ID)
|
|
require.NoError(t, err)
|
|
require.True(t, ok)
|
|
}
|
|
|
|
func TestRoundRobinAndLowestLoad_Cov9(t *testing.T) {
|
|
db, rdb := setupFullAADB_Cov9(t)
|
|
_, agent, inbox, _ := seedAssignableConversation_Cov9(t, db)
|
|
rr := NewRoundRobinSelector(rdb)
|
|
rr.SyncQueue(context.Background(), inbox.ID, []uint{agent.ID, agent.ID + 1})
|
|
next, err := rr.Next(context.Background(), inbox.ID)
|
|
require.NoError(t, err)
|
|
require.NotZero(t, next)
|
|
|
|
ll := NewLowestLoadSelector(db)
|
|
selected, err := ll.Select(context.Background(), inbox.ID, []uint{agent.ID, agent.ID + 1})
|
|
require.NoError(t, err)
|
|
require.NotZero(t, selected)
|
|
}
|
|
|
|
func TestAutoAssignmentListener_OnEventPaths_Cov9(t *testing.T) {
|
|
db, rdb := setupFullAADB_Cov9(t)
|
|
acc, _, inbox, conv := seedAssignableConversation_Cov9(t, db)
|
|
listener := NewAutoAssignmentListener(db, rdb)
|
|
data := map[string]any{"conversation_id": float64(conv.ID), "inbox_id": float64(inbox.ID), "account_id": float64(acc.ID)}
|
|
require.NoError(t, listener.OnEvent(context.Background(), &channel.ChannelEvent{Type: channel.EventConversationCreated, Data: data}))
|
|
// Reset assignee to exercise opened/unassigned paths.
|
|
require.NoError(t, db.Model(&model.Conversation{}).Where("id = ?", conv.ID).Update("assignee_id", nil).Error)
|
|
require.NoError(t, listener.OnEvent(context.Background(), &channel.ChannelEvent{Type: channel.EventConversationOpened, Data: data}))
|
|
require.NoError(t, listener.OnEvent(context.Background(), &channel.ChannelEvent{Type: channel.EventConversationUnassigned, Data: data}))
|
|
require.NoError(t, listener.OnEvent(context.Background(), &channel.ChannelEvent{Type: "ignored", Data: data}))
|
|
}
|
|
|
|
func TestEventNames_Cov9(t *testing.T) {
|
|
names := EventNames()
|
|
require.Contains(t, names, string(channel.EventConversationCreated))
|
|
require.Contains(t, names, string(channel.EventConversationOpened))
|
|
require.Contains(t, names, string(channel.EventConversationUnassigned))
|
|
}
|
|
|
|
func TestConversationQueryModel_TableName_Cov9(t *testing.T) {
|
|
require.Equal(t, "conversations", ConversationQueryModel{}.TableName())
|
|
}
|