472 lines
18 KiB
Go
472 lines
18 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"gorm.io/datatypes"
|
|
"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"
|
|
)
|
|
|
|
// ========== Test Setup ==========
|
|
|
|
func setupConversationServiceTestDB(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, "failed to open SQLite test db")
|
|
|
|
require.NoError(t, db.AutoMigrate(
|
|
&model.Account{},
|
|
&model.User{},
|
|
&model.Inbox{},
|
|
&model.Contact{},
|
|
&model.ContactInbox{},
|
|
&model.Conversation{},
|
|
&model.Message{},
|
|
&model.ConversationParticipant{},
|
|
&model.InboxMember{},
|
|
&model.ConversationLabel{},
|
|
&model.Team{},
|
|
&model.Tag{},
|
|
&model.SlaPolicy{},
|
|
&model.AppliedSLA{},
|
|
&model.SlaEvent{},
|
|
), "failed to auto-migrate")
|
|
|
|
t.Cleanup(func() {
|
|
sqlDB, _ := db.DB()
|
|
sqlDB.Close()
|
|
})
|
|
|
|
return db
|
|
}
|
|
|
|
func createConversationServiceTestAccount(t *testing.T, db *gorm.DB) *model.Account {
|
|
t.Helper()
|
|
account := &model.Account{Name: "ConvSvcTestOrg", Locale: "en", Active: true}
|
|
require.NoError(t, db.Create(account).Error)
|
|
return account
|
|
}
|
|
|
|
func createConversationServiceTestInbox(t *testing.T, db *gorm.DB, accountID uint) *model.Inbox {
|
|
t.Helper()
|
|
inbox := &model.Inbox{AccountID: accountID, Name: "ConvSvcTestInbox", ChannelType: "web_widget", ChannelID: 1, Enabled: true}
|
|
require.NoError(t, db.Create(inbox).Error)
|
|
return inbox
|
|
}
|
|
|
|
func createConversationServiceTestContact(t *testing.T, db *gorm.DB, accountID uint) *model.Contact {
|
|
t.Helper()
|
|
contact := &model.Contact{AccountID: accountID, Name: "ConvSvcTestContact", Email: "contact@test.com"}
|
|
require.NoError(t, db.Create(contact).Error)
|
|
return contact
|
|
}
|
|
|
|
func createConversationServiceTestConversation(t *testing.T, db *gorm.DB, accountID, inboxID, contactID uint, status string) *model.Conversation {
|
|
t.Helper()
|
|
conv := &model.Conversation{
|
|
AccountID: accountID,
|
|
InboxID: inboxID,
|
|
ContactID: contactID,
|
|
Status: status,
|
|
ChannelType: "web_widget",
|
|
Channel: "web_widget",
|
|
}
|
|
require.NoError(t, db.Create(conv).Error)
|
|
return conv
|
|
}
|
|
|
|
func setupConversationService(t *testing.T) (*ConversationService, *gorm.DB) {
|
|
t.Helper()
|
|
db := setupConversationServiceTestDB(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)
|
|
appliedSlaSvc := NewAppliedSlaService(repository.NewAppliedSlaRepo(db), repository.NewSlaEventRepo(db), repository.NewSlaPolicyRepo(db), convRepo)
|
|
svc.SetAppliedSlaService(appliedSlaSvc)
|
|
return svc, db
|
|
}
|
|
|
|
func createConversationServiceTestSlaPolicy(t *testing.T, db *gorm.DB, accountID uint) *model.SlaPolicy {
|
|
t.Helper()
|
|
policy := &model.SlaPolicy{
|
|
AccountID: accountID,
|
|
Name: "Gold SLA",
|
|
FirstResponseTimeThreshold: 10,
|
|
NextResponseTimeThreshold: 20,
|
|
ResolutionTimeThreshold: 60,
|
|
}
|
|
require.NoError(t, db.Create(policy).Error)
|
|
return policy
|
|
}
|
|
|
|
func TestConversationService_Create_AppliesSlaPolicy(t *testing.T) {
|
|
svc, db := setupConversationService(t)
|
|
|
|
account := createConversationServiceTestAccount(t, db)
|
|
inbox := createConversationServiceTestInbox(t, db, account.ID)
|
|
contact := createConversationServiceTestContact(t, db, account.ID)
|
|
policy := createConversationServiceTestSlaPolicy(t, db, account.ID)
|
|
|
|
conversation, err := svc.Create(context.Background(), account.ID, CreateConversationRequest{
|
|
InboxID: inbox.ID,
|
|
ContactID: contact.ID,
|
|
SlaPolicyID: &policy.ID,
|
|
})
|
|
require.NoError(t, err)
|
|
require.NotNil(t, conversation.SlaPolicyID)
|
|
assert.Equal(t, policy.ID, *conversation.SlaPolicyID)
|
|
|
|
var applied model.AppliedSLA
|
|
require.NoError(t, db.Where("conversation_id = ?", conversation.ID).First(&applied).Error)
|
|
assert.Equal(t, account.ID, applied.AccountID)
|
|
assert.Equal(t, policy.ID, applied.SlaPolicyID)
|
|
assert.Equal(t, model.SLAStatusActive, applied.SLAStatus)
|
|
require.NotNil(t, applied.FRTTargetAt)
|
|
require.NotNil(t, applied.NRTTargetAt)
|
|
require.NotNil(t, applied.RTTargetAt)
|
|
assert.Equal(t, conversation.CreatedAt.Add(10*time.Second).Unix(), applied.FRTTargetAt.Unix())
|
|
}
|
|
|
|
func TestConversationService_Update_AppliesSlaPolicyOnce(t *testing.T) {
|
|
svc, db := setupConversationService(t)
|
|
|
|
account := createConversationServiceTestAccount(t, db)
|
|
inbox := createConversationServiceTestInbox(t, db, account.ID)
|
|
contact := createConversationServiceTestContact(t, db, account.ID)
|
|
policy := createConversationServiceTestSlaPolicy(t, db, account.ID)
|
|
conversation := createConversationServiceTestConversation(t, db, account.ID, inbox.ID, contact.ID, "open")
|
|
|
|
updated, err := svc.Update(context.Background(), account.ID, conversation.ID, UpdateConversationRequest{SlaPolicyID: &policy.ID})
|
|
require.NoError(t, err)
|
|
require.NotNil(t, updated.SlaPolicyID)
|
|
assert.Equal(t, policy.ID, *updated.SlaPolicyID)
|
|
|
|
updated, err = svc.Update(context.Background(), account.ID, conversation.ID, UpdateConversationRequest{SlaPolicyID: &policy.ID})
|
|
require.NoError(t, err)
|
|
assert.Equal(t, policy.ID, *updated.SlaPolicyID)
|
|
|
|
var count int64
|
|
require.NoError(t, db.Model(&model.AppliedSLA{}).Where("conversation_id = ?", conversation.ID).Count(&count).Error)
|
|
assert.Equal(t, int64(1), count)
|
|
}
|
|
|
|
func TestConversationService_Update_RejectsSlaPolicyReplacement(t *testing.T) {
|
|
svc, db := setupConversationService(t)
|
|
|
|
account := createConversationServiceTestAccount(t, db)
|
|
inbox := createConversationServiceTestInbox(t, db, account.ID)
|
|
contact := createConversationServiceTestContact(t, db, account.ID)
|
|
policy := createConversationServiceTestSlaPolicy(t, db, account.ID)
|
|
otherPolicy := createConversationServiceTestSlaPolicy(t, db, account.ID)
|
|
conversation := createConversationServiceTestConversation(t, db, account.ID, inbox.ID, contact.ID, "open")
|
|
|
|
_, err := svc.Update(context.Background(), account.ID, conversation.ID, UpdateConversationRequest{SlaPolicyID: &policy.ID})
|
|
require.NoError(t, err)
|
|
|
|
_, err = svc.Update(context.Background(), account.ID, conversation.ID, UpdateConversationRequest{SlaPolicyID: &otherPolicy.ID})
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "different sla")
|
|
}
|
|
|
|
// ========== GetMeta Tests ==========
|
|
|
|
func TestConversationService_GetMeta(t *testing.T) {
|
|
svc, db := setupConversationService(t)
|
|
|
|
account := createConversationServiceTestAccount(t, db)
|
|
inbox := createConversationServiceTestInbox(t, db, account.ID)
|
|
contact := createConversationServiceTestContact(t, db, account.ID)
|
|
|
|
// Create conversations with different statuses
|
|
createConversationServiceTestConversation(t, db, account.ID, inbox.ID, contact.ID, "open")
|
|
conv2 := createConversationServiceTestConversation(t, db, account.ID, inbox.ID, contact.ID, "resolved")
|
|
require.NoError(t, db.Model(conv2).Update("labels", "support").Error)
|
|
createConversationServiceTestConversation(t, db, account.ID, inbox.ID, contact.ID, "pending")
|
|
|
|
meta, err := svc.GetMeta(context.Background(), account.ID)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, int64(3), meta.TotalCount)
|
|
assert.Equal(t, int64(1), meta.StatusCounts["open"])
|
|
assert.Equal(t, int64(1), meta.StatusCounts["resolved"])
|
|
assert.Equal(t, int64(1), meta.StatusCounts["pending"])
|
|
assert.Equal(t, int64(1), meta.LabelCounts["support"])
|
|
}
|
|
|
|
func TestConversationService_GetMeta_EmptyAccount(t *testing.T) {
|
|
svc, db := setupConversationService(t)
|
|
|
|
account := createConversationServiceTestAccount(t, db)
|
|
|
|
meta, err := svc.GetMeta(context.Background(), account.ID)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, int64(0), meta.TotalCount)
|
|
assert.Empty(t, meta.StatusCounts)
|
|
}
|
|
|
|
// ========== MarkUnread Tests ==========
|
|
|
|
func TestConversationService_MarkUnread(t *testing.T) {
|
|
svc, db := setupConversationService(t)
|
|
|
|
account := createConversationServiceTestAccount(t, db)
|
|
inbox := createConversationServiceTestInbox(t, db, account.ID)
|
|
contact := createConversationServiceTestContact(t, db, account.ID)
|
|
|
|
conv := createConversationServiceTestConversation(t, db, account.ID, inbox.ID, contact.ID, "open")
|
|
|
|
// Set agent_last_seen_at to a non-nil value
|
|
seenAt := int64(1700000000)
|
|
require.NoError(t, db.Model(conv).Update("agent_last_seen_at", seenAt).Error)
|
|
|
|
// Create an incoming message so MarkUnread sets agent_last_seen_at to last_incoming.CreatedAt - 1s
|
|
incomingMsg := &model.Message{
|
|
AccountID: account.ID,
|
|
InboxID: inbox.ID,
|
|
ConversationID: conv.ID,
|
|
MessageType: string(model.MessageTypeIncoming),
|
|
Content: "hello from customer",
|
|
ContentAttributes: datatypes.JSON(`{"type": "text"}`),
|
|
}
|
|
require.NoError(t, db.Create(incomingMsg).Error)
|
|
|
|
conversation, err := svc.MarkUnread(context.Background(), account.ID, conv.ID)
|
|
assert.NoError(t, err)
|
|
assert.NotNil(t, conversation)
|
|
|
|
// Chatwoot behavior: agent_last_seen_at = last_incoming_message.CreatedAt - 1s
|
|
expectedSeenAt := incomingMsg.CreatedAt.Unix() - 1
|
|
assert.NotNil(t, conversation.AgentLastSeenAt)
|
|
assert.Equal(t, expectedSeenAt, *conversation.AgentLastSeenAt)
|
|
}
|
|
|
|
func TestConversationService_MarkUnread_NoIncomingMessages(t *testing.T) {
|
|
svc, db := setupConversationService(t)
|
|
|
|
account := createConversationServiceTestAccount(t, db)
|
|
inbox := createConversationServiceTestInbox(t, db, account.ID)
|
|
contact := createConversationServiceTestContact(t, db, account.ID)
|
|
|
|
conv := createConversationServiceTestConversation(t, db, account.ID, inbox.ID, contact.ID, "open")
|
|
|
|
// Set agent_last_seen_at to a non-nil value
|
|
seenAt := int64(1700000000)
|
|
require.NoError(t, db.Model(conv).Update("agent_last_seen_at", seenAt).Error)
|
|
|
|
// No incoming messages — MarkUnread should clear agent_last_seen_at to nil
|
|
conversation, err := svc.MarkUnread(context.Background(), account.ID, conv.ID)
|
|
assert.NoError(t, err)
|
|
assert.NotNil(t, conversation)
|
|
assert.Nil(t, conversation.AgentLastSeenAt)
|
|
}
|
|
|
|
func TestConversationService_MarkUnread_WrongAccount(t *testing.T) {
|
|
svc, db := setupConversationService(t)
|
|
|
|
account := createConversationServiceTestAccount(t, db)
|
|
inbox := createConversationServiceTestInbox(t, db, account.ID)
|
|
contact := createConversationServiceTestContact(t, db, account.ID)
|
|
|
|
conv := createConversationServiceTestConversation(t, db, account.ID, inbox.ID, contact.ID, "open")
|
|
|
|
// Try marking unread with a wrong account ID
|
|
conversation, err := svc.MarkUnread(context.Background(), 9999, conv.ID)
|
|
assert.Error(t, err)
|
|
assert.Nil(t, conversation)
|
|
}
|
|
|
|
func TestConversationService_MarkUnread_NotFound(t *testing.T) {
|
|
svc, _ := setupConversationService(t)
|
|
|
|
// Try marking unread on a non-existent conversation
|
|
conversation, err := svc.MarkUnread(context.Background(), 1, 9999)
|
|
assert.Error(t, err)
|
|
assert.Nil(t, conversation)
|
|
}
|
|
|
|
// ========== SendTranscript Tests ==========
|
|
|
|
func TestConversationService_SendTranscript(t *testing.T) {
|
|
svc, db := setupConversationService(t)
|
|
|
|
account := createConversationServiceTestAccount(t, db)
|
|
inbox := createConversationServiceTestInbox(t, db, account.ID)
|
|
contact := createConversationServiceTestContact(t, db, account.ID)
|
|
|
|
conv := createConversationServiceTestConversation(t, db, account.ID, inbox.ID, contact.ID, "open")
|
|
|
|
err := svc.SendTranscript(context.Background(), account.ID, conv.ID, "test@example.com")
|
|
assert.NoError(t, err)
|
|
}
|
|
|
|
func TestConversationService_SendTranscript_EmptyEmail(t *testing.T) {
|
|
svc, db := setupConversationService(t)
|
|
|
|
account := createConversationServiceTestAccount(t, db)
|
|
inbox := createConversationServiceTestInbox(t, db, account.ID)
|
|
contact := createConversationServiceTestContact(t, db, account.ID)
|
|
|
|
conv := createConversationServiceTestConversation(t, db, account.ID, inbox.ID, contact.ID, "open")
|
|
|
|
err := svc.SendTranscript(context.Background(), account.ID, conv.ID, "")
|
|
assert.Error(t, err)
|
|
assert.Equal(t, "email address is required for transcript", err.Error())
|
|
}
|
|
|
|
func TestConversationService_SendTranscript_WrongAccount(t *testing.T) {
|
|
svc, db := setupConversationService(t)
|
|
|
|
account := createConversationServiceTestAccount(t, db)
|
|
inbox := createConversationServiceTestInbox(t, db, account.ID)
|
|
contact := createConversationServiceTestContact(t, db, account.ID)
|
|
|
|
conv := createConversationServiceTestConversation(t, db, account.ID, inbox.ID, contact.ID, "open")
|
|
|
|
// Try sending transcript with a wrong account ID
|
|
err := svc.SendTranscript(context.Background(), 9999, conv.ID, "test@example.com")
|
|
assert.Error(t, err)
|
|
}
|
|
|
|
func TestConversationService_SendTranscript_ConversationNotFound(t *testing.T) {
|
|
svc, _ := setupConversationService(t)
|
|
|
|
err := svc.SendTranscript(context.Background(), 1, 9999, "test@example.com")
|
|
assert.Error(t, err)
|
|
}
|
|
|
|
// ========== UpdateCustomAttributes Tests ==========
|
|
|
|
func TestConversationService_UpdateCustomAttributes(t *testing.T) {
|
|
svc, db := setupConversationService(t)
|
|
|
|
account := createConversationServiceTestAccount(t, db)
|
|
inbox := createConversationServiceTestInbox(t, db, account.ID)
|
|
contact := createConversationServiceTestContact(t, db, account.ID)
|
|
|
|
conv := createConversationServiceTestConversation(t, db, account.ID, inbox.ID, contact.ID, "open")
|
|
|
|
attrs := datatypes.JSON(`{"priority_reason": "vip_customer", "region": "us-west"}`)
|
|
conversation, err := svc.UpdateCustomAttributes(context.Background(), account.ID, conv.ID, attrs)
|
|
assert.NoError(t, err)
|
|
assert.NotNil(t, conversation)
|
|
assert.NotNil(t, conversation.CustomAttributes)
|
|
}
|
|
|
|
func TestConversationService_UpdateCustomAttributes_WrongAccount(t *testing.T) {
|
|
svc, db := setupConversationService(t)
|
|
|
|
account := createConversationServiceTestAccount(t, db)
|
|
inbox := createConversationServiceTestInbox(t, db, account.ID)
|
|
contact := createConversationServiceTestContact(t, db, account.ID)
|
|
|
|
conv := createConversationServiceTestConversation(t, db, account.ID, inbox.ID, contact.ID, "open")
|
|
|
|
attrs := datatypes.JSON(`{"key": "value"}`)
|
|
conversation, err := svc.UpdateCustomAttributes(context.Background(), 9999, conv.ID, attrs)
|
|
assert.Error(t, err)
|
|
assert.Nil(t, conversation)
|
|
}
|
|
|
|
func TestConversationService_UpdateCustomAttributes_NotFound(t *testing.T) {
|
|
svc, _ := setupConversationService(t)
|
|
|
|
attrs := datatypes.JSON(`{"key": "value"}`)
|
|
conversation, err := svc.UpdateCustomAttributes(context.Background(), 1, 9999, attrs)
|
|
assert.Error(t, err)
|
|
assert.Nil(t, conversation)
|
|
}
|
|
|
|
// ========== UnreadCounts Tests ==========
|
|
|
|
func TestConversationService_UnreadCounts(t *testing.T) {
|
|
svc, db := setupConversationService(t)
|
|
|
|
account := createConversationServiceTestAccount(t, db)
|
|
inbox := createConversationServiceTestInbox(t, db, account.ID)
|
|
contact := createConversationServiceTestContact(t, db, account.ID)
|
|
|
|
// Create open conversations with agent_last_seen_at older than last message
|
|
conv1 := createConversationServiceTestConversation(t, db, account.ID, inbox.ID, contact.ID, "open")
|
|
conv2 := createConversationServiceTestConversation(t, db, account.ID, inbox.ID, contact.ID, "open")
|
|
|
|
// Create incoming messages
|
|
msg1 := &model.Message{
|
|
AccountID: account.ID,
|
|
InboxID: inbox.ID,
|
|
ConversationID: conv1.ID,
|
|
MessageType: string(model.MessageTypeIncoming),
|
|
Content: "msg1",
|
|
ContentAttributes: datatypes.JSON(`{"type": "text"}`),
|
|
}
|
|
require.NoError(t, db.Create(msg1).Error)
|
|
|
|
msg2 := &model.Message{
|
|
AccountID: account.ID,
|
|
InboxID: inbox.ID,
|
|
ConversationID: conv2.ID,
|
|
MessageType: string(model.MessageTypeIncoming),
|
|
Content: "msg2",
|
|
ContentAttributes: datatypes.JSON(`{"type": "text"}`),
|
|
}
|
|
require.NoError(t, db.Create(msg2).Error)
|
|
|
|
// Set agent_last_seen_at to before messages so conversations appear unread
|
|
pastTime := int64(1700000000)
|
|
require.NoError(t, db.Model(conv1).Update("agent_last_seen_at", pastTime).Error)
|
|
require.NoError(t, db.Model(conv2).Update("agent_last_seen_at", pastTime).Error)
|
|
|
|
// Set last_message_at to a time after agent_last_seen_at so conversations are unread
|
|
lastMsgTime := int64(1700000100)
|
|
require.NoError(t, db.Model(conv1).Update("last_message_at", lastMsgTime).Error)
|
|
require.NoError(t, db.Model(conv2).Update("last_message_at", lastMsgTime).Error)
|
|
|
|
payload, err := svc.GetUnreadCounts(context.Background(), account.ID)
|
|
assert.NoError(t, err)
|
|
assert.NotNil(t, payload)
|
|
|
|
// Both conversations are unread under the same inbox
|
|
inboxCount, ok := payload.Inboxes[inbox.ID]
|
|
assert.True(t, ok)
|
|
assert.Equal(t, int64(2), inboxCount)
|
|
}
|
|
|
|
func TestConversationService_UnreadCounts_NoUnread(t *testing.T) {
|
|
svc, db := setupConversationService(t)
|
|
|
|
account := createConversationServiceTestAccount(t, db)
|
|
inbox := createConversationServiceTestInbox(t, db, account.ID)
|
|
contact := createConversationServiceTestContact(t, db, account.ID)
|
|
|
|
conv := createConversationServiceTestConversation(t, db, account.ID, inbox.ID, contact.ID, "open")
|
|
|
|
// Set agent_last_seen_at to a recent time and last_message_at to an earlier time,
|
|
// so agent has seen all messages — conversation is NOT unread
|
|
nowTime := int64(1700000100)
|
|
lastMsgTime := int64(1700000000)
|
|
require.NoError(t, db.Model(conv).Update("agent_last_seen_at", nowTime).Error)
|
|
require.NoError(t, db.Model(conv).Update("last_message_at", lastMsgTime).Error)
|
|
|
|
payload, err := svc.GetUnreadCounts(context.Background(), account.ID)
|
|
assert.NoError(t, err)
|
|
assert.NotNil(t, payload)
|
|
assert.Empty(t, payload.Inboxes)
|
|
}
|