1119 lines
51 KiB
Go
1119 lines
51 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/automation"
|
|
"github.com/gochat/gochat/internal/channel"
|
|
"github.com/gochat/gochat/internal/model"
|
|
"github.com/gochat/gochat/internal/repository"
|
|
"github.com/gochat/gochat/internal/worker"
|
|
)
|
|
|
|
// ========== 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.Notification{},
|
|
&model.ConversationParticipant{},
|
|
&model.InboxMember{},
|
|
&model.ConversationLabel{},
|
|
&model.Team{},
|
|
&model.TeamMember{},
|
|
&model.Tag{},
|
|
&model.AccountUser{},
|
|
&model.CustomRole{},
|
|
&model.SlaPolicy{},
|
|
&model.AppliedSLA{},
|
|
&model.SlaEvent{},
|
|
&model.BackgroundJob{},
|
|
), "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, FeatureFlags: `{"conversation_unread_counts":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
|
|
}
|
|
|
|
type captureConversationEventsListener struct {
|
|
events []*channel.ChannelEvent
|
|
}
|
|
|
|
func (l *captureConversationEventsListener) Name() string { return "capture_conversation_events" }
|
|
|
|
func (l *captureConversationEventsListener) OnEvent(ctx context.Context, event *channel.ChannelEvent) error {
|
|
l.events = append(l.events, event)
|
|
return nil
|
|
}
|
|
|
|
type recordingConversationTranscriptDeliverer struct {
|
|
requests []automation.AutomationTranscriptRequest
|
|
result automation.ActionDeliveryResult
|
|
err error
|
|
}
|
|
|
|
func (d *recordingConversationTranscriptDeliverer) DeliverTranscript(ctx context.Context, req automation.AutomationTranscriptRequest) (automation.ActionDeliveryResult, error) {
|
|
d.requests = append(d.requests, req)
|
|
result := d.result
|
|
if result.DeliveryType == "" {
|
|
result.DeliveryType = "email_transcript"
|
|
}
|
|
if result.Target == "" {
|
|
result.Target = req.Recipient
|
|
}
|
|
return result, d.err
|
|
}
|
|
|
|
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")
|
|
}
|
|
|
|
func TestConversationService_Update_DispatchesChangedAttributesForAutomation(t *testing.T) {
|
|
svc, db := setupConversationService(t)
|
|
capture := &captureConversationEventsListener{}
|
|
svc.dispatcher.Register(capture)
|
|
|
|
account := createConversationServiceTestAccount(t, db)
|
|
inbox := createConversationServiceTestInbox(t, db, account.ID)
|
|
contact := createConversationServiceTestContact(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{Status: "resolved", Priority: "high"})
|
|
require.NoError(t, err)
|
|
|
|
var updatedEvent, resolvedEvent *channel.ChannelEvent
|
|
for _, event := range capture.events {
|
|
switch event.Type {
|
|
case channel.EventConversationUpdated:
|
|
updatedEvent = event
|
|
case channel.EventConversationResolved:
|
|
resolvedEvent = event
|
|
}
|
|
}
|
|
require.NotNil(t, updatedEvent)
|
|
require.NotNil(t, resolvedEvent)
|
|
|
|
for _, event := range []*channel.ChannelEvent{updatedEvent, resolvedEvent} {
|
|
changes, ok := event.Data["changed_attributes"].(map[string]interface{})
|
|
require.True(t, ok)
|
|
statusChange, ok := changes["status"].(map[string]interface{})
|
|
require.True(t, ok)
|
|
assert.Equal(t, "open", statusChange["from"])
|
|
assert.Equal(t, "resolved", statusChange["to"])
|
|
priorityChange, ok := changes["priority"].(map[string]interface{})
|
|
require.True(t, ok)
|
|
assert.Equal(t, "none", priorityChange["from"])
|
|
assert.Equal(t, "high", priorityChange["to"])
|
|
}
|
|
}
|
|
|
|
func TestConversationService_Create_DispatchesInitialMessageWithConversationData(t *testing.T) {
|
|
svc, db := setupConversationService(t)
|
|
capture := &captureConversationEventsListener{}
|
|
svc.dispatcher.Register(capture)
|
|
|
|
account := createConversationServiceTestAccount(t, db)
|
|
inbox := createConversationServiceTestInbox(t, db, account.ID)
|
|
contact := createConversationServiceTestContact(t, db, account.ID)
|
|
|
|
conversation, err := svc.Create(context.Background(), account.ID, CreateConversationRequest{InboxID: inbox.ID, ContactID: contact.ID, MessageContent: "hello"})
|
|
require.NoError(t, err)
|
|
|
|
var messageEvent *channel.ChannelEvent
|
|
for _, event := range capture.events {
|
|
if event.Type == channel.EventMessageCreated {
|
|
messageEvent = event
|
|
break
|
|
}
|
|
}
|
|
require.NotNil(t, messageEvent)
|
|
assert.Equal(t, conversation.ID, messageEvent.ConversationID)
|
|
assert.Equal(t, contact.ID, messageEvent.ContactID)
|
|
message, ok := messageEvent.Data["message"].(*model.Message)
|
|
require.True(t, ok)
|
|
assert.Equal(t, conversation.ID, message.ConversationID)
|
|
assert.Equal(t, account.ID, message.AccountID)
|
|
assert.Equal(t, inbox.ID, message.InboxID)
|
|
}
|
|
|
|
func TestConversationService_ToggleTyping_DispatchesChatwootTypingEvents(t *testing.T) {
|
|
svc, db := setupConversationService(t)
|
|
capture := &captureConversationEventsListener{}
|
|
svc.dispatcher.Register(capture)
|
|
|
|
account := createConversationServiceTestAccount(t, db)
|
|
inbox := createConversationServiceTestInbox(t, db, account.ID)
|
|
contact := createConversationServiceTestContact(t, db, account.ID)
|
|
conversation := createConversationServiceTestConversation(t, db, account.ID, inbox.ID, contact.ID, "open")
|
|
userID := uint(42)
|
|
|
|
require.NoError(t, svc.ToggleTyping(context.Background(), account.ID, conversation.ID, userID, "on", true))
|
|
require.NoError(t, svc.ToggleTyping(context.Background(), account.ID, conversation.ID, userID, "off", false))
|
|
|
|
require.Len(t, capture.events, 2)
|
|
assert.Equal(t, channel.EventConversationTypingOn, capture.events[0].Type)
|
|
assert.Equal(t, conversation.ID, capture.events[0].ConversationID)
|
|
assert.Equal(t, contact.ID, capture.events[0].ContactID)
|
|
assert.Equal(t, userID, capture.events[0].UserID)
|
|
assert.Equal(t, true, capture.events[0].Data["is_private"])
|
|
assert.Equal(t, channel.EventConversationTypingOff, capture.events[1].Type)
|
|
}
|
|
|
|
func TestConversationService_ToggleTyping_BlankStatusIsNoopAfterConversationLookup(t *testing.T) {
|
|
svc, db := setupConversationService(t)
|
|
capture := &captureConversationEventsListener{}
|
|
svc.dispatcher.Register(capture)
|
|
|
|
account := createConversationServiceTestAccount(t, db)
|
|
inbox := createConversationServiceTestInbox(t, db, account.ID)
|
|
contact := createConversationServiceTestContact(t, db, account.ID)
|
|
conversation := createConversationServiceTestConversation(t, db, account.ID, inbox.ID, contact.ID, "open")
|
|
|
|
require.NoError(t, svc.ToggleTyping(context.Background(), account.ID, conversation.ID, 0, "", false))
|
|
assert.Empty(t, capture.events)
|
|
|
|
err := svc.ToggleTyping(context.Background(), account.ID, 99999, 0, "", false)
|
|
assert.Error(t, err)
|
|
}
|
|
|
|
// ========== 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)
|
|
user := &model.User{Name: "Meta Agent", Email: "meta-agent@example.com"}
|
|
require.NoError(t, db.Create(user).Error)
|
|
require.NoError(t, db.Create(&model.AccountUser{AccountID: account.ID, UserID: user.ID, Role: "administrator"}).Error)
|
|
|
|
// Chatwoot meta defaults to open conversations and reports assignee buckets.
|
|
openUnassigned := createConversationServiceTestConversation(t, db, account.ID, inbox.ID, contact.ID, "open")
|
|
assert.NotZero(t, openUnassigned.ID)
|
|
openAssigned := createConversationServiceTestConversation(t, db, account.ID, inbox.ID, contact.ID, "open")
|
|
require.NoError(t, db.Model(openAssigned).Update("assignee_id", user.ID).Error)
|
|
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, user.ID, FilterParams{})
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, int64(2), meta.AllCount)
|
|
assert.Equal(t, int64(1), meta.MineCount)
|
|
assert.Equal(t, int64(1), meta.AssignedCount)
|
|
assert.Equal(t, int64(1), meta.UnassignedCount)
|
|
}
|
|
|
|
func TestConversationService_GetMeta_EmptyAccount(t *testing.T) {
|
|
svc, db := setupConversationService(t)
|
|
|
|
account := createConversationServiceTestAccount(t, db)
|
|
|
|
meta, err := svc.GetMeta(context.Background(), account.ID, 0, FilterParams{})
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, int64(0), meta.AllCount)
|
|
assert.Equal(t, int64(0), meta.UnassignedCount)
|
|
}
|
|
|
|
func TestConversationService_GetMeta_FiltersByLabelAndVisibility(t *testing.T) {
|
|
svc, db := setupConversationService(t)
|
|
|
|
account := createConversationServiceTestAccount(t, db)
|
|
inbox := createConversationServiceTestInbox(t, db, account.ID)
|
|
hiddenInbox := createConversationServiceTestInbox(t, db, account.ID)
|
|
contact := createConversationServiceTestContact(t, db, account.ID)
|
|
user := &model.User{Name: "Scoped Meta Agent", Email: "scoped-meta@example.com"}
|
|
require.NoError(t, db.Create(user).Error)
|
|
require.NoError(t, db.Create(&model.AccountUser{AccountID: account.ID, UserID: user.ID, Role: "agent"}).Error)
|
|
require.NoError(t, db.Create(&model.InboxMember{InboxID: inbox.ID, UserID: user.ID}).Error)
|
|
|
|
visible := createConversationServiceTestConversation(t, db, account.ID, inbox.ID, contact.ID, "open")
|
|
require.NoError(t, db.Model(visible).Updates(map[string]any{"labels": "vip", "assignee_id": user.ID}).Error)
|
|
hidden := createConversationServiceTestConversation(t, db, account.ID, hiddenInbox.ID, contact.ID, "open")
|
|
require.NoError(t, db.Model(hidden).Update("labels", "vip").Error)
|
|
|
|
meta, err := svc.GetMeta(context.Background(), account.ID, user.ID, FilterParams{Labels: "vip"})
|
|
require.NoError(t, err)
|
|
assert.Equal(t, int64(1), meta.AllCount)
|
|
assert.Equal(t, int64(1), meta.MineCount)
|
|
}
|
|
|
|
func TestConversationService_Filter_RestrictsAgentToAssignedInboxes(t *testing.T) {
|
|
svc, db := setupConversationService(t)
|
|
|
|
account := createConversationServiceTestAccount(t, db)
|
|
visibleInbox := createConversationServiceTestInbox(t, db, account.ID)
|
|
hiddenInbox := createConversationServiceTestInbox(t, db, account.ID)
|
|
contact := createConversationServiceTestContact(t, db, account.ID)
|
|
agent := &model.User{Name: "Filter Agent", Email: "filter-agent@example.com"}
|
|
require.NoError(t, db.Create(agent).Error)
|
|
require.NoError(t, db.Create(&model.AccountUser{AccountID: account.ID, UserID: agent.ID, Role: "agent"}).Error)
|
|
require.NoError(t, db.Create(&model.InboxMember{InboxID: visibleInbox.ID, UserID: agent.ID}).Error)
|
|
|
|
visible := createConversationServiceTestConversation(t, db, account.ID, visibleInbox.ID, contact.ID, "open")
|
|
require.NoError(t, db.Model(visible).Update("labels", "vip").Error)
|
|
hidden := createConversationServiceTestConversation(t, db, account.ID, hiddenInbox.ID, contact.ID, "open")
|
|
require.NoError(t, db.Model(hidden).Update("labels", "vip").Error)
|
|
|
|
result, err := svc.Filter(context.Background(), account.ID, agent.ID, FilterParams{Labels: "vip", AssigneeType: "all"}, 0, 25)
|
|
require.NoError(t, err)
|
|
require.Len(t, result.Conversations, 1)
|
|
assert.Equal(t, visible.ID, result.Conversations[0].ID)
|
|
assert.Equal(t, int64(1), result.Count.AllCount)
|
|
}
|
|
|
|
func TestConversationService_Filter_AllowsAdministratorAllInboxes(t *testing.T) {
|
|
svc, db := setupConversationService(t)
|
|
|
|
account := createConversationServiceTestAccount(t, db)
|
|
inboxA := createConversationServiceTestInbox(t, db, account.ID)
|
|
inboxB := createConversationServiceTestInbox(t, db, account.ID)
|
|
contact := createConversationServiceTestContact(t, db, account.ID)
|
|
admin := &model.User{Name: "Filter Admin", Email: "filter-admin@example.com"}
|
|
require.NoError(t, db.Create(admin).Error)
|
|
require.NoError(t, db.Create(&model.AccountUser{AccountID: account.ID, UserID: admin.ID, Role: "administrator"}).Error)
|
|
|
|
first := createConversationServiceTestConversation(t, db, account.ID, inboxA.ID, contact.ID, "open")
|
|
require.NoError(t, db.Model(first).Update("labels", "vip").Error)
|
|
second := createConversationServiceTestConversation(t, db, account.ID, inboxB.ID, contact.ID, "open")
|
|
require.NoError(t, db.Model(second).Update("labels", "vip").Error)
|
|
|
|
result, err := svc.Filter(context.Background(), account.ID, admin.ID, FilterParams{Labels: "vip", AssigneeType: "all"}, 0, 25)
|
|
require.NoError(t, err)
|
|
assert.Len(t, result.Conversations, 2)
|
|
assert.Equal(t, int64(2), result.Count.AllCount)
|
|
}
|
|
|
|
func TestConversationService_Filter_QueryUsesStoredMessageTypeStrings(t *testing.T) {
|
|
svc, db := setupConversationService(t)
|
|
|
|
account := createConversationServiceTestAccount(t, db)
|
|
inbox := createConversationServiceTestInbox(t, db, account.ID)
|
|
contact := createConversationServiceTestContact(t, db, account.ID)
|
|
admin := &model.User{Name: "Filter Query Admin", Email: "filter-query-admin@example.com"}
|
|
require.NoError(t, db.Create(admin).Error)
|
|
require.NoError(t, db.Create(&model.AccountUser{AccountID: account.ID, UserID: admin.ID, Role: "administrator"}).Error)
|
|
|
|
match := createConversationServiceTestConversation(t, db, account.ID, inbox.ID, contact.ID, "open")
|
|
activityOnly := createConversationServiceTestConversation(t, db, account.ID, inbox.ID, contact.ID, "open")
|
|
noMatch := createConversationServiceTestConversation(t, db, account.ID, inbox.ID, contact.ID, "open")
|
|
|
|
require.NoError(t, db.Create(&model.Message{AccountID: account.ID, InboxID: inbox.ID, ConversationID: match.ID, Content: "Needle from customer", MessageType: string(model.MessageTypeIncoming)}).Error)
|
|
require.NoError(t, db.Create(&model.Message{AccountID: account.ID, InboxID: inbox.ID, ConversationID: activityOnly.ID, Content: "Needle activity", MessageType: string(model.MessageTypeActivity)}).Error)
|
|
require.NoError(t, db.Create(&model.Message{AccountID: account.ID, InboxID: inbox.ID, ConversationID: noMatch.ID, Content: "other", MessageType: string(model.MessageTypeIncoming)}).Error)
|
|
|
|
result, err := svc.Filter(context.Background(), account.ID, admin.ID, FilterParams{Query: "needle", AssigneeType: "all"}, 0, 25)
|
|
require.NoError(t, err)
|
|
require.Len(t, result.Conversations, 1)
|
|
assert.Equal(t, match.ID, result.Conversations[0].ID)
|
|
assert.Equal(t, int64(1), result.Count.AllCount)
|
|
}
|
|
|
|
func TestConversationService_Mute_BlocksContactAndResolvesConversation(t *testing.T) {
|
|
svc, db := setupConversationService(t)
|
|
|
|
account := createConversationServiceTestAccount(t, db)
|
|
inbox := createConversationServiceTestInbox(t, db, account.ID)
|
|
contact := createConversationServiceTestContact(t, db, account.ID)
|
|
conversation := createConversationServiceTestConversation(t, db, account.ID, inbox.ID, contact.ID, "open")
|
|
require.NoError(t, db.Model(conversation).Update("labels", "vip").Error)
|
|
|
|
muted, err := svc.Mute(context.Background(), account.ID, conversation.ID)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, string(model.ConversationStatusResolved), muted.Status)
|
|
assert.True(t, muted.Muted)
|
|
assert.Equal(t, "vip", muted.Labels)
|
|
|
|
var persistedConversation model.Conversation
|
|
require.NoError(t, db.First(&persistedConversation, conversation.ID).Error)
|
|
assert.Equal(t, string(model.ConversationStatusResolved), persistedConversation.Status)
|
|
assert.True(t, persistedConversation.Muted)
|
|
assert.Equal(t, "vip", persistedConversation.Labels)
|
|
|
|
var persistedContact model.Contact
|
|
require.NoError(t, db.First(&persistedContact, contact.ID).Error)
|
|
assert.True(t, persistedContact.Blocked)
|
|
}
|
|
|
|
func TestConversationService_Unmute_UnblocksContactWithoutReopeningConversation(t *testing.T) {
|
|
svc, db := setupConversationService(t)
|
|
|
|
account := createConversationServiceTestAccount(t, db)
|
|
inbox := createConversationServiceTestInbox(t, db, account.ID)
|
|
contact := createConversationServiceTestContact(t, db, account.ID)
|
|
conversation := createConversationServiceTestConversation(t, db, account.ID, inbox.ID, contact.ID, string(model.ConversationStatusResolved))
|
|
require.NoError(t, db.Model(contact).Update("blocked", true).Error)
|
|
require.NoError(t, db.Model(conversation).Update("muted", true).Error)
|
|
|
|
unmuted, err := svc.Unmute(context.Background(), account.ID, conversation.ID)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, string(model.ConversationStatusResolved), unmuted.Status)
|
|
assert.False(t, unmuted.Muted)
|
|
|
|
var persistedConversation model.Conversation
|
|
require.NoError(t, db.First(&persistedConversation, conversation.ID).Error)
|
|
assert.Equal(t, string(model.ConversationStatusResolved), persistedConversation.Status)
|
|
assert.False(t, persistedConversation.Muted)
|
|
|
|
var persistedContact model.Contact
|
|
require.NoError(t, db.First(&persistedContact, contact.ID).Error)
|
|
assert.False(t, persistedContact.Blocked)
|
|
}
|
|
|
|
// ========== 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).Updates(map[string]any{"agent_last_seen_at": seenAt, "assignee_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)
|
|
assert.NotNil(t, conversation.AssigneeLastSeenAt)
|
|
assert.Equal(t, expectedSeenAt, *conversation.AssigneeLastSeenAt)
|
|
}
|
|
|
|
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).Updates(map[string]any{"agent_last_seen_at": seenAt, "assignee_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)
|
|
assert.Nil(t, conversation.AssigneeLastSeenAt)
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
func TestConversationService_UpdateLastSeen_AssigneeUnreadUpdatesBothColumnsAndReadsNotifications(t *testing.T) {
|
|
svc, db := setupConversationService(t)
|
|
|
|
account := createConversationServiceTestAccount(t, db)
|
|
inbox := createConversationServiceTestInbox(t, db, account.ID)
|
|
contact := createConversationServiceTestContact(t, db, account.ID)
|
|
user := &model.User{Name: "Last Seen Agent", Email: "last-seen-agent@example.com"}
|
|
require.NoError(t, db.Create(user).Error)
|
|
conv := createConversationServiceTestConversation(t, db, account.ID, inbox.ID, contact.ID, "open")
|
|
oldSeen := time.Now().Add(-2 * time.Hour).Unix()
|
|
require.NoError(t, db.Model(conv).Updates(map[string]any{"assignee_id": user.ID, "agent_last_seen_at": oldSeen, "assignee_last_seen_at": oldSeen}).Error)
|
|
require.NoError(t, db.Create(&model.Message{AccountID: account.ID, InboxID: inbox.ID, ConversationID: conv.ID, MessageType: string(model.MessageTypeIncoming), Content: "new"}).Error)
|
|
accountID := account.ID
|
|
notification := &model.Notification{UserID: user.ID, AccountID: &accountID, NotificationType: "assigned_conversation_new_message", PrimaryActorType: "Conversation", PrimaryActorID: conv.ID}
|
|
require.NoError(t, db.Create(notification).Error)
|
|
|
|
require.NoError(t, svc.UpdateLastSeen(context.Background(), account.ID, conv.ID, user.ID))
|
|
|
|
var updated model.Conversation
|
|
require.NoError(t, db.First(&updated, conv.ID).Error)
|
|
require.NotNil(t, updated.AgentLastSeenAt)
|
|
require.NotNil(t, updated.AssigneeLastSeenAt)
|
|
assert.Greater(t, *updated.AgentLastSeenAt, oldSeen)
|
|
assert.Greater(t, *updated.AssigneeLastSeenAt, oldSeen)
|
|
|
|
var updatedNotification model.Notification
|
|
require.NoError(t, db.First(&updatedNotification, notification.ID).Error)
|
|
assert.NotNil(t, updatedNotification.ReadAt)
|
|
}
|
|
|
|
func TestConversationService_UpdateLastSeen_NonAssigneeUnreadUpdatesOnlyAgentColumn(t *testing.T) {
|
|
svc, db := setupConversationService(t)
|
|
|
|
account := createConversationServiceTestAccount(t, db)
|
|
inbox := createConversationServiceTestInbox(t, db, account.ID)
|
|
contact := createConversationServiceTestContact(t, db, account.ID)
|
|
assignee := &model.User{Name: "Assigned Agent", Email: "assigned-agent@example.com"}
|
|
viewer := &model.User{Name: "Viewing Agent", Email: "viewing-agent@example.com"}
|
|
require.NoError(t, db.Create(assignee).Error)
|
|
require.NoError(t, db.Create(viewer).Error)
|
|
conv := createConversationServiceTestConversation(t, db, account.ID, inbox.ID, contact.ID, "open")
|
|
oldSeen := time.Now().Add(-2 * time.Hour).Unix()
|
|
require.NoError(t, db.Model(conv).Updates(map[string]any{"assignee_id": assignee.ID, "agent_last_seen_at": oldSeen, "assignee_last_seen_at": oldSeen}).Error)
|
|
require.NoError(t, db.Create(&model.Message{AccountID: account.ID, InboxID: inbox.ID, ConversationID: conv.ID, MessageType: string(model.MessageTypeIncoming), Content: "new"}).Error)
|
|
|
|
require.NoError(t, svc.UpdateLastSeen(context.Background(), account.ID, conv.ID, viewer.ID))
|
|
|
|
var updated model.Conversation
|
|
require.NoError(t, db.First(&updated, conv.ID).Error)
|
|
require.NotNil(t, updated.AgentLastSeenAt)
|
|
require.NotNil(t, updated.AssigneeLastSeenAt)
|
|
assert.Greater(t, *updated.AgentLastSeenAt, oldSeen)
|
|
assert.Equal(t, oldSeen, *updated.AssigneeLastSeenAt)
|
|
}
|
|
|
|
func TestConversationService_UpdateLastSeen_ThrottlesRecentNoUnreadTimestamps(t *testing.T) {
|
|
svc, db := setupConversationService(t)
|
|
|
|
account := createConversationServiceTestAccount(t, db)
|
|
inbox := createConversationServiceTestInbox(t, db, account.ID)
|
|
contact := createConversationServiceTestContact(t, db, account.ID)
|
|
user := &model.User{Name: "Recent Agent", Email: "recent-agent@example.com"}
|
|
require.NoError(t, db.Create(user).Error)
|
|
conv := createConversationServiceTestConversation(t, db, account.ID, inbox.ID, contact.ID, "open")
|
|
recentSeen := time.Now().Add(-time.Minute).Unix()
|
|
require.NoError(t, db.Model(conv).Updates(map[string]any{"agent_last_seen_at": recentSeen, "assignee_last_seen_at": recentSeen}).Error)
|
|
|
|
require.NoError(t, svc.UpdateLastSeen(context.Background(), account.ID, conv.ID, user.ID))
|
|
|
|
var updated model.Conversation
|
|
require.NoError(t, db.First(&updated, conv.ID).Error)
|
|
require.NotNil(t, updated.AgentLastSeenAt)
|
|
require.NotNil(t, updated.AssigneeLastSeenAt)
|
|
assert.Equal(t, recentSeen, *updated.AgentLastSeenAt)
|
|
assert.Equal(t, recentSeen, *updated.AssigneeLastSeenAt)
|
|
}
|
|
|
|
// ========== SendTranscript Tests ==========
|
|
|
|
func TestConversationService_SendTranscript(t *testing.T) {
|
|
svc, db := setupConversationService(t)
|
|
deliverer := &recordingConversationTranscriptDeliverer{}
|
|
svc.SetTranscriptDeliverer(deliverer)
|
|
|
|
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")
|
|
require.NoError(t, db.Model(conv).Update("display_id", 42).Error)
|
|
require.NoError(t, db.Create(&model.Message{ConversationID: conv.ID, AccountID: account.ID, InboxID: inbox.ID, Content: "visible incoming", ContentType: "text", MessageType: string(model.MessageTypeIncoming)}).Error)
|
|
require.NoError(t, db.Create(&model.Message{ConversationID: conv.ID, AccountID: account.ID, InboxID: inbox.ID, Content: "visible outgoing", ContentType: "text", MessageType: string(model.MessageTypeOutgoing)}).Error)
|
|
require.NoError(t, db.Create(&model.Message{ConversationID: conv.ID, AccountID: account.ID, InboxID: inbox.ID, Content: "hidden private", ContentType: "text", MessageType: string(model.MessageTypeIncoming), Private: true}).Error)
|
|
require.NoError(t, db.Create(&model.Message{ConversationID: conv.ID, AccountID: account.ID, InboxID: inbox.ID, Content: "hidden activity", ContentType: "text", MessageType: string(model.MessageTypeActivity)}).Error)
|
|
require.NoError(t, db.Create(&model.Message{ConversationID: conv.ID, AccountID: account.ID, InboxID: inbox.ID, Content: "hidden template", ContentType: "text", MessageType: string(model.MessageTypeTemplate)}).Error)
|
|
|
|
err := svc.SendTranscript(context.Background(), account.ID, conv.ID, "test@example.com")
|
|
assert.NoError(t, err)
|
|
require.Len(t, deliverer.requests, 1)
|
|
assert.Equal(t, "test@example.com", deliverer.requests[0].Recipient)
|
|
assert.Equal(t, "[#42] Conversation Transcript", deliverer.requests[0].Subject)
|
|
assert.Contains(t, deliverer.requests[0].Body, "visible incoming")
|
|
assert.Contains(t, deliverer.requests[0].Body, "visible outgoing")
|
|
assert.NotContains(t, deliverer.requests[0].Body, "hidden private")
|
|
assert.NotContains(t, deliverer.requests[0].Body, "hidden activity")
|
|
assert.NotContains(t, deliverer.requests[0].Body, "hidden template")
|
|
|
|
var updated model.Account
|
|
require.NoError(t, db.First(&updated, account.ID).Error)
|
|
assert.Equal(t, 1, updated.EmailsSentToday(time.Now()))
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
func TestConversationService_SendTranscript_Disabled(t *testing.T) {
|
|
svc, db := setupConversationService(t)
|
|
deliverer := &recordingConversationTranscriptDeliverer{}
|
|
svc.SetTranscriptDeliverer(deliverer)
|
|
|
|
account := createConversationServiceTestAccount(t, db)
|
|
require.NoError(t, db.Model(account).Update("limits", datatypes.JSON(`{"email_transcript_enabled":false}`)).Error)
|
|
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.ErrorIs(t, err, ErrEmailTranscriptDisabled)
|
|
assert.Empty(t, deliverer.requests)
|
|
}
|
|
|
|
func TestConversationService_SendTranscript_RateLimited(t *testing.T) {
|
|
svc, db := setupConversationService(t)
|
|
deliverer := &recordingConversationTranscriptDeliverer{}
|
|
svc.SetTranscriptDeliverer(deliverer)
|
|
|
|
account := createConversationServiceTestAccount(t, db)
|
|
require.NoError(t, db.Model(account).Updates(map[string]any{
|
|
"limits": datatypes.JSON(`{"emails":1}`),
|
|
"custom_attributes": datatypes.JSON(`{"_outbound_email_count":{"date":"` + time.Now().Format("2006-01-02") + `","count":1}}`),
|
|
}).Error)
|
|
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.ErrorIs(t, err, ErrEmailRateLimited)
|
|
assert.Empty(t, deliverer.requests)
|
|
}
|
|
|
|
func TestConversationService_Delete(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")
|
|
|
|
require.NoError(t, svc.Delete(context.Background(), account.ID, conv.ID))
|
|
|
|
var found model.Conversation
|
|
assert.Error(t, db.First(&found, conv.ID).Error)
|
|
require.NoError(t, db.Unscoped().First(&found, conv.ID).Error)
|
|
assert.True(t, found.DeletedAt.Valid)
|
|
}
|
|
|
|
func TestConversationService_Delete_QueuesDeleteObjectJob(t *testing.T) {
|
|
svc, db := setupConversationService(t)
|
|
wp := worker.NewWorkerPool(db)
|
|
svc.SetWorkerPool(wp)
|
|
|
|
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")
|
|
|
|
require.NoError(t, svc.Delete(context.Background(), account.ID, conv.ID))
|
|
|
|
var queued model.BackgroundJob
|
|
require.NoError(t, db.Where("job_type = ? AND queue = ? AND status = ?", TaskTypeConversationDeleteObject, "low", model.BackgroundJobStatusQueued).First(&queued).Error)
|
|
var stillVisible model.Conversation
|
|
require.NoError(t, db.First(&stillVisible, conv.ID).Error)
|
|
|
|
processed, err := wp.ProcessOne(context.Background())
|
|
require.NoError(t, err)
|
|
assert.True(t, processed)
|
|
|
|
var deleted model.Conversation
|
|
assert.Error(t, db.First(&deleted, conv.ID).Error)
|
|
}
|
|
|
|
func TestConversationService_DeleteWithWorker_NotFoundDoesNotQueue(t *testing.T) {
|
|
svc, db := setupConversationService(t)
|
|
wp := worker.NewWorkerPool(db)
|
|
svc.SetWorkerPool(wp)
|
|
|
|
err := svc.Delete(context.Background(), 1, 9999)
|
|
require.ErrorIs(t, err, gorm.ErrRecordNotFound)
|
|
|
|
var count int64
|
|
require.NoError(t, db.Model(&model.BackgroundJob{}).Where("job_type = ?", TaskTypeConversationDeleteObject).Count(&count).Error)
|
|
assert.Zero(t, count)
|
|
}
|
|
|
|
// ========== 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, 0)
|
|
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, 0)
|
|
assert.NoError(t, err)
|
|
assert.NotNil(t, payload)
|
|
assert.Empty(t, payload.Inboxes)
|
|
}
|
|
|
|
func TestConversationService_UnreadCounts_FeatureDisabled(t *testing.T) {
|
|
svc, db := setupConversationService(t)
|
|
|
|
account := &model.Account{Name: "UnreadDisabled", Locale: "en", Active: true}
|
|
require.NoError(t, db.Create(account).Error)
|
|
|
|
payload, err := svc.GetUnreadCounts(context.Background(), account.ID, 0)
|
|
assert.Nil(t, payload)
|
|
assert.ErrorIs(t, err, ErrConversationUnreadCountsFeatureNotEnabled)
|
|
}
|
|
|
|
func TestConversationService_UnreadCounts_LabelIDsAndSidebarOnly(t *testing.T) {
|
|
svc, db := setupConversationService(t)
|
|
|
|
account := createConversationServiceTestAccount(t, db)
|
|
inbox := createConversationServiceTestInbox(t, db, account.ID)
|
|
contact := createConversationServiceTestContact(t, db, account.ID)
|
|
show := true
|
|
hide := false
|
|
visibleLabel := &model.Tag{AccountID: account.ID, Name: "visible", ShowOnSidebar: &show}
|
|
hiddenLabel := &model.Tag{AccountID: account.ID, Name: "hidden", ShowOnSidebar: &hide}
|
|
require.NoError(t, db.Create(visibleLabel).Error)
|
|
require.NoError(t, db.Create(hiddenLabel).Error)
|
|
|
|
lastMsgTime := int64(1700000100)
|
|
conv := createConversationServiceTestConversation(t, db, account.ID, inbox.ID, contact.ID, "open")
|
|
require.NoError(t, db.Model(conv).Updates(map[string]any{"agent_last_seen_at": int64(1700000000), "last_message_at": lastMsgTime}).Error)
|
|
require.NoError(t, db.Create(&model.ConversationLabel{AccountID: account.ID, ConversationID: conv.ID, TagID: visibleLabel.ID}).Error)
|
|
require.NoError(t, db.Create(&model.ConversationLabel{AccountID: account.ID, ConversationID: conv.ID, TagID: hiddenLabel.ID}).Error)
|
|
|
|
payload, err := svc.GetUnreadCounts(context.Background(), account.ID, 0)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, int64(1), payload.Labels[visibleLabel.ID])
|
|
assert.NotContains(t, payload.Labels, hiddenLabel.ID)
|
|
}
|
|
|
|
func TestConversationService_UnreadCounts_CustomRoleParticipatingScope(t *testing.T) {
|
|
svc, db := setupConversationService(t)
|
|
|
|
account := createConversationServiceTestAccount(t, db)
|
|
inbox := createConversationServiceTestInbox(t, db, account.ID)
|
|
contact := createConversationServiceTestContact(t, db, account.ID)
|
|
user := &model.User{Name: "Scoped Agent", Email: "scoped@example.com"}
|
|
require.NoError(t, db.Create(user).Error)
|
|
role := &model.CustomRole{AccountID: account.ID, Name: "Mine only"}
|
|
require.NoError(t, role.SetPermissionKeys([]model.PermissionDimension{model.DimensionConversationParticipatingManage}))
|
|
require.NoError(t, db.Create(role).Error)
|
|
require.NoError(t, db.Create(&model.AccountUser{AccountID: account.ID, UserID: user.ID, Role: "agent", CustomRoleID: role.ID}).Error)
|
|
require.NoError(t, db.Create(&model.InboxMember{InboxID: inbox.ID, UserID: user.ID}).Error)
|
|
|
|
seenAt := int64(1700000000)
|
|
lastMsgTime := int64(1700000100)
|
|
mine := createConversationServiceTestConversation(t, db, account.ID, inbox.ID, contact.ID, "open")
|
|
mine.AssigneeID = &user.ID
|
|
require.NoError(t, db.Model(mine).Updates(map[string]any{"assignee_id": user.ID, "agent_last_seen_at": seenAt, "last_message_at": lastMsgTime}).Error)
|
|
unassigned := createConversationServiceTestConversation(t, db, account.ID, inbox.ID, contact.ID, "open")
|
|
require.NoError(t, db.Model(unassigned).Updates(map[string]any{"agent_last_seen_at": seenAt, "last_message_at": lastMsgTime}).Error)
|
|
|
|
payload, err := svc.GetUnreadCounts(context.Background(), account.ID, user.ID)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, int64(1), payload.Inboxes[inbox.ID])
|
|
}
|
|
|
|
func TestConversationService_MutationEventsCarryChatwootChangeData(t *testing.T) {
|
|
db := setupConversationServiceTestDB(t)
|
|
convRepo := repository.NewConversationRepo(db)
|
|
msgRepo := repository.NewMessageRepo(db)
|
|
dispatcher := channel.NewDispatcher()
|
|
listener := &captureConversationEventsListener{}
|
|
dispatcher.Register(listener)
|
|
inboxMemberSvc := NewInboxMemberService(repository.NewInboxMemberRepo(db))
|
|
accountUserRepo := repository.NewAccountUserRepo(db)
|
|
teamRepo := repository.NewTeamRepo(db)
|
|
teamMemberRepo := repository.NewTeamMemberRepo(db)
|
|
svc := NewConversationService(convRepo, msgRepo, dispatcher, inboxMemberSvc, accountUserRepo, teamRepo, teamMemberRepo)
|
|
|
|
ctx := context.Background()
|
|
account := createConversationServiceTestAccount(t, db)
|
|
inbox := createConversationServiceTestInbox(t, db, account.ID)
|
|
contact := createConversationServiceTestContact(t, db, account.ID)
|
|
conversation := createConversationServiceTestConversation(t, db, account.ID, inbox.ID, contact.ID, string(model.ConversationStatusOpen))
|
|
user := &model.User{Name: "Realtime Agent", Email: "realtime-agent@example.com"}
|
|
require.NoError(t, db.Create(user).Error)
|
|
require.NoError(t, db.Create(&model.AccountUser{AccountID: account.ID, UserID: user.ID, Role: "agent"}).Error)
|
|
require.NoError(t, db.Create(&model.InboxMember{InboxID: inbox.ID, UserID: user.ID}).Error)
|
|
team := &model.Team{AccountID: account.ID, Name: "Realtime Team", AllowAutoAssignment: true}
|
|
require.NoError(t, db.Create(team).Error)
|
|
|
|
_, err := svc.ToggleStatus(ctx, account.ID, conversation.ID, ToggleStatusRequest{Status: string(model.ConversationStatusResolved)})
|
|
require.NoError(t, err)
|
|
statusEvent := listener.events[len(listener.events)-1]
|
|
assert.Equal(t, channel.EventConversationResolved, statusEvent.Type)
|
|
assert.Equal(t, account.ID, statusEvent.AccountID)
|
|
assert.Equal(t, inbox.ID, statusEvent.InboxID)
|
|
assert.Equal(t, conversation.ID, statusEvent.ConversationID)
|
|
statusChanges := statusEvent.Data["changed_attributes"].(map[string]interface{})
|
|
assert.Equal(t, map[string]interface{}{"from": "open", "to": "resolved"}, statusChanges["status"])
|
|
assert.Equal(t, conversation.ID, statusEvent.Data["conversation"].(*model.Conversation).ID)
|
|
|
|
_, err = svc.UpdatePriority(ctx, account.ID, conversation.ID, string(model.ConversationPriorityUrgent))
|
|
require.NoError(t, err)
|
|
priorityEvent := listener.events[len(listener.events)-1]
|
|
assert.Equal(t, channel.EventConversationPriorityUpdated, priorityEvent.Type)
|
|
priorityChanges := priorityEvent.Data["changed_attributes"].(map[string]interface{})
|
|
assert.Equal(t, map[string]interface{}{"from": "none", "to": "urgent"}, priorityChanges["priority"])
|
|
|
|
_, err = svc.UpdateLabels(ctx, account.ID, conversation.ID, []string{"support", "urgent"})
|
|
require.NoError(t, err)
|
|
labelsEvent := listener.events[len(listener.events)-1]
|
|
assert.Equal(t, channel.EventConversationLabelsUpdated, labelsEvent.Type)
|
|
assert.Equal(t, "support,urgent", labelsEvent.Data["conversation"].(*model.Conversation).Labels)
|
|
|
|
_, err = svc.AssignTeam(ctx, account.ID, conversation.ID, &user.ID, &team.ID)
|
|
require.NoError(t, err)
|
|
assignmentEvent := listener.events[len(listener.events)-1]
|
|
assert.Equal(t, channel.EventConversationAssigned, assignmentEvent.Type)
|
|
assert.Equal(t, user.ID, assignmentEvent.UserID)
|
|
assignedConversation := assignmentEvent.Data["conversation"].(*model.Conversation)
|
|
require.NotNil(t, assignedConversation.AssigneeID)
|
|
require.NotNil(t, assignedConversation.TeamID)
|
|
assert.Equal(t, user.ID, *assignedConversation.AssigneeID)
|
|
assert.Equal(t, team.ID, *assignedConversation.TeamID)
|
|
}
|
|
|
|
func TestConversationService_ToggleTypingDispatchesChatwootPayload(t *testing.T) {
|
|
db := setupConversationServiceTestDB(t)
|
|
convRepo := repository.NewConversationRepo(db)
|
|
msgRepo := repository.NewMessageRepo(db)
|
|
dispatcher := channel.NewDispatcher()
|
|
listener := &captureConversationEventsListener{}
|
|
dispatcher.Register(listener)
|
|
svc := NewConversationService(convRepo, msgRepo, dispatcher, nil, nil, nil, nil)
|
|
|
|
ctx := context.Background()
|
|
account := createConversationServiceTestAccount(t, db)
|
|
inbox := createConversationServiceTestInbox(t, db, account.ID)
|
|
contact := createConversationServiceTestContact(t, db, account.ID)
|
|
conversation := createConversationServiceTestConversation(t, db, account.ID, inbox.ID, contact.ID, string(model.ConversationStatusOpen))
|
|
user := &model.User{Name: "Typing Agent", Email: "typing-agent@example.com"}
|
|
require.NoError(t, db.Create(user).Error)
|
|
|
|
require.NoError(t, svc.ToggleTyping(ctx, account.ID, conversation.ID, user.ID, "on", true))
|
|
require.Len(t, listener.events, 1)
|
|
typingOn := listener.events[0]
|
|
assert.Equal(t, channel.EventConversationTypingOn, typingOn.Type)
|
|
assert.Equal(t, account.ID, typingOn.AccountID)
|
|
assert.Equal(t, inbox.ID, typingOn.InboxID)
|
|
assert.Equal(t, conversation.ID, typingOn.ConversationID)
|
|
assert.Equal(t, contact.ID, typingOn.ContactID)
|
|
assert.Equal(t, user.ID, typingOn.UserID)
|
|
assert.Equal(t, "on", typingOn.Data["typing_status"])
|
|
assert.Equal(t, true, typingOn.Data["is_private"])
|
|
|
|
require.NoError(t, svc.ToggleTyping(ctx, account.ID, conversation.ID, user.ID, "typing_off", false))
|
|
require.Len(t, listener.events, 2)
|
|
typingOff := listener.events[1]
|
|
assert.Equal(t, channel.EventConversationTypingOff, typingOff.Type)
|
|
assert.Equal(t, "typing_off", typingOff.Data["typing_status"])
|
|
assert.Equal(t, false, typingOff.Data["is_private"])
|
|
|
|
require.NoError(t, svc.ToggleTyping(ctx, account.ID, conversation.ID, user.ID, "", false))
|
|
assert.Len(t, listener.events, 2)
|
|
}
|