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) }