Files
gochat/internal/service/contact_service_g3_test.go
T

707 lines
26 KiB
Go

package service
import (
"bytes"
"context"
"fmt"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gorm.io/datatypes"
"github.com/gochat/gochat/internal/model"
"github.com/gochat/gochat/internal/search"
"github.com/gochat/gochat/internal/worker"
)
type mockContactSearchReader struct {
results []search.SearchResult
total int64
filter *search.SearchFilter
query string
}
func (m *mockContactSearchReader) SearchContacts(ctx context.Context, accountID uint, query string, filter *search.SearchFilter) ([]search.SearchResult, int64, error) {
m.query = query
m.filter = filter
return m.results, m.total, nil
}
type fakeContactExportMailer struct {
calls int
called bool
toEmail string
fileURL string
subject string
}
func (m *fakeContactExportMailer) SendContactExportComplete(ctx context.Context, account *model.Account, user *model.User, export *model.ContactExport) error {
m.calls++
m.called = true
m.toEmail = user.Email
m.fileURL = export.FileURL
m.subject = contactExportCompleteSubject
return nil
}
// ========== ListActive ==========
func TestContactService_ListActive_ReturnsContactsWithActivity(t *testing.T) {
db, _, svc := setupContactService(t)
account := createTestAccount(t, db)
// Create contacts: one with last_activity_at set, one without
now := time.Now().Unix()
activeContact := &model.Contact{
AccountID: account.ID,
Name: "Active Contact",
Email: "active@test.com",
LastActivityAt: &now,
}
inactiveContact := &model.Contact{
AccountID: account.ID,
Name: "Inactive Contact",
Email: "inactive@test.com",
// LastActivityAt is nil
}
require.NoError(t, db.Create(activeContact).Error)
require.NoError(t, db.Create(inactiveContact).Error)
contacts, total, err := svc.ListActive(context.Background(), account.ID, 0, 10, "")
require.NoError(t, err)
assert.Equal(t, int64(1), total)
assert.Len(t, contacts, 1)
assert.Equal(t, "Active Contact", contacts[0].Name)
}
func TestContactService_ListActive_EmptyWhenNoActiveContacts(t *testing.T) {
db, _, svc := setupContactService(t)
account := createTestAccount(t, db)
// Create a contact without last_activity_at
contact := &model.Contact{
AccountID: account.ID,
Name: "No Activity Contact",
Email: "noact@test.com",
}
require.NoError(t, db.Create(contact).Error)
contacts, total, err := svc.ListActive(context.Background(), account.ID, 0, 10, "")
require.NoError(t, err)
assert.Equal(t, int64(0), total)
assert.Len(t, contacts, 0)
}
func TestContactService_ListActive_EmptyWhenNoContacts(t *testing.T) {
db, _, svc := setupContactService(t)
account := createTestAccount(t, db)
contacts, total, err := svc.ListActive(context.Background(), account.ID, 0, 10, "")
require.NoError(t, err)
assert.Equal(t, int64(0), total)
assert.Len(t, contacts, 0)
}
func TestContactService_Search_UsesSearchReader(t *testing.T) {
db, _, svc := setupContactService(t)
account := createTestAccount(t, db)
contact := &model.Contact{AccountID: account.ID, Name: "Meili Contact", Email: "meili@example.com"}
rejected := &model.Contact{AccountID: account.ID, Name: "DB Contact", Email: "db@example.com"}
require.NoError(t, db.Create(contact).Error)
require.NoError(t, db.Create(rejected).Error)
reader := &mockContactSearchReader{
results: []search.SearchResult{{Type: search.ResultTypeContact, ID: contact.ID, AccountID: account.ID}},
total: 1,
}
svc.SetSearchReader(reader)
contacts, total, err := svc.Search(context.Background(), account.ID, "meili", 0, 10, "", search.SearchModeILike)
require.NoError(t, err)
assert.Equal(t, int64(1), total)
require.Len(t, contacts, 1)
assert.Equal(t, contact.ID, contacts[0].ID)
assert.Equal(t, "meili", reader.query)
require.NotNil(t, reader.filter)
assert.Equal(t, []search.SearchResultType{search.ResultTypeContact}, reader.filter.Types)
assert.Equal(t, 1, reader.filter.Page)
assert.Equal(t, 10, reader.filter.PerPage)
}
func TestContactService_ListActive_Pagination(t *testing.T) {
db, _, svc := setupContactService(t)
account := createTestAccount(t, db)
now := time.Now().Unix()
// Create 3 active contacts
for i := 0; i < 3; i++ {
c := &model.Contact{
AccountID: account.ID,
Name: fmt.Sprintf("Contact %c", 'A'+i),
Email: fmt.Sprintf("page%c@test.com", 'A'+i),
LastActivityAt: &now,
}
require.NoError(t, db.Create(c).Error)
}
// Get first page with limit=2
contacts, total, err := svc.ListActive(context.Background(), account.ID, 0, 2, "")
require.NoError(t, err)
assert.Equal(t, int64(3), total)
assert.Len(t, contacts, 2)
// Get second page
contacts2, total2, err := svc.ListActive(context.Background(), account.ID, 2, 2, "")
require.NoError(t, err)
assert.Equal(t, int64(3), total2)
assert.Len(t, contacts2, 1)
}
// ========== ExportCSV ==========
func TestContactService_ExportCSV_WritesCorrectHeadersAndRows(t *testing.T) {
db, _, svc := setupContactService(t)
account := createTestAccount(t, db)
// Create a contact
contact := &model.Contact{
AccountID: account.ID,
Name: "Export Contact",
Email: "export@test.com",
PhoneNumber: "+1234567890",
Identifier: "id123",
CountryCode: "US",
Location: "New York",
ContactType: "visitor",
Blocked: false,
}
require.NoError(t, db.Create(contact).Error)
var buf bytes.Buffer
err := svc.ExportCSV(context.Background(), account.ID, &buf)
require.NoError(t, err)
csvOutput := strings.TrimPrefix(buf.String(), "\ufeff")
lines := strings.Split(csvOutput, "\n")
// Verify header
assert.Equal(t, "id,name,email,phone_number,labels", lines[0])
// Verify data row contains the contact info
dataLine := lines[1]
assert.Contains(t, dataLine, "Export Contact")
assert.Contains(t, dataLine, "export@test.com")
assert.Contains(t, dataLine, "+1234567890")
}
func TestContactService_ExportCSV_EmptyAccount(t *testing.T) {
db, _, svc := setupContactService(t)
account := createTestAccount(t, db)
var buf bytes.Buffer
err := svc.ExportCSV(context.Background(), account.ID, &buf)
require.NoError(t, err)
csvOutput := strings.TrimPrefix(buf.String(), "\ufeff")
lines := strings.Split(csvOutput, "\n")
// Should have header only (plus trailing empty line from csv writer)
assert.Equal(t, "id,name,email,phone_number,labels", lines[0])
// No data rows
assert.Empty(t, strings.TrimSpace(lines[1]))
}
func TestContactService_ExportCSV_MultipleContacts(t *testing.T) {
db, _, svc := setupContactService(t)
account := createTestAccount(t, db)
for i := 0; i < 3; i++ {
c := &model.Contact{
AccountID: account.ID,
Name: fmt.Sprintf("Multi Contact %c", 'A'+i),
Email: fmt.Sprintf("multi%c@test.com", 'A'+i),
}
require.NoError(t, db.Create(c).Error)
}
var buf bytes.Buffer
err := svc.ExportCSV(context.Background(), account.ID, &buf)
require.NoError(t, err)
csvOutput := buf.String()
lines := strings.Split(strings.TrimSpace(csvOutput), "\n")
// Header + 3 data rows
assert.Len(t, lines, 4)
}
func TestContactService_ExportContacts_PersistsArtifactAndNotification(t *testing.T) {
db, _, svc := setupContactService(t)
account := createTestAccount(t, db)
user := createTestUser(t, db, account.ID)
mailer := &fakeContactExportMailer{}
svc.SetContactExportMailer(mailer)
contact := &model.Contact{AccountID: account.ID, Name: "Export Alice", Email: "alice@example.com", PhoneNumber: "+111"}
require.NoError(t, db.Create(contact).Error)
tag := &model.Tag{AccountID: account.ID, Name: "vip"}
require.NoError(t, db.Create(tag).Error)
require.NoError(t, db.Create(&model.ContactLabel{AccountID: account.ID, ContactID: contact.ID, TagID: tag.ID}).Error)
export, err := svc.ExportContacts(context.Background(), account.ID, user.ID, ContactExportRequest{})
require.NoError(t, err)
assert.Equal(t, string(model.DataImportStatusCompleted), export.Status)
assert.Equal(t, 1, export.RowCount)
assert.Contains(t, export.FileName, "contacts.csv")
assert.Contains(t, export.FileURL, "/contacts/export/")
assert.Contains(t, string(export.CSVData), "id,name,email,phone_number,labels")
assert.Contains(t, string(export.CSVData), "alice@example.com")
assert.Contains(t, string(export.CSVData), "vip")
var notification model.Notification
require.NoError(t, db.Where("user_id = ? AND notification_type = ?", user.ID, "contacts_export_complete").First(&notification).Error)
assert.Equal(t, "ContactExport", notification.PrimaryActorType)
assert.Equal(t, export.ID, notification.PrimaryActorID)
assert.True(t, notification.EmailEnabled)
assert.True(t, mailer.called)
assert.Equal(t, user.Email, mailer.toEmail)
assert.Equal(t, export.FileURL, mailer.fileURL)
assert.Equal(t, contactExportCompleteSubject, mailer.subject)
}
func TestContactService_ExportContacts_QueuesDurableArtifactGeneration(t *testing.T) {
db, _, svc := setupContactService(t)
account := createTestAccount(t, db)
user := createTestUser(t, db, account.ID)
mailer := &fakeContactExportMailer{}
svc.SetContactExportMailer(mailer)
wp := worker.NewWorkerPoolWithOptions(db, worker.WithNow(func() time.Time { return time.Date(2026, 6, 5, 18, 0, 0, 0, time.UTC) }))
svc.SetWorkerPool(wp)
contact := &model.Contact{AccountID: account.ID, Name: "Queued Alice", Email: "queued@example.com"}
require.NoError(t, db.Create(contact).Error)
export, err := svc.ExportContacts(context.Background(), account.ID, user.ID, ContactExportRequest{ColumnNames: []string{"email"}})
require.NoError(t, err)
assert.Equal(t, string(model.DataImportStatusPending), export.Status)
assert.Empty(t, export.CSVData)
assert.False(t, mailer.called)
var jobCount int64
require.NoError(t, db.Model(&model.BackgroundJob{}).Where("job_type = ? AND queue = ? AND status = ?", TaskTypeContactExport, "low", model.BackgroundJobStatusQueued).Count(&jobCount).Error)
assert.Equal(t, int64(1), jobCount)
processed, err := wp.ProcessOne(context.Background())
require.NoError(t, err)
assert.True(t, processed)
var completed model.ContactExport
require.NoError(t, db.First(&completed, export.ID).Error)
assert.Equal(t, string(model.DataImportStatusCompleted), completed.Status)
assert.Equal(t, 1, completed.RowCount)
assert.Contains(t, string(completed.CSVData), "email")
assert.Contains(t, string(completed.CSVData), "queued@example.com")
assert.Contains(t, completed.FileURL, fmt.Sprintf("/contacts/export/%d/download", completed.ID))
assert.True(t, mailer.called)
assert.Equal(t, 1, mailer.calls)
assert.Equal(t, user.Email, mailer.toEmail)
_, err = wp.Enqueue(context.Background(), TaskTypeContactExport, contactExportJob{ExportID: export.ID}, worker.WithQueue("low"))
require.NoError(t, err)
processed, err = wp.ProcessOne(context.Background())
require.NoError(t, err)
assert.True(t, processed)
var notificationCount int64
require.NoError(t, db.Model(&model.Notification{}).Where("primary_actor_type = ? AND primary_actor_id = ?", "ContactExport", export.ID).Count(&notificationCount).Error)
assert.Equal(t, int64(1), notificationCount)
assert.Equal(t, 1, mailer.calls)
}
func TestContactService_ContactExportJobRetriesMissingExport(t *testing.T) {
db, _, svc := setupContactService(t)
wp := worker.NewWorkerPoolWithOptions(db, worker.WithNow(func() time.Time { return time.Date(2026, 6, 5, 18, 15, 0, 0, time.UTC) }), worker.WithBackoff(func(attempt int) time.Duration { return time.Minute }))
svc.SetWorkerPool(wp)
_, err := wp.Enqueue(context.Background(), TaskTypeContactExport, contactExportJob{ExportID: 9999}, worker.WithQueue("low"), worker.WithMaxAttempts(3))
require.NoError(t, err)
processed, err := wp.ProcessOne(context.Background())
if err == nil || !processed {
t.Fatalf("expected missing contact export to retry, processed=%v err=%v", processed, err)
}
var job model.BackgroundJob
require.NoError(t, db.Where("job_type = ?", TaskTypeContactExport).First(&job).Error)
assert.Equal(t, model.BackgroundJobStatusRetrying, job.Status)
assert.NotEmpty(t, job.LastError)
}
func TestContactService_ImportContacts_QueuesDurableDataImport(t *testing.T) {
db, _, svc := setupContactService(t)
account := createTestAccount(t, db)
userID := uint(42)
require.NoError(t, db.Create(&model.Tag{AccountID: account.ID, Name: "vip"}).Error)
wp := worker.NewWorkerPoolWithOptions(db, worker.WithNow(func() time.Time { return time.Date(2026, 6, 7, 9, 0, 0, 0, time.UTC) }))
svc.SetWorkerPool(wp)
dataImport, err := svc.ImportContacts(context.Background(), account.ID, userID, strings.NewReader("name,email,labels\nQueued,queued@test.com,vip\n"))
require.NoError(t, err)
assert.Equal(t, "contacts", dataImport.DataType)
assert.Equal(t, string(model.DataImportStatusPending), dataImport.Status)
assert.NotEmpty(t, dataImport.ImportConfig)
require.NotNil(t, dataImport.UserID)
assert.Equal(t, userID, *dataImport.UserID)
var contactCount int64
require.NoError(t, db.Model(&model.Contact{}).Where("account_id = ? AND email = ?", account.ID, "queued@test.com").Count(&contactCount).Error)
assert.Equal(t, int64(0), contactCount)
var jobCount int64
require.NoError(t, db.Model(&model.BackgroundJob{}).Where("job_type = ? AND queue = ? AND status = ?", TaskTypeContactImport, "low", model.BackgroundJobStatusQueued).Count(&jobCount).Error)
assert.Equal(t, int64(1), jobCount)
processed, err := wp.ProcessOne(context.Background())
require.NoError(t, err)
assert.True(t, processed)
var completed model.DataImport
require.NoError(t, db.First(&completed, dataImport.ID).Error)
assert.Equal(t, string(model.DataImportStatusCompleted), completed.Status)
assert.Equal(t, 1, completed.TotalRecords)
assert.Equal(t, 1, completed.ProcessedRecords)
require.NoError(t, db.Model(&model.Contact{}).Where("account_id = ? AND email = ?", account.ID, "queued@test.com").Count(&contactCount).Error)
assert.Equal(t, int64(1), contactCount)
}
func TestContactService_ContactImportJobRetriesMissingImport(t *testing.T) {
db, _, svc := setupContactService(t)
wp := worker.NewWorkerPoolWithOptions(db, worker.WithNow(func() time.Time { return time.Date(2026, 6, 7, 9, 15, 0, 0, time.UTC) }), worker.WithBackoff(func(attempt int) time.Duration { return time.Minute }))
svc.SetWorkerPool(wp)
_, err := wp.Enqueue(context.Background(), TaskTypeContactImport, contactImportJob{ImportID: 9999}, worker.WithQueue("low"), worker.WithMaxAttempts(3))
require.NoError(t, err)
processed, err := wp.ProcessOne(context.Background())
if err == nil || !processed {
t.Fatalf("expected missing contact import to retry, processed=%v err=%v", processed, err)
}
var job model.BackgroundJob
require.NoError(t, db.Where("job_type = ?", TaskTypeContactImport).First(&job).Error)
assert.Equal(t, model.BackgroundJobStatusRetrying, job.Status)
assert.NotEmpty(t, job.LastError)
}
func TestContactService_ExportContacts_FiltersByLabelAndColumns(t *testing.T) {
db, _, svc := setupContactService(t)
account := createTestAccount(t, db)
keep := &model.Contact{AccountID: account.ID, Name: "Keep", Email: "keep@example.com"}
drop := &model.Contact{AccountID: account.ID, Name: "Drop", Email: "drop@example.com"}
require.NoError(t, db.Create(keep).Error)
require.NoError(t, db.Create(drop).Error)
tag := &model.Tag{AccountID: account.ID, Name: "vip"}
require.NoError(t, db.Create(tag).Error)
require.NoError(t, db.Create(&model.ContactLabel{AccountID: account.ID, ContactID: keep.ID, TagID: tag.ID}).Error)
csvData, rowCount, err := svc.GenerateContactExportCSV(context.Background(), account.ID, ContactExportRequest{
ColumnNames: []string{"email", "labels", "bogus", "email"},
Label: "vip",
})
require.NoError(t, err)
assert.Equal(t, 1, rowCount)
output := strings.TrimPrefix(string(csvData), "\ufeff")
assert.Contains(t, output, "email,labels")
assert.Contains(t, output, "keep@example.com,vip")
assert.NotContains(t, output, "drop@example.com")
assert.NotContains(t, output, "bogus")
}
// ========== ImportCSV ==========
func TestContactService_ImportCSV_ImportsValidRows(t *testing.T) {
db, _, svc := setupContactService(t)
indexer := &mockServiceSearchIndexer{}
svc.SetSearchIndexer(indexer)
account := createTestAccount(t, db)
csvData := "name,email,phone_number\nAlice,alice@test.com,+111\nBob,bob@test.com,+222\n"
result, err := svc.ImportCSV(context.Background(), account.ID, strings.NewReader(csvData))
require.NoError(t, err)
assert.Equal(t, 2, result.Imported)
assert.Equal(t, 0, result.Skipped)
assert.Equal(t, 0, result.Failed)
// Verify contacts were created
var count int64
db.Model(&model.Contact{}).Where("account_id = ?", account.ID).Count(&count)
assert.Equal(t, int64(2), count)
assert.Equal(t, []string{"contact", "contact"}, indexer.indexed)
}
func TestContactService_ImportCSV_AllowsRowsWithoutNameWhenIdentityPresent(t *testing.T) {
db, _, svc := setupContactService(t)
account := createTestAccount(t, db)
csvData := "name,email\nAlice,alice@test.com\n,bob@test.com\n"
result, err := svc.ImportCSV(context.Background(), account.ID, strings.NewReader(csvData))
require.NoError(t, err)
assert.Equal(t, 2, result.Imported)
assert.Equal(t, 0, result.Skipped)
assert.Equal(t, 0, result.Failed)
}
func TestContactService_ImportCSV_MergesDuplicateEmail(t *testing.T) {
db, _, svc := setupContactService(t)
indexer := &mockServiceSearchIndexer{}
svc.SetSearchIndexer(indexer)
account := createTestAccount(t, db)
// Pre-create a contact with the same email
existing := &model.Contact{
AccountID: account.ID,
Name: "Existing",
Email: "dup@test.com",
}
require.NoError(t, db.Create(existing).Error)
csvData := "name,email\nNewDup,dup@test.com\nUnique,unique@test.com\n"
result, err := svc.ImportCSV(context.Background(), account.ID, strings.NewReader(csvData))
require.NoError(t, err)
assert.Equal(t, 2, result.Imported)
assert.Equal(t, 0, result.Skipped)
assert.Equal(t, 0, result.Failed)
require.NoError(t, db.First(existing, existing.ID).Error)
assert.Equal(t, "NewDup", existing.Name)
var count int64
db.Model(&model.Contact{}).Where("account_id = ?", account.ID).Count(&count)
assert.Equal(t, int64(2), count)
assert.Equal(t, []string{"contact", "contact"}, indexer.indexed)
}
func TestContactService_ImportContacts_CreatesCompletedDataImport(t *testing.T) {
db, _, svc := setupContactService(t)
account := createTestAccount(t, db)
userID := uint(42)
require.NoError(t, db.Create(&model.Tag{AccountID: account.ID, Name: "vip"}).Error)
csvData := "name,email,labels\nAlice,alice@test.com,vip\n"
dataImport, err := svc.ImportContacts(context.Background(), account.ID, userID, strings.NewReader(csvData))
require.NoError(t, err)
assert.Equal(t, "contacts", dataImport.DataType)
assert.Equal(t, string(model.DataImportStatusCompleted), dataImport.Status)
assert.Equal(t, 1, dataImport.TotalRecords)
assert.Equal(t, 1, dataImport.ProcessedRecords)
assert.NotNil(t, dataImport.UserID)
assert.Equal(t, userID, *dataImport.UserID)
var labelCount int64
db.Model(&model.ContactLabel{}).Where("account_id = ?", account.ID).Count(&labelCount)
assert.Equal(t, int64(1), labelCount)
}
func TestContactService_ImportContacts_RejectsUnknownLabels(t *testing.T) {
db, _, svc := setupContactService(t)
account := createTestAccount(t, db)
require.NoError(t, db.Create(&model.Tag{AccountID: account.ID, Name: "vip"}).Error)
csvData := "name,email,labels\nAlice,alice@test.com,vip\nBob,bob@test.com,unknown\n"
dataImport, err := svc.ImportContacts(context.Background(), account.ID, 0, strings.NewReader(csvData))
require.NoError(t, err)
assert.Equal(t, string(model.DataImportStatusCompleted), dataImport.Status)
assert.Equal(t, 2, dataImport.TotalRecords)
assert.Equal(t, 1, dataImport.ProcessedRecords)
assert.Equal(t, 1, dataImport.FailedRecords)
var alice model.Contact
require.NoError(t, db.Where("account_id = ? AND email = ?", account.ID, "alice@test.com").First(&alice).Error)
var bobCount int64
db.Model(&model.Contact{}).Where("account_id = ? AND email = ?", account.ID, "bob@test.com").Count(&bobCount)
assert.Equal(t, int64(0), bobCount)
var tagCount int64
db.Model(&model.Tag{}).Where("account_id = ? AND name = ?", account.ID, "unknown").Count(&tagCount)
assert.Equal(t, int64(0), tagCount)
}
func TestContactService_ImportCSV_FailsOnBadCSV(t *testing.T) {
db, _, svc := setupContactService(t)
account := createTestAccount(t, db)
// Empty input should fail on header read
result, err := svc.ImportCSV(context.Background(), account.ID, strings.NewReader(""))
assert.Error(t, err)
assert.Nil(t, result)
}
func TestContactService_ImportCSV_AllColumns(t *testing.T) {
db, _, svc := setupContactService(t)
account := createTestAccount(t, db)
csvData := "name,email,phone_number,identifier,country_code,location,contact_type\nFull,full@test.com,+999,fid001,DE,Berlin,visitor\n"
result, err := svc.ImportCSV(context.Background(), account.ID, strings.NewReader(csvData))
require.NoError(t, err)
assert.Equal(t, 1, result.Imported)
// Verify all fields were imported
var contact model.Contact
require.NoError(t, db.Where("account_id = ? AND email = ?", account.ID, "full@test.com").First(&contact).Error)
assert.Equal(t, "Full", contact.Name)
assert.Equal(t, "+999", contact.PhoneNumber)
assert.Equal(t, "fid001", contact.Identifier)
assert.Equal(t, "DE", contact.CountryCode)
assert.Equal(t, "Berlin", contact.Location)
assert.Equal(t, "lead", contact.ContactType) // visitor upgraded to lead by BeforeSave (has email+phone)
}
// ========== DeleteCustomAttributes ==========
func TestContactService_DeleteCustomAttributes_ClearsCustomAttributes(t *testing.T) {
db, _, svc := setupContactService(t)
account := createTestAccount(t, db)
// Create a contact with custom attributes
contact := &model.Contact{
AccountID: account.ID,
Name: "Custom Attr Contact",
Email: "custom@test.com",
CustomAttributes: datatypes.JSON(`{"key1":"val1","key2":"val2"}`),
}
require.NoError(t, db.Create(contact).Error)
err := svc.DeleteCustomAttributes(context.Background(), account.ID, contact.ID)
require.NoError(t, err)
// Verify custom_attributes is now empty
var updated model.Contact
require.NoError(t, db.First(&updated, contact.ID).Error)
assert.Equal(t, datatypes.JSON("{}"), updated.CustomAttributes)
}
func TestContactService_DeleteCustomAttributes_ContactNotFound(t *testing.T) {
db, _, svc := setupContactService(t)
account := createTestAccount(t, db)
err := svc.DeleteCustomAttributes(context.Background(), account.ID, 99999)
assert.Error(t, err)
assert.Equal(t, "contact not found", err.Error())
}
func TestContactService_DeleteCustomAttributes_WrongAccount(t *testing.T) {
db, _, svc := setupContactService(t)
account1 := createTestAccountWithName(t, db, "Account 1")
account2 := createTestAccountWithName(t, db, "Account 2")
// Contact belongs to account1
contact := &model.Contact{
AccountID: account1.ID,
Name: "Scoped Contact",
Email: "scoped@test.com",
CustomAttributes: datatypes.JSON(`{"key":"val"}`),
}
require.NoError(t, db.Create(contact).Error)
// Try to delete custom attrs using account2 — should fail
err := svc.DeleteCustomAttributes(context.Background(), account2.ID, contact.ID)
assert.Error(t, err)
assert.Equal(t, "contact not found", err.Error())
}
// ========== GetContactableInboxes ==========
func TestContactService_GetContactableInboxes_ReturnsExistingInboxes(t *testing.T) {
db, _, svc := setupContactService(t)
account := createTestAccount(t, db)
contact := createTestContact(t, db, account.ID)
inbox := createTestInbox(t, db, account.ID, "web_widget")
// Create a contact_inbox association
ci := &model.ContactInbox{
ContactID: contact.ID,
InboxID: inbox.ID,
SourceID: "src_test",
HMACToken: "hmac_test",
PubsubToken: "pubsub_test",
}
require.NoError(t, db.Create(ci).Error)
result, err := svc.GetContactableInboxes(context.Background(), account.ID, contact.ID)
require.NoError(t, err)
assert.Len(t, result, 1)
assert.Equal(t, inbox.ID, result[0].Inbox.ID)
assert.Equal(t, "web_widget", result[0].Inbox.ChannelType)
assert.Equal(t, ci.ID, result[0].ContactInbox.ID)
assert.Equal(t, "src_test", result[0].SourceID)
}
func TestContactService_GetContactableInboxes_MultipleInboxes(t *testing.T) {
db, _, svc := setupContactService(t)
account := createTestAccount(t, db)
contact := createTestContact(t, db, account.ID)
inbox1 := createTestInbox(t, db, account.ID, "web_widget")
inbox2 := createTestInbox(t, db, account.ID, "api")
// Create two contact_inbox associations
ci1 := &model.ContactInbox{
ContactID: contact.ID,
InboxID: inbox1.ID,
SourceID: "src_web",
HMACToken: "hmac1",
PubsubToken: "pubsub1",
}
ci2 := &model.ContactInbox{
ContactID: contact.ID,
InboxID: inbox2.ID,
SourceID: "src_api",
HMACToken: "hmac2",
PubsubToken: "pubsub2",
}
require.NoError(t, db.Create(ci1).Error)
require.NoError(t, db.Create(ci2).Error)
result, err := svc.GetContactableInboxes(context.Background(), account.ID, contact.ID)
require.NoError(t, err)
assert.Len(t, result, 2)
// Check both inboxes are present
inboxTypes := map[string]bool{}
for _, r := range result {
inboxTypes[r.Inbox.ChannelType] = true
}
assert.True(t, inboxTypes["web_widget"])
assert.True(t, inboxTypes["api"])
}
func TestContactService_GetContactableInboxes_ContactNotFound(t *testing.T) {
db, _, svc := setupContactService(t)
account := createTestAccount(t, db)
result, err := svc.GetContactableInboxes(context.Background(), account.ID, 99999)
assert.Error(t, err)
assert.Equal(t, "contact not found", err.Error())
assert.Nil(t, result)
}
func TestContactService_GetContactableInboxes_WrongAccount(t *testing.T) {
db, _, svc := setupContactService(t)
account1 := createTestAccountWithName(t, db, "Account 1")
account2 := createTestAccountWithName(t, db, "Account 2")
contact := createTestContact(t, db, account1.ID)
// Try with wrong account
result, err := svc.GetContactableInboxes(context.Background(), account2.ID, contact.ID)
assert.Error(t, err)
assert.Equal(t, "contact not found", err.Error())
assert.Nil(t, result)
}
func TestContactService_GetContactableInboxes_NoInboxes(t *testing.T) {
db, _, svc := setupContactService(t)
account := createTestAccount(t, db)
contact := createTestContact(t, db, account.ID)
// No contact_inbox associations created
result, err := svc.GetContactableInboxes(context.Background(), account.ID, contact.ID)
require.NoError(t, err)
assert.Len(t, result, 0)
}