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

418 lines
14 KiB
Go

package service
import (
"context"
"encoding/json"
"fmt"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
"gorm.io/gorm/logger"
"github.com/gochat/gochat/internal/model"
"github.com/gochat/gochat/internal/repository"
)
// ========== Test Setup ==========
// setupInboxServiceTest creates a test DB and InboxService using SQLite in-memory.
// WhatsApp-specific repos (whatsappService, whatsappRepo) are nil since we
// test the non-WhatsApp paths exclusively (web_widget, api channel types).
func setupInboxServiceTest(t *testing.T) (*InboxService, *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.AgentBot{},
&model.Inbox{},
&model.AgentBotInbox{},
&model.WebhookSubscription{},
), "failed to auto-migrate")
t.Cleanup(func() {
sqlDB, _ := db.DB()
sqlDB.Close()
})
repo := repository.NewInboxRepo(db)
agentBotInboxRepo := repository.NewAgentBotInboxRepo(db)
agentBotRepo := repository.NewAgentBotRepo(db)
webhookSubRepo := repository.NewWebhookSubscriptionRepo(db)
// WhatsApp repos/services are nil — we only test non-WhatsApp paths
svc := NewInboxService(repo, agentBotInboxRepo, agentBotRepo, nil, webhookSubRepo, nil, nil)
return svc, db
}
// createInboxTestPrereqs creates prerequisite Account and Inbox for service tests.
func createInboxTestPrereqs(t *testing.T, db *gorm.DB, channelType string) (*model.Account, *model.Inbox) {
t.Helper()
account := &model.Account{Name: "InboxSvcTestOrg", Locale: "en", Active: true}
require.NoError(t, db.Create(account).Error)
inbox := &model.Inbox{
AccountID: account.ID,
Name: fmt.Sprintf("SvcTest%s", channelType),
ChannelType: channelType,
ChannelID: 1,
}
require.NoError(t, db.Create(inbox).Error)
return account, inbox
}
// createTestAgentBot creates an AgentBot for the given account.
func createTestAgentBot(t *testing.T, db *gorm.DB, accountID uint, suffix string) *model.AgentBot {
t.Helper()
bot := &model.AgentBot{
AccountID: &accountID,
Name: fmt.Sprintf("SvcBot-%s", suffix),
BotType: "default",
Secret: fmt.Sprintf("secret-svc-%s", suffix),
AccessToken: fmt.Sprintf("token-svc-%s", suffix),
}
require.NoError(t, db.Create(bot).Error)
return bot
}
// ========================================
// SetAgentBot service tests
// ========================================
func TestInboxService_SetAgentBot_AssignBot(t *testing.T) {
svc, db := setupInboxServiceTest(t)
account, inbox := createInboxTestPrereqs(t, db, "web_widget")
bot := createTestAgentBot(t, db, account.ID, "assign")
binding, err := svc.SetAgentBot(context.Background(), account.ID, inbox.ID, SetAgentBotRequest{
AgentBotID: bot.ID,
})
require.NoError(t, err)
assert.NotNil(t, binding)
assert.Equal(t, bot.ID, binding.AgentBotID)
assert.Equal(t, inbox.ID, binding.InboxID)
assert.Equal(t, model.AgentBotInboxActive, binding.Status)
}
func TestInboxService_SetAgentBot_RemoveBot(t *testing.T) {
svc, db := setupInboxServiceTest(t)
account, inbox := createInboxTestPrereqs(t, db, "web_widget")
bot := createTestAgentBot(t, db, account.ID, "remove")
// First assign a bot
binding, err := svc.SetAgentBot(context.Background(), account.ID, inbox.ID, SetAgentBotRequest{
AgentBotID: bot.ID,
})
require.NoError(t, err)
assert.NotNil(t, binding)
// Now remove it (agent_bot_id = 0)
result, err := svc.SetAgentBot(context.Background(), account.ID, inbox.ID, SetAgentBotRequest{
AgentBotID: 0,
})
require.NoError(t, err)
assert.Nil(t, result, "removing bot should return nil binding")
}
func TestInboxService_SetAgentBot_InboxNotFound(t *testing.T) {
svc, _ := setupInboxServiceTest(t)
binding, err := svc.SetAgentBot(context.Background(), 9999, 9999, SetAgentBotRequest{
AgentBotID: 1,
})
assert.Error(t, err)
assert.Nil(t, binding)
assert.Contains(t, err.Error(), "inbox not found")
}
func TestInboxService_SetAgentBot_BotNotFound(t *testing.T) {
svc, db := setupInboxServiceTest(t)
account, inbox := createInboxTestPrereqs(t, db, "web_widget")
binding, err := svc.SetAgentBot(context.Background(), account.ID, inbox.ID, SetAgentBotRequest{
AgentBotID: 9999,
})
assert.Error(t, err)
assert.Nil(t, binding)
assert.Contains(t, err.Error(), "agent bot not found")
}
func TestInboxService_SetAgentBot_ReassignBot(t *testing.T) {
svc, db := setupInboxServiceTest(t)
account, inbox := createInboxTestPrereqs(t, db, "web_widget")
bot1 := createTestAgentBot(t, db, account.ID, "first")
bot2 := createTestAgentBot(t, db, account.ID, "second")
// Assign first bot
binding1, err := svc.SetAgentBot(context.Background(), account.ID, inbox.ID, SetAgentBotRequest{
AgentBotID: bot1.ID,
})
require.NoError(t, err)
assert.Equal(t, bot1.ID, binding1.AgentBotID)
// Reassign to second bot — should deactivate bot1 and activate bot2
binding2, err := svc.SetAgentBot(context.Background(), account.ID, inbox.ID, SetAgentBotRequest{
AgentBotID: bot2.ID,
})
require.NoError(t, err)
assert.Equal(t, bot2.ID, binding2.AgentBotID)
assert.Equal(t, model.AgentBotInboxActive, binding2.Status)
}
func TestInboxService_SetAgentBot_ReactivateExistingBinding(t *testing.T) {
svc, db := setupInboxServiceTest(t)
account, inbox := createInboxTestPrereqs(t, db, "web_widget")
bot := createTestAgentBot(t, db, account.ID, "reactivate")
// Assign bot
_, err := svc.SetAgentBot(context.Background(), account.ID, inbox.ID, SetAgentBotRequest{
AgentBotID: bot.ID,
})
require.NoError(t, err)
// Remove bot (deactivate)
_, err = svc.SetAgentBot(context.Background(), account.ID, inbox.ID, SetAgentBotRequest{
AgentBotID: 0,
})
require.NoError(t, err)
// Re-assign same bot — should reactivate the existing binding
binding2, err := svc.SetAgentBot(context.Background(), account.ID, inbox.ID, SetAgentBotRequest{
AgentBotID: bot.ID,
})
require.NoError(t, err)
assert.NotNil(t, binding2)
assert.Equal(t, bot.ID, binding2.AgentBotID)
assert.Equal(t, model.AgentBotInboxActive, binding2.Status)
}
// ========================================
// Health service tests
// ========================================
func TestInboxService_Health_APIInbox_Healthy(t *testing.T) {
svc, db := setupInboxServiceTest(t)
account, inbox := createInboxTestPrereqs(t, db, "api")
// Set webhook_url and secret on the inbox (API inbox health checks both)
require.NoError(t, db.Model(inbox).Update("webhook_url", "https://example.com/hook").Error)
require.NoError(t, db.Model(inbox).Update("secret", "my-secret-key").Error)
result, err := svc.Health(context.Background(), account.ID, inbox.ID)
require.NoError(t, err)
assert.NotNil(t, result)
assert.Equal(t, inbox.ID, result.InboxID)
assert.Equal(t, "api", result.ChannelType)
assert.True(t, result.Healthy)
assert.Equal(t, "connected", result.Status)
}
func TestInboxService_Health_APIInbox_Misconfigured(t *testing.T) {
svc, db := setupInboxServiceTest(t)
account, inbox := createInboxTestPrereqs(t, db, "api")
// Leave webhook_url empty — should be misconfigured
result, err := svc.Health(context.Background(), account.ID, inbox.ID)
require.NoError(t, err)
assert.NotNil(t, result)
assert.False(t, result.Healthy)
assert.Equal(t, "misconfigured", result.Status)
}
func TestInboxService_Health_APIInbox_MissingSecret(t *testing.T) {
svc, db := setupInboxServiceTest(t)
account, inbox := createInboxTestPrereqs(t, db, "api")
// Set webhook_url but leave secret empty
require.NoError(t, db.Model(inbox).Update("webhook_url", "https://example.com/hook").Error)
// secret is empty by default
result, err := svc.Health(context.Background(), account.ID, inbox.ID)
require.NoError(t, err)
assert.False(t, result.Healthy)
assert.Equal(t, "misconfigured", result.Status)
assert.Contains(t, result.Details, "secret")
}
func TestInboxService_Health_APIInbox_FullyConfigured(t *testing.T) {
svc, db := setupInboxServiceTest(t)
account, inbox := createInboxTestPrereqs(t, db, "api")
// Set both webhook_url and secret
require.NoError(t, db.Model(inbox).Updates(map[string]interface{}{
"webhook_url": "https://example.com/hook",
"secret": "my-hmac-secret",
}).Error)
result, err := svc.Health(context.Background(), account.ID, inbox.ID)
require.NoError(t, err)
assert.True(t, result.Healthy)
assert.Equal(t, "connected", result.Status)
}
func TestInboxService_Health_WidgetInbox(t *testing.T) {
svc, db := setupInboxServiceTest(t)
account, inbox := createInboxTestPrereqs(t, db, "web_widget")
result, err := svc.Health(context.Background(), account.ID, inbox.ID)
require.NoError(t, err)
assert.NotNil(t, result)
assert.True(t, result.Healthy)
assert.Equal(t, "connected", result.Status)
assert.Contains(t, result.Details, "always healthy")
}
func TestInboxService_Health_UnknownChannelType(t *testing.T) {
svc, db := setupInboxServiceTest(t)
account, inbox := createInboxTestPrereqs(t, db, "email")
result, err := svc.Health(context.Background(), account.ID, inbox.ID)
require.NoError(t, err)
assert.NotNil(t, result)
assert.True(t, result.Healthy)
assert.Equal(t, "unknown", result.Status)
assert.Contains(t, result.Details, "not implemented")
}
func TestInboxService_Health_InboxNotFound(t *testing.T) {
svc, _ := setupInboxServiceTest(t)
result, err := svc.Health(context.Background(), 9999, 9999)
assert.Error(t, err)
assert.Nil(t, result)
assert.Contains(t, err.Error(), "inbox not found")
}
// ========================================
// SyncTemplates service tests
// ========================================
func TestInboxService_SyncTemplates_NonWhatsAppInbox(t *testing.T) {
svc, db := setupInboxServiceTest(t)
account, inbox := createInboxTestPrereqs(t, db, "api")
templates, err := svc.SyncTemplates(context.Background(), account.ID, inbox.ID)
assert.Error(t, err)
assert.Nil(t, templates)
assert.Contains(t, err.Error(), "only supported for WhatsApp")
}
func TestInboxService_SyncTemplates_InboxNotFound(t *testing.T) {
svc, _ := setupInboxServiceTest(t)
templates, err := svc.SyncTemplates(context.Background(), 9999, 9999)
assert.Error(t, err)
assert.Nil(t, templates)
assert.Contains(t, err.Error(), "inbox not found")
}
// SyncTemplates for WhatsApp inboxes requires WhatsApp service/repo which are nil in tests.
// The WhatsApp path will fail at getWhatsAppChannel — we verify that it correctly
// rejects non-WhatsApp inboxes first (tested above), and for WhatsApp inboxes without
// the WhatsApp service, it should error out at the channel lookup step.
func TestInboxService_SyncTemplates_WhatsAppInbox_NoWaService(t *testing.T) {
svc, db := setupInboxServiceTest(t)
account, inbox := createInboxTestPrereqs(t, db, "whatsapp")
templates, err := svc.SyncTemplates(context.Background(), account.ID, inbox.ID)
// Will fail because whatsapp service/repo are nil
assert.Error(t, err)
assert.Nil(t, templates)
}
// ========================================
// RegisterWebhook service tests
// ========================================
func TestInboxService_RegisterWebhook_APIInbox(t *testing.T) {
svc, db := setupInboxServiceTest(t)
account, inbox := createInboxTestPrereqs(t, db, "api")
err := svc.RegisterWebhook(context.Background(), account.ID, inbox.ID, RegisterWebhookRequest{
URL: "https://example.com/webhook",
Events: []string{"message_created", "conversation_updated"},
})
require.NoError(t, err)
// Verify the inbox webhook_url was updated
var updatedInbox model.Inbox
require.NoError(t, db.First(&updatedInbox, inbox.ID).Error)
assert.Equal(t, "https://example.com/webhook", updatedInbox.WebhookURL)
// Verify a webhook subscription was created
var subs []model.WebhookSubscription
require.NoError(t, db.Where("account_id = ?", account.ID).Find(&subs).Error)
assert.Len(t, subs, 1)
assert.Equal(t, "https://example.com/webhook", subs[0].URL)
assert.True(t, subs[0].Active)
// Verify events are stored correctly
var events []string
require.NoError(t, json.Unmarshal(subs[0].Events, &events))
assert.Contains(t, events, "message_created")
assert.Contains(t, events, "conversation_updated")
}
func TestInboxService_RegisterWebhook_APIInbox_NoEvents(t *testing.T) {
svc, db := setupInboxServiceTest(t)
account, inbox := createInboxTestPrereqs(t, db, "api")
err := svc.RegisterWebhook(context.Background(), account.ID, inbox.ID, RegisterWebhookRequest{
URL: "https://example.com/webhook2",
})
require.NoError(t, err)
// Verify the inbox webhook_url was updated
var updatedInbox model.Inbox
require.NoError(t, db.First(&updatedInbox, inbox.ID).Error)
assert.Equal(t, "https://example.com/webhook2", updatedInbox.WebhookURL)
// No webhook subscription should be created when events are empty
var subs []model.WebhookSubscription
require.NoError(t, db.Where("account_id = ?", account.ID).Find(&subs).Error)
assert.Len(t, subs, 0)
}
func TestInboxService_RegisterWebhook_NonAPINonWhatsAppInbox(t *testing.T) {
svc, db := setupInboxServiceTest(t)
account, inbox := createInboxTestPrereqs(t, db, "web_widget")
err := svc.RegisterWebhook(context.Background(), account.ID, inbox.ID, RegisterWebhookRequest{
URL: "https://example.com/webhook",
})
// For channel types other than whatsapp/api, the service returns an error
assert.Error(t, err)
assert.Contains(t, err.Error(), "only supported for WhatsApp and API")
}
func TestInboxService_RegisterWebhook_InboxNotFound(t *testing.T) {
svc, _ := setupInboxServiceTest(t)
err := svc.RegisterWebhook(context.Background(), 9999, 9999, RegisterWebhookRequest{
URL: "https://example.com/webhook",
})
assert.Error(t, err)
assert.Contains(t, err.Error(), "inbox not found")
}
func TestInboxService_RegisterWebhook_WhatsAppInbox_NoWaService(t *testing.T) {
svc, db := setupInboxServiceTest(t)
account, inbox := createInboxTestPrereqs(t, db, "whatsapp")
err := svc.RegisterWebhook(context.Background(), account.ID, inbox.ID, RegisterWebhookRequest{
URL: "https://example.com/wa-webhook",
})
// Will fail because whatsapp service/repo are nil
assert.Error(t, err)
}