418 lines
14 KiB
Go
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)
|
|
} |