Files
gochat/backend/internal/autoassignment/coverage9_test.go
T

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())
}