Files
gochat/internal/service/contact_service_g3_test.go
T
2026-06-04 15:44:48 +08:00

419 lines
13 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"
)
// ========== 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_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 := buf.String()
lines := strings.Split(csvOutput, "\n")
// Verify header
assert.Equal(t, "id,name,email,phone_number,identifier,country_code,location,contact_type,blocked,created_at", 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")
assert.Contains(t, dataLine, "id123")
assert.Contains(t, dataLine, "US")
assert.Contains(t, dataLine, "New York")
assert.Contains(t, dataLine, "lead")
assert.Contains(t, dataLine, "false")
}
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 := buf.String()
lines := strings.Split(csvOutput, "\n")
// Should have header only (plus trailing empty line from csv writer)
assert.Equal(t, "id,name,email,phone_number,identifier,country_code,location,contact_type,blocked,created_at", 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)
}
// ========== ImportCSV ==========
func TestContactService_ImportCSV_ImportsValidRows(t *testing.T) {
db, _, svc := setupContactService(t)
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)
}
func TestContactService_ImportCSV_SkipsRowsWithoutName(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, 1, result.Imported)
assert.Equal(t, 1, result.Skipped) // row without name is skipped
assert.Equal(t, 0, result.Failed)
}
func TestContactService_ImportCSV_SkipsDuplicateEmail(t *testing.T) {
db, _, svc := setupContactService(t)
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, 1, result.Imported) // unique@test.com
assert.Equal(t, 1, result.Skipped) // dup@test.com is skipped
assert.Equal(t, 0, result.Failed)
}
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)
}