Files
gochat/internal/canned/service_test.go
T

419 lines
14 KiB
Go

package canned
import (
"context"
"testing"
"github.com/gochat/gochat/internal/model"
)
func TestCannedResponseService_Create(t *testing.T) {
provider := setupCannedTestDBProvider(t)
accountID, _ := seedTestAccount(provider.DB(), t)
svc := NewCannedResponseService(provider)
cr := &CannedResponse{
AccountID: accountID,
ShortCode: "greeting",
Content: "Hello! How can I help you?",
}
if err := svc.Create(context.Background(), cr); err != nil {
t.Fatalf("Create failed: %v", err)
}
if cr.ID == 0 {
t.Fatal("expected ID to be set after Create")
}
if cr.ShortCode != "greeting" {
t.Fatalf("expected ShortCode 'greeting', got '%s'", cr.ShortCode)
}
if cr.Content != "Hello! How can I help you?" {
t.Fatalf("expected Content 'Hello! How can I help you?', got '%s'", cr.Content)
}
}
func TestCannedResponseService_Create_DuplicateShortCode(t *testing.T) {
provider := setupCannedTestDBProvider(t)
accountID, _ := seedTestAccount(provider.DB(), t)
svc := NewCannedResponseService(provider)
cr1 := &CannedResponse{
AccountID: accountID,
ShortCode: "greeting",
Content: "Hello!",
}
if err := svc.Create(context.Background(), cr1); err != nil {
t.Fatalf("first Create failed: %v", err)
}
// Same short_code within the same account should fail due to uniqueIndex.
cr2 := &CannedResponse{
AccountID: accountID,
ShortCode: "greeting",
Content: "Hi there!",
}
if err := svc.Create(context.Background(), cr2); err == nil {
t.Fatal("expected error on duplicate short_code, got nil")
}
}
func TestCannedResponseService_GetByID(t *testing.T) {
provider := setupCannedTestDBProvider(t)
accountID, _ := seedTestAccount(provider.DB(), t)
svc := NewCannedResponseService(provider)
cr := &CannedResponse{
AccountID: accountID,
ShortCode: "thanks",
Content: "Thank you for contacting us!",
}
if err := svc.Create(context.Background(), cr); err != nil {
t.Fatalf("Create failed: %v", err)
}
found, err := svc.GetByID(context.Background(), cr.ID)
if err != nil {
t.Fatalf("GetByID failed: %v", err)
}
if found.ID != cr.ID {
t.Fatalf("expected ID %d, got %d", cr.ID, found.ID)
}
if found.ShortCode != "thanks" {
t.Fatalf("expected ShortCode 'thanks', got '%s'", found.ShortCode)
}
if found.Content != "Thank you for contacting us!" {
t.Fatalf("expected Content 'Thank you for contacting us!', got '%s'", found.Content)
}
}
func TestCannedResponseService_GetByID_NotFound(t *testing.T) {
provider := setupCannedTestDBProvider(t)
svc := NewCannedResponseService(provider)
_, err := svc.GetByID(context.Background(), 9999)
if err == nil {
t.Fatal("expected error for non-existent ID, got nil")
}
}
func TestCannedResponseService_GetByAccountAndID(t *testing.T) {
provider := setupCannedTestDBProvider(t)
accountID, _ := seedTestAccount(provider.DB(), t)
otherAccount := &model.Account{Name: "Other Scoped Account"}
if err := provider.DB().Create(otherAccount).Error; err != nil {
t.Fatalf("failed to create other account: %v", err)
}
svc := NewCannedResponseService(provider)
cr := &CannedResponse{AccountID: accountID, ShortCode: "scoped", Content: "Scoped content"}
if err := svc.Create(context.Background(), cr); err != nil {
t.Fatalf("Create failed: %v", err)
}
found, err := svc.GetByAccountAndID(context.Background(), accountID, cr.ID)
if err != nil {
t.Fatalf("GetByAccountAndID failed: %v", err)
}
if found.ID != cr.ID {
t.Fatalf("expected ID %d, got %d", cr.ID, found.ID)
}
if _, err := svc.GetByAccountAndID(context.Background(), otherAccount.ID, cr.ID); err == nil {
t.Fatal("expected cross-account lookup to fail")
}
}
func TestCannedResponseService_Update(t *testing.T) {
provider := setupCannedTestDBProvider(t)
accountID, _ := seedTestAccount(provider.DB(), t)
svc := NewCannedResponseService(provider)
cr := &CannedResponse{
AccountID: accountID,
ShortCode: "bye",
Content: "Goodbye!",
}
if err := svc.Create(context.Background(), cr); err != nil {
t.Fatalf("Create failed: %v", err)
}
updates := map[string]interface{}{
"short_code": "farewell",
"content": "Farewell and thank you!",
}
if err := svc.Update(context.Background(), cr.ID, updates); err != nil {
t.Fatalf("Update failed: %v", err)
}
found, err := svc.GetByID(context.Background(), cr.ID)
if err != nil {
t.Fatalf("GetByID after Update failed: %v", err)
}
if found.ShortCode != "farewell" {
t.Fatalf("expected ShortCode 'farewell', got '%s'", found.ShortCode)
}
if found.Content != "Farewell and thank you!" {
t.Fatalf("expected Content 'Farewell and thank you!', got '%s'", found.Content)
}
}
func TestCannedResponseService_UpdateByAccountAndID(t *testing.T) {
provider := setupCannedTestDBProvider(t)
accountID, _ := seedTestAccount(provider.DB(), t)
otherAccount := &model.Account{Name: "Other Update Account"}
if err := provider.DB().Create(otherAccount).Error; err != nil {
t.Fatalf("failed to create other account: %v", err)
}
svc := NewCannedResponseService(provider)
cr := &CannedResponse{AccountID: accountID, ShortCode: "account_update", Content: "Original"}
if err := svc.Create(context.Background(), cr); err != nil {
t.Fatalf("Create failed: %v", err)
}
if err := svc.UpdateByAccountAndID(context.Background(), otherAccount.ID, cr.ID, map[string]interface{}{"content": "Wrong"}); err == nil {
t.Fatal("expected cross-account update to fail")
}
if err := svc.UpdateByAccountAndID(context.Background(), accountID, cr.ID, map[string]interface{}{"content": "Updated"}); err != nil {
t.Fatalf("UpdateByAccountAndID failed: %v", err)
}
found, err := svc.GetByAccountAndID(context.Background(), accountID, cr.ID)
if err != nil {
t.Fatalf("GetByAccountAndID after update failed: %v", err)
}
if found.Content != "Updated" {
t.Fatalf("expected updated content, got %q", found.Content)
}
}
func TestCannedResponseService_Delete(t *testing.T) {
provider := setupCannedTestDBProvider(t)
accountID, _ := seedTestAccount(provider.DB(), t)
svc := NewCannedResponseService(provider)
cr := &CannedResponse{
AccountID: accountID,
ShortCode: "hello",
Content: "Hello!",
}
if err := svc.Create(context.Background(), cr); err != nil {
t.Fatalf("Create failed: %v", err)
}
if err := svc.Delete(context.Background(), cr.ID); err != nil {
t.Fatalf("Delete failed: %v", err)
}
// After soft delete, GetByID should return error.
_, err := svc.GetByID(context.Background(), cr.ID)
if err == nil {
t.Fatal("expected error after Delete, got nil (soft-deleted record should not be found)")
}
}
func TestCannedResponseService_DeleteByAccountAndID(t *testing.T) {
provider := setupCannedTestDBProvider(t)
accountID, _ := seedTestAccount(provider.DB(), t)
otherAccount := &model.Account{Name: "Other Delete Account"}
if err := provider.DB().Create(otherAccount).Error; err != nil {
t.Fatalf("failed to create other account: %v", err)
}
svc := NewCannedResponseService(provider)
cr := &CannedResponse{AccountID: accountID, ShortCode: "account_delete", Content: "Delete"}
if err := svc.Create(context.Background(), cr); err != nil {
t.Fatalf("Create failed: %v", err)
}
if err := svc.DeleteByAccountAndID(context.Background(), otherAccount.ID, cr.ID); err == nil {
t.Fatal("expected cross-account delete to fail")
}
if _, err := svc.GetByAccountAndID(context.Background(), accountID, cr.ID); err != nil {
t.Fatalf("record should still exist after failed cross-account delete: %v", err)
}
if err := svc.DeleteByAccountAndID(context.Background(), accountID, cr.ID); err != nil {
t.Fatalf("DeleteByAccountAndID failed: %v", err)
}
if _, err := svc.GetByAccountAndID(context.Background(), accountID, cr.ID); err == nil {
t.Fatal("expected account-scoped lookup to fail after delete")
}
recreated := &CannedResponse{AccountID: accountID, ShortCode: "account_delete", Content: "Recreated"}
if err := svc.Create(context.Background(), recreated); err != nil {
t.Fatalf("expected hard-deleted short_code to be reusable: %v", err)
}
}
func TestCannedResponseService_ListByAccount(t *testing.T) {
provider := setupCannedTestDBProvider(t)
accountID, _ := seedTestAccount(provider.DB(), t)
svc := NewCannedResponseService(provider)
// Seed multiple canned responses.
items := []CannedResponse{
{AccountID: accountID, ShortCode: "alpha", Content: "Alpha content"},
{AccountID: accountID, ShortCode: "beta", Content: "Beta content"},
{AccountID: accountID, ShortCode: "gamma", Content: "Gamma content"},
}
for i := range items {
if err := svc.Create(context.Background(), &items[i]); err != nil {
t.Fatalf("Create #%d failed: %v", i, err)
}
}
// Create a second account with its own canned response — should not appear.
db := provider.DB()
account2 := &model.Account{Name: "Other Account"}
if err := db.Create(account2).Error; err != nil {
t.Fatalf("failed to create second account: %v", err)
}
crOther := &CannedResponse{
AccountID: account2.ID,
ShortCode: "other",
Content: "Other account content",
}
if err := svc.Create(context.Background(), crOther); err != nil {
t.Fatalf("Create other-account canned response failed: %v", err)
}
results, err := svc.ListByAccount(context.Background(), accountID)
if err != nil {
t.Fatalf("ListByAccount failed: %v", err)
}
if len(results) != 3 {
t.Fatalf("expected 3 results, got %d", len(results))
}
// Results should be ordered by short_code ASC.
if results[0].ShortCode != "alpha" {
t.Fatalf("expected first result ShortCode 'alpha', got '%s'", results[0].ShortCode)
}
if results[1].ShortCode != "beta" {
t.Fatalf("expected second result ShortCode 'beta', got '%s'", results[1].ShortCode)
}
if results[2].ShortCode != "gamma" {
t.Fatalf("expected third result ShortCode 'gamma', got '%s'", results[2].ShortCode)
}
}
func TestCannedResponseService_Search(t *testing.T) {
provider := setupCannedTestDBProvider(t)
accountID, _ := seedTestAccount(provider.DB(), t)
svc := NewCannedResponseService(provider)
items := []CannedResponse{
{AccountID: accountID, ShortCode: "greet_hi", Content: "Hi there! Welcome."},
{AccountID: accountID, ShortCode: "greet_hello", Content: "Hello! How can we help?"},
{AccountID: accountID, ShortCode: "farewell", Content: "Goodbye and have a great day!"},
}
for i := range items {
if err := svc.Create(context.Background(), &items[i]); err != nil {
t.Fatalf("Create #%d failed: %v", i, err)
}
}
// Search for "greet" — should match short_code on first two items.
results, err := svc.Search(context.Background(), accountID, "greet")
if err != nil {
t.Fatalf("Search failed: %v", err)
}
// short_code matches should rank before content-only matches.
// Both "greet_hi" and "greet_hello" match on short_code.
if len(results) < 2 {
t.Fatalf("expected at least 2 results for 'greet', got %d", len(results))
}
// Verify that short_code match results come first.
for i := 0; i < 2; i++ {
if results[i].ShortCode != "greet_hi" && results[i].ShortCode != "greet_hello" {
t.Fatalf("expected short_code match in top results, got '%s'", results[i].ShortCode)
}
}
// Search for "great" — matches content in "farewell".
results2, err := svc.Search(context.Background(), accountID, "great")
if err != nil {
t.Fatalf("Search for 'great' failed: %v", err)
}
if len(results2) != 1 {
t.Fatalf("expected 1 result for 'great', got %d", len(results2))
}
if results2[0].ShortCode != "farewell" {
t.Fatalf("expected ShortCode 'farewell', got '%s'", results2[0].ShortCode)
}
// Empty query should return all (same as ListByAccount).
results3, err := svc.Search(context.Background(), accountID, "")
if err != nil {
t.Fatalf("Search with empty query failed: %v", err)
}
if len(results3) != 3 {
t.Fatalf("expected 3 results for empty query, got %d", len(results3))
}
}
func TestCannedResponseService_Search_RanksLikeChatwoot(t *testing.T) {
provider := setupCannedTestDBProvider(t)
accountID, _ := seedTestAccount(provider.DB(), t)
svc := NewCannedResponseService(provider)
items := []CannedResponse{
{AccountID: accountID, ShortCode: "body_only", Content: "Please mention help in the body"},
{AccountID: accountID, ShortCode: "say_help", Content: "Generic"},
{AccountID: accountID, ShortCode: "help_start", Content: "Generic"},
}
for i := range items {
if err := svc.Create(context.Background(), &items[i]); err != nil {
t.Fatalf("Create #%d failed: %v", i, err)
}
}
results, err := svc.Search(context.Background(), accountID, "help")
if err != nil {
t.Fatalf("Search failed: %v", err)
}
if len(results) != 3 {
t.Fatalf("expected 3 results, got %d", len(results))
}
if results[0].ShortCode != "help_start" || results[1].ShortCode != "say_help" || results[2].ShortCode != "body_only" {
t.Fatalf("unexpected ranking: %s, %s, %s", results[0].ShortCode, results[1].ShortCode, results[2].ShortCode)
}
}
func TestCannedResponseService_GetByShortCode(t *testing.T) {
provider := setupCannedTestDBProvider(t)
accountID, _ := seedTestAccount(provider.DB(), t)
svc := NewCannedResponseService(provider)
cr := &CannedResponse{
AccountID: accountID,
ShortCode: "welcome",
Content: "Welcome to our service!",
}
if err := svc.Create(context.Background(), cr); err != nil {
t.Fatalf("Create failed: %v", err)
}
found, err := svc.GetByShortCode(context.Background(), accountID, "welcome")
if err != nil {
t.Fatalf("GetByShortCode failed: %v", err)
}
if found.ID != cr.ID {
t.Fatalf("expected ID %d, got %d", cr.ID, found.ID)
}
if found.ShortCode != "welcome" {
t.Fatalf("expected ShortCode 'welcome', got '%s'", found.ShortCode)
}
// Non-existent short_code should return error.
_, err = svc.GetByShortCode(context.Background(), accountID, "nonexistent")
if err == nil {
t.Fatal("expected error for non-existent short_code, got nil")
}
}