707 lines
26 KiB
Go
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(¬ification).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(¬ificationCount).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)
|
|
}
|