package webhook import ( "bytes" "context" "encoding/json" "net/http" "net/http/httptest" "net/url" "strings" "testing" "time" "github.com/gin-gonic/gin" "github.com/gochat/gochat/internal/channel" linechannel "github.com/gochat/gochat/internal/channel/line" channelprovider "github.com/gochat/gochat/internal/channel/provider" telegramchannel "github.com/gochat/gochat/internal/channel/telegram" "github.com/gochat/gochat/internal/model" channelmodel "github.com/gochat/gochat/internal/model/channel" "github.com/gochat/gochat/internal/worker" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) // ============================================================ // webhook_handler.go — NewHandler + Handle // ============================================================ // stubChannelProvider is a minimal ChannelProvider for testing WebhookHandler.Handle. type stubChannelProvider struct { providerType channel.ChannelType incomingMsg *channel.IncomingMessage err error } func (s *stubChannelProvider) Type() channel.ChannelType { return s.providerType } func (s *stubChannelProvider) Name() string { return string(s.providerType) } func (s *stubChannelProvider) Description() string { return "stub" } func (s *stubChannelProvider) ConfigSchema() *channel.ConfigSchemaDefinition { return nil } func (s *stubChannelProvider) ValidateConfig(ctx context.Context, config channel.ChannelConfig) error { return nil } func (s *stubChannelProvider) DefaultConfig() channel.ChannelConfig { return nil } func (s *stubChannelProvider) OnCreate(ctx context.Context, inbox *model.Inbox, config channel.ChannelConfig) (channel.ChannelConfig, error) { return config, nil } func (s *stubChannelProvider) OnDestroy(ctx context.Context, inbox *model.Inbox, config channel.ChannelConfig) error { return nil } func (s *stubChannelProvider) ProcessIncoming(ctx context.Context, inbox *model.Inbox, rawPayload []byte) (*channel.IncomingMessage, error) { if s.err != nil { return nil, s.err } if s.incomingMsg != nil { return s.incomingMsg, nil } return &channel.IncomingMessage{SourceID: "stub-src", Content: "stub"}, nil } func (s *stubChannelProvider) SendMessage(ctx context.Context, inbox *model.Inbox, message *model.Message, contact *model.Contact) (*channel.SendResult, error) { return nil, nil } func (s *stubChannelProvider) GetContactProfile(ctx context.Context, inbox *model.Inbox, contactSource string) (*channel.ContactProfile, error) { return nil, nil } func (s *stubChannelProvider) Capabilities() channel.ChannelCapabilities { return channel.ChannelCapabilities{} } func (s *stubChannelProvider) ValidateWebhookRequest(ctx context.Context, inbox *model.Inbox, request *channel.WebhookRequest) error { return nil } func TestNewHandler_Cov2(t *testing.T) { registry := &channel.ChannelRegistry{} h := NewHandler(registry) assert.NotNil(t, h) } func TestHandler_Handle_UnknownChannel_Cov2(t *testing.T) { gin.SetMode(gin.TestMode) // Use global registry — it's always initialized h := NewHandler(channel.GetRegistry()) r := gin.New() r.POST("/webhooks/:channel_type/:inbox_id", h.Handle) w := httptest.NewRecorder() body := bytes.NewReader([]byte(`{}`)) req, _ := http.NewRequest("POST", "/webhooks/unknown_chan_type/1", body) r.ServeHTTP(w, req) assert.Equal(t, http.StatusBadRequest, w.Code) } func TestHandler_Handle_InvalidInboxID_Cov2(t *testing.T) { gin.SetMode(gin.TestMode) // Register a stub provider on the global registry provider := &stubChannelProvider{providerType: "stub_channel_test"} _ = channel.Register(provider) defer func() { // Clean up by re-registering won't work, but tests are isolated }() h := NewHandler(channel.GetRegistry()) r := gin.New() r.POST("/webhooks/:channel_type/:inbox_id", h.Handle) w := httptest.NewRecorder() body := bytes.NewReader([]byte(`{}`)) req, _ := http.NewRequest("POST", "/webhooks/stub_channel_test/abc", body) r.ServeHTTP(w, req) assert.Equal(t, http.StatusBadRequest, w.Code) } func TestHandler_Handle_Success_Cov2(t *testing.T) { gin.SetMode(gin.TestMode) // Register a stub provider — Register is idempotent-safe (returns error if already registered) provider := &stubChannelProvider{providerType: "stub_channel_test2"} _ = channel.Register(provider) h := NewHandler(channel.GetRegistry()) r := gin.New() r.POST("/webhooks/:channel_type/:inbox_id", h.Handle) w := httptest.NewRecorder() body := bytes.NewReader([]byte(`{"hello":"world"}`)) req, _ := http.NewRequest("POST", "/webhooks/stub_channel_test2/42", body) r.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) var resp map[string]interface{} require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp)) assert.Equal(t, "processed", resp["status"]) } // ============================================================ // email_webhook.go — all functions // ============================================================ func TestNewEmailWebhookHandler_Cov2(t *testing.T) { h := NewEmailWebhookHandler(nil, nil, nil) assert.NotNil(t, h) } func TestEmailWebhookHandleVerification_Cov2(t *testing.T) { gin.SetMode(gin.TestMode) h := NewEmailWebhookHandler(nil, nil, nil) r := gin.New() r.GET("/verify", h.HandleEmailVerification) w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/verify", nil) r.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) } func TestEmailWebhookHandle_InvalidInboxID_Cov2(t *testing.T) { gin.SetMode(gin.TestMode) h := NewEmailWebhookHandler(nil, nil, nil) r := gin.New() r.POST("/webhooks/email/:inbox_id", h.HandleEmailWebhook) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/webhooks/email/abc", strings.NewReader(`{}`)) r.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) } func TestEmailWebhookHandle_InboxNotFound_Cov2(t *testing.T) { gin.SetMode(gin.TestMode) db := newWebhookLookupTestDB(t) h := NewEmailWebhookHandler(nil, nil, db) r := gin.New() r.POST("/webhooks/email/:inbox_id", h.HandleEmailWebhook) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/webhooks/email/999", strings.NewReader(`{}`)) r.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) } func TestEmailWebhookHandle_PipelineNil_Cov2(t *testing.T) { gin.SetMode(gin.TestMode) db := newWebhookLookupTestDB(t) inbox := seedWebhookInbox(t, db, "email") // ChannelConfig with valid JSON inbox.ChannelConfig = `{"mailgun_api_key":"key123"}` db.Save(&inbox) h := NewEmailWebhookHandler(nil, nil, db) r := gin.New() r.POST("/webhooks/email/:inbox_id", h.HandleEmailWebhook) // Since pipeline is nil, calling Process will panic; but the body parse will // succeed first, and pipeline.Process will be nil-pointer — we expect panic recovery // Instead, test parseChannelConfig and lookupInbox directly inb, err := h.lookupInbox(inbox.ID) require.NoError(t, err) assert.Equal(t, inbox.ID, inb.ID) cfg := h.parseChannelConfig(inb) assert.Equal(t, "key123", cfg["mailgun_api_key"]) } func TestEmailWebhookParseChannelConfig_Empty_Cov2(t *testing.T) { h := NewEmailWebhookHandler(nil, nil, nil) cfg := h.parseChannelConfig(&model.Inbox{ChannelConfig: ""}) assert.Empty(t, cfg) } func TestEmailWebhookParseChannelConfig_InvalidJSON_Cov2(t *testing.T) { h := NewEmailWebhookHandler(nil, nil, nil) cfg := h.parseChannelConfig(&model.Inbox{ChannelConfig: "{invalid"}) assert.Empty(t, cfg) } func TestEmailWebhookLookupInbox_NotFound_Cov2(t *testing.T) { db := newWebhookLookupTestDB(t) h := NewEmailWebhookHandler(nil, nil, db) _, err := h.lookupInbox(99999) assert.Error(t, err) } // ============================================================ // facebook_webhook.go — WithWorkerPool, WithSearchIndexer, HandleFacebookVerification, resolveVerifyToken // ============================================================ func TestFacebookWebhookWithWorkerPool_Cov2(t *testing.T) { db := newWebhookLookupTestDB(t) h := NewFacebookWebhookHandler(nil, nil, db) wp := worker.NewWorkerPoolWithOptions(db, worker.WithQueues("low")) result := h.WithWorkerPool(wp) assert.NotNil(t, result) } func TestFacebookWebhookWithWorkerPool_NilReceiver_Cov2(t *testing.T) { var h *FacebookWebhookHandler wp := worker.NewWorkerPoolWithOptions(newWebhookLookupTestDB(t), worker.WithQueues("low")) result := h.WithWorkerPool(wp) assert.Nil(t, result) } func TestFacebookWebhookWithSearchIndexer_Cov2(t *testing.T) { db := newWebhookLookupTestDB(t) h := NewFacebookWebhookHandler(nil, nil, db) indexer := &recordingIncomingSearchIndexer{} result := h.WithSearchIndexer(indexer) assert.NotNil(t, result) } func TestFacebookWebhookWithSearchIndexer_NilReceiver_Cov2(t *testing.T) { var h *FacebookWebhookHandler result := h.WithSearchIndexer(&recordingIncomingSearchIndexer{}) assert.Nil(t, result) } func TestFacebookWebhookHandleVerification_InboxNotFound_Cov2(t *testing.T) { gin.SetMode(gin.TestMode) db := newWebhookLookupTestDB(t) h := NewFacebookWebhookHandler(nil, nil, db) r := gin.New() r.GET("/webhooks/facebook/:page_id", h.HandleFacebookVerification) w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/webhooks/facebook/nonexistent_page?hub.mode=subscribe&hub.verify_token=token&hub.challenge=challenge123", nil) r.ServeHTTP(w, req) assert.Equal(t, http.StatusNotFound, w.Code) } func TestFacebookWebhookHandleVerification_NoVerifyToken_Cov2(t *testing.T) { gin.SetMode(gin.TestMode) db := newWebhookLookupTestDB(t) inbox := seedWebhookInbox(t, db, "facebook") fbChannel := channelmodel.ChannelFacebook{ AccountID: 1, InboxID: inbox.ID, PageID: "fb-page-123", } require.NoError(t, db.Create(&fbChannel).Error) h := NewFacebookWebhookHandler(nil, nil, db) r := gin.New() r.GET("/webhooks/facebook/:page_id", h.HandleFacebookVerification) w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/webhooks/facebook/fb-page-123?hub.mode=subscribe&hub.verify_token=token&hub.challenge=challenge123", nil) r.ServeHTTP(w, req) assert.Equal(t, http.StatusUnprocessableEntity, w.Code) } func TestFacebookWebhookHandleVerification_TokenMismatch_Cov2(t *testing.T) { gin.SetMode(gin.TestMode) db := newWebhookLookupTestDB(t) inbox := seedWebhookInbox(t, db, "facebook") inbox.ChannelConfig = `{"webhook_verify_token":"correct-token"}` require.NoError(t, db.Save(&inbox).Error) fbChannel := channelmodel.ChannelFacebook{ AccountID: 1, InboxID: inbox.ID, PageID: "fb-page-456", } require.NoError(t, db.Create(&fbChannel).Error) h := NewFacebookWebhookHandler(nil, nil, db) r := gin.New() r.GET("/webhooks/facebook/:page_id", h.HandleFacebookVerification) w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/webhooks/facebook/fb-page-456?hub.mode=subscribe&hub.verify_token=wrong-token&hub.challenge=challenge123", nil) r.ServeHTTP(w, req) assert.Equal(t, http.StatusForbidden, w.Code) } func TestFacebookWebhookHandleVerification_Success_Cov2(t *testing.T) { gin.SetMode(gin.TestMode) db := newWebhookLookupTestDB(t) inbox := seedWebhookInbox(t, db, "facebook") inbox.ChannelConfig = `{"webhook_verify_token":"correct-token"}` require.NoError(t, db.Save(&inbox).Error) fbChannel := channelmodel.ChannelFacebook{ AccountID: 1, InboxID: inbox.ID, PageID: "fb-page-789", } require.NoError(t, db.Create(&fbChannel).Error) h := NewFacebookWebhookHandler(nil, nil, db) r := gin.New() r.GET("/webhooks/facebook/:page_id", h.HandleFacebookVerification) w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/webhooks/facebook/fb-page-789?hub.mode=subscribe&hub.verify_token=correct-token&hub.challenge=challenge789", nil) r.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) assert.Equal(t, "challenge789", w.Body.String()) } func TestFacebookWebhookResolveVerifyToken_Cov2(t *testing.T) { h := NewFacebookWebhookHandler(nil, nil, nil) // With token token := h.resolveVerifyToken(&model.Inbox{ChannelConfig: `{"webhook_verify_token":"my-token"}`}) assert.Equal(t, "my-token", token) // Without token token = h.resolveVerifyToken(&model.Inbox{ChannelConfig: ``}) assert.Equal(t, "", token) // Invalid JSON token = h.resolveVerifyToken(&model.Inbox{ChannelConfig: `{invalid`}) assert.Equal(t, "", token) } func TestFacebookWebhookResolveAppSecret_Cov2(t *testing.T) { h := NewFacebookWebhookHandler(nil, nil, nil) secret := h.resolveAppSecret(&model.Inbox{ChannelConfig: `{"app_secret":"my-secret"}`}) assert.Equal(t, "my-secret", secret) secret = h.resolveAppSecret(&model.Inbox{ChannelConfig: ``}) assert.Equal(t, "", secret) } func TestFacebookWebhookParseChannelConfig_Cov2(t *testing.T) { h := NewFacebookWebhookHandler(nil, nil, nil) // Valid cfg := h.parseChannelConfig(&model.Inbox{ChannelConfig: `{"key":"value"}`}) assert.Equal(t, "value", cfg["key"]) // Empty cfg = h.parseChannelConfig(&model.Inbox{ChannelConfig: ``}) assert.Empty(t, cfg) // Invalid cfg = h.parseChannelConfig(&model.Inbox{ChannelConfig: `{bad json`}) assert.Empty(t, cfg) } func TestFacebookWebhookHandleWebhook_InboxNotFound_Cov2(t *testing.T) { gin.SetMode(gin.TestMode) db := newWebhookLookupTestDB(t) h := NewFacebookWebhookHandler(nil, nil, db) r := gin.New() r.POST("/webhooks/facebook/:page_id", h.HandleFacebookWebhook) w := httptest.NewRecorder() body := []byte(`{"object":"page","entry":[]}`) req, _ := http.NewRequest("POST", "/webhooks/facebook/nonexistent", bytes.NewReader(body)) r.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) } func TestParseOptionalUintParam_Cov2(t *testing.T) { // Empty v, err := parseOptionalUintParam("") assert.NoError(t, err) assert.Equal(t, uint(0), v) // Valid v, err = parseOptionalUintParam("42") assert.NoError(t, err) assert.Equal(t, uint(42), v) // Invalid _, err = parseOptionalUintParam("abc") assert.Error(t, err) } func TestMillisToTime_Cov2(t *testing.T) { // Zero/negative → nil t1 := millisToTime(0) assert.Nil(t, t1) t2 := millisToTime(-1) assert.Nil(t, t2) // Positive → valid time t3 := millisToTime(1710000000000) assert.NotNil(t, t3) } // ============================================================ // line_webhook.go — WithWorkerPool, WithSearchIndexer, HandleLineVerification, lookupInbox // ============================================================ func TestLineWebhookWithWorkerPool_Cov2(t *testing.T) { db := newWebhookLookupTestDB(t) lineRepo := linechannel.NewRepository(db) lineSvc := linechannel.NewLineService(lineRepo) h := NewLineWebhookHandler(nil, nil, lineSvc, db) wp := worker.NewWorkerPoolWithOptions(db, worker.WithQueues("low")) result := h.WithWorkerPool(wp) assert.NotNil(t, result) } func TestLineWebhookWithWorkerPool_NilReceiver_Cov2(t *testing.T) { var h *LineWebhookHandler wp := worker.NewWorkerPoolWithOptions(newWebhookLookupTestDB(t), worker.WithQueues("low")) result := h.WithWorkerPool(wp) assert.Nil(t, result) } func TestLineWebhookWithSearchIndexer_Cov2(t *testing.T) { db := newWebhookLookupTestDB(t) lineRepo := linechannel.NewRepository(db) lineSvc := linechannel.NewLineService(lineRepo) h := NewLineWebhookHandler(nil, nil, lineSvc, db) result := h.WithSearchIndexer(&recordingIncomingSearchIndexer{}) assert.NotNil(t, result) } func TestLineWebhookWithSearchIndexer_NilReceiver_Cov2(t *testing.T) { var h *LineWebhookHandler result := h.WithSearchIndexer(&recordingIncomingSearchIndexer{}) assert.Nil(t, result) } func TestLineWebhookHandleVerification_Cov2(t *testing.T) { gin.SetMode(gin.TestMode) h := NewLineWebhookHandler(nil, nil, nil, nil) r := gin.New() r.GET("/verify", h.HandleLineVerification) w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/verify", nil) r.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) } func TestLineWebhookLookupInbox_Cov2(t *testing.T) { db := newWebhookLookupTestDB(t) inbox := seedWebhookInbox(t, db, "line") h := NewLineWebhookHandler(nil, nil, nil, db) found, err := h.lookupInbox(inbox.ID) require.NoError(t, err) assert.Equal(t, inbox.ID, found.ID) _, err = h.lookupInbox(99999) assert.Error(t, err) } func TestLineWebhookLookupInboxByLineChannelID_NotFound_Cov2(t *testing.T) { db := newWebhookLookupTestDB(t) h := NewLineWebhookHandler(nil, nil, nil, db) _, err := h.lookupInboxByLineChannelID("nonexistent") assert.Error(t, err) } func TestLineWebhookLookupInboxByLineChannelID_NilDB_Cov2(t *testing.T) { h := NewLineWebhookHandler(nil, nil, nil, nil) _, err := h.lookupInboxByLineChannelID("some-id") assert.Error(t, err) } func TestLineWebhookParseChannelConfig_Cov2(t *testing.T) { h := NewLineWebhookHandler(nil, nil, nil, nil) // Empty cfg := h.parseChannelConfig(&model.Inbox{ChannelConfig: ""}) assert.Empty(t, cfg) // Valid cfg = h.parseChannelConfig(&model.Inbox{ChannelConfig: `{"channel_secret":"secret123"}`}) assert.Equal(t, "secret123", cfg["channel_secret"]) // Invalid cfg = h.parseChannelConfig(&model.Inbox{ChannelConfig: `{bad`}) assert.Empty(t, cfg) } func TestLineWebhookHandle_MissingChannelID_Cov2(t *testing.T) { gin.SetMode(gin.TestMode) h := NewLineWebhookHandler(nil, nil, nil, nil) // Use a route without the line_channel_id param so it's empty r := gin.New() r.POST("/webhooks/line", h.HandleLineWebhook) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/webhooks/line", strings.NewReader(`{}`)) r.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) } // ============================================================ // telegram_webhook.go — WithWorkerPool, WithSearchIndexer // ============================================================ func TestTelegramWebhookWithWorkerPool_Cov2(t *testing.T) { db := newWebhookLookupTestDB(t) telProvider := channelprovider.NewTelegramProvider() telWebhook := telegramchannel.NewWebhookHandler(telProvider) h := NewTelegramWebhookHandler(telProvider, telWebhook, db) wp := worker.NewWorkerPoolWithOptions(db, worker.WithQueues("low")) result := h.WithWorkerPool(wp) assert.NotNil(t, result) } func TestTelegramWebhookWithWorkerPool_NilReceiver_Cov2(t *testing.T) { var h *TelegramWebhookHandler wp := worker.NewWorkerPoolWithOptions(newWebhookLookupTestDB(t), worker.WithQueues("low")) result := h.WithWorkerPool(wp) assert.Nil(t, result) } func TestTelegramWebhookWithSearchIndexer_Cov2(t *testing.T) { db := newWebhookLookupTestDB(t) telProvider := channelprovider.NewTelegramProvider() telWebhook := telegramchannel.NewWebhookHandler(telProvider) h := NewTelegramWebhookHandler(telProvider, telWebhook, db) result := h.WithSearchIndexer(&recordingIncomingSearchIndexer{}) assert.NotNil(t, result) } func TestTelegramWebhookWithSearchIndexer_NilReceiver_Cov2(t *testing.T) { var h *TelegramWebhookHandler result := h.WithSearchIndexer(&recordingIncomingSearchIndexer{}) assert.Nil(t, result) } func TestTelegramWebhookHandle_NoBotToken_Cov2(t *testing.T) { gin.SetMode(gin.TestMode) h := NewTelegramWebhookHandler(nil, nil, nil) r := gin.New() r.POST("/webhooks/telegram", h.HandleTelegramWebhook) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/webhooks/telegram", strings.NewReader(`{}`)) r.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) } func TestTelegramWebhookHandle_InvalidJSON_Cov2(t *testing.T) { gin.SetMode(gin.TestMode) h := NewTelegramWebhookHandler(nil, nil, nil) r := gin.New() r.POST("/webhooks/telegram/:bot_token", h.HandleTelegramWebhook) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/webhooks/telegram/some-token", strings.NewReader(`{invalid json`)) r.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) } func TestTelegramWebhookHandle_InboxNotFound_Cov2(t *testing.T) { gin.SetMode(gin.TestMode) db := newWebhookLookupTestDB(t) h := NewTelegramWebhookHandler(nil, nil, db) r := gin.New() r.POST("/webhooks/telegram/:bot_token", h.HandleTelegramWebhook) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/webhooks/telegram/nonexistent-token", strings.NewReader(`{"update_id":1}`)) r.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) } func TestTelegramWebhookLookupInbox_NilDB_Cov2(t *testing.T) { h := NewTelegramWebhookHandler(nil, nil, nil) _, err := h.lookupInbox("some-token") assert.Error(t, err) } func TestTelegramWebhookMaskBotToken_Cov2(t *testing.T) { // Long token masked := maskBotToken("1234567890:ABC-DEF") assert.Equal(t, "12345678...", masked) // Short token masked = maskBotToken("short") assert.Equal(t, "short", masked) } // ============================================================ // tiktok_webhook.go — WithWorkerPool, WithSearchIndexer, HandleTikTokVerification, lookupInbox, parseChannelConfig // ============================================================ func TestTikTokWebhookWithWorkerPool_Cov2(t *testing.T) { db := newWebhookLookupTestDB(t) h := NewTikTokWebhookHandler(nil, nil, db) wp := worker.NewWorkerPoolWithOptions(db, worker.WithQueues("low")) result := h.WithWorkerPool(wp) assert.NotNil(t, result) } func TestTikTokWebhookWithWorkerPool_NilReceiver_Cov2(t *testing.T) { var h *TikTokWebhookHandler wp := worker.NewWorkerPoolWithOptions(newWebhookLookupTestDB(t), worker.WithQueues("low")) result := h.WithWorkerPool(wp) assert.Nil(t, result) } func TestTikTokWebhookWithSearchIndexer_Cov2(t *testing.T) { db := newWebhookLookupTestDB(t) h := NewTikTokWebhookHandler(nil, nil, db) result := h.WithSearchIndexer(&recordingIncomingSearchIndexer{}) assert.NotNil(t, result) } func TestTikTokWebhookWithSearchIndexer_NilReceiver_Cov2(t *testing.T) { var h *TikTokWebhookHandler result := h.WithSearchIndexer(&recordingIncomingSearchIndexer{}) assert.Nil(t, result) } func TestTikTokWebhookHandleVerification_Cov2(t *testing.T) { gin.SetMode(gin.TestMode) h := NewTikTokWebhookHandler(nil, nil, nil) r := gin.New() r.POST("/webhooks/tiktok/verify", h.HandleTikTokVerification) w := httptest.NewRecorder() body := `{"challenge":"my-challenge"}` req, _ := http.NewRequest("POST", "/webhooks/tiktok/verify", strings.NewReader(body)) r.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) var resp map[string]interface{} require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp)) assert.Equal(t, "my-challenge", resp["challenge"]) } func TestTikTokWebhookHandleVerification_InvalidJSON_Cov2(t *testing.T) { gin.SetMode(gin.TestMode) h := NewTikTokWebhookHandler(nil, nil, nil) r := gin.New() r.POST("/webhooks/tiktok/verify", h.HandleTikTokVerification) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/webhooks/tiktok/verify", strings.NewReader(`{invalid`)) r.ServeHTTP(w, req) assert.Equal(t, http.StatusBadRequest, w.Code) } func TestTikTokWebhookHandleVerification_ReadBodyFail_Cov2(t *testing.T) { gin.SetMode(gin.TestMode) h := NewTikTokWebhookHandler(nil, nil, nil) r := gin.New() r.POST("/webhooks/tiktok/verify", h.HandleTikTokVerification) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/webhooks/tiktok/verify", &failingReader{}) r.ServeHTTP(w, req) assert.Equal(t, http.StatusUnprocessableEntity, w.Code) } func TestTikTokWebhookLookupInbox_Cov2(t *testing.T) { db := newWebhookLookupTestDB(t) inbox := seedWebhookInbox(t, db, "tiktok") h := NewTikTokWebhookHandler(nil, nil, db) found, err := h.lookupInbox(inbox.ID) require.NoError(t, err) assert.Equal(t, inbox.ID, found.ID) _, err = h.lookupInbox(99999) assert.Error(t, err) } func TestTikTokWebhookLookupInboxByBusinessID_NilDB_Cov2(t *testing.T) { h := NewTikTokWebhookHandler(nil, nil, nil) _, err := h.lookupInboxByBusinessID("biz-123") assert.Error(t, err) } func TestTikTokWebhookParseChannelConfig_Cov2(t *testing.T) { h := NewTikTokWebhookHandler(nil, nil, nil) // Empty cfg := h.parseChannelConfig(&model.Inbox{ChannelConfig: ""}) assert.Empty(t, cfg) // Valid cfg = h.parseChannelConfig(&model.Inbox{ChannelConfig: `{"key":"val"}`}) assert.Equal(t, "val", cfg["key"]) // Invalid cfg = h.parseChannelConfig(&model.Inbox{ChannelConfig: `{bad`}) assert.Empty(t, cfg) } func TestExtractTikTokBusinessID_Cov2(t *testing.T) { tests := []struct { json string want string }{ {`{"biz_id":"b1"}`, "b1"}, {`{"business_id":"b2"}`, "b2"}, {`{"tiktok_business_id":"b3"}`, "b3"}, {`{"data":{"biz_id":"b4"}}`, "b4"}, {`{"data":{"business_id":"b5"}}`, "b5"}, {`{"data":{"tiktok_business_id":"b6"}}`, "b6"}, {`{}`, ""}, {`invalid`, ""}, } for _, tt := range tests { got := extractTikTokBusinessID([]byte(tt.json)) assert.Equal(t, tt.want, got, "input: %s", tt.json) } } func TestExtractTikTokSignatureParts_Cov2(t *testing.T) { // Empty ts, sig := extractTikTokSignatureParts("") assert.Equal(t, int64(0), ts) assert.Equal(t, "", sig) // Valid ts, sig = extractTikTokSignatureParts("t=1710000000,s=abc123") assert.Equal(t, int64(1710000000), ts) assert.Equal(t, "abc123", sig) // Malformed ts, sig = extractTikTokSignatureParts("garbage") assert.Equal(t, int64(0), ts) assert.Equal(t, "", sig) } func TestTiktokDataString_Cov2(t *testing.T) { data := map[string]interface{}{"message_id": "msg-1", "count": float64(42)} assert.Equal(t, "msg-1", tiktokDataString(data, "message_id")) assert.Equal(t, "", tiktokDataString(data, "count")) // not a string assert.Equal(t, "", tiktokDataString(data, "missing")) } func TestVerifyTikTokSignature_MissingCredentials_Cov2(t *testing.T) { t.Setenv("TIKTOK_APP_SECRET", "") err := verifyTikTokSignature("t=1710000000,s=abc", []byte(`{}`), time.Now()) assert.Error(t, err) } // ============================================================ // twilio_webhook.go — WithSearchIndexer, lookupInbox // ============================================================ func TestTwilioWebhookWithSearchIndexer_Cov2(t *testing.T) { db := newWebhookLookupTestDB(t) h := NewTwilioWebhookHandler(nil, db) result := h.WithSearchIndexer(&recordingIncomingSearchIndexer{}) assert.NotNil(t, result) } func TestTwilioWebhookWithSearchIndexer_NilReceiver_Cov2(t *testing.T) { var h *TwilioWebhookHandler result := h.WithSearchIndexer(&recordingIncomingSearchIndexer{}) assert.Nil(t, result) } func TestTwilioWebhookLookupInbox_Cov2(t *testing.T) { db := newWebhookLookupTestDB(t) inbox := seedWebhookInbox(t, db, "twilio_sms") h := NewTwilioWebhookHandler(nil, db) found, err := h.lookupInbox(inbox.ID) require.NoError(t, err) assert.Equal(t, inbox.ID, found.ID) _, err = h.lookupInbox(99999) assert.Error(t, err) } func TestTwilioWebhookLookupInboxByPhoneNumber_NilDB_Cov2(t *testing.T) { h := NewTwilioWebhookHandler(nil, nil) _, err := h.lookupInboxByPhoneNumber("+15551234567") assert.Error(t, err) } func TestTwilioWebhookHandleInboundSMS_MissingPhone_Cov2(t *testing.T) { gin.SetMode(gin.TestMode) h := NewTwilioWebhookHandler(nil, nil) r := gin.New() r.POST("/webhooks/sms", h.HandleTwilioInboundSMS) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/webhooks/sms", strings.NewReader("")) r.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) assert.Contains(t, w.Body.String(), "") } func TestTwilioWebhookHandleInboundSMS_InboxNotFound_Cov2(t *testing.T) { gin.SetMode(gin.TestMode) db := newWebhookLookupTestDB(t) h := NewTwilioWebhookHandler(nil, db) r := gin.New() r.POST("/webhooks/sms/:phone_number", h.HandleTwilioInboundSMS) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/webhooks/sms/15551234567", strings.NewReader("")) r.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) } func TestTwilioWebhookHandleCallback_NoWebhook_Cov2(t *testing.T) { gin.SetMode(gin.TestMode) db := newWebhookLookupTestDB(t) h := NewTwilioWebhookHandler(nil, db) r := gin.New() r.POST("/webhooks/twilio/callback", h.HandleTwilioCallback) w := httptest.NewRecorder() form := url.Values{} req, _ := http.NewRequest("POST", "/webhooks/twilio/callback", strings.NewReader(form.Encode())) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") r.ServeHTTP(w, req) assert.Equal(t, http.StatusNoContent, w.Code) } func TestTwilioWebhookHandleCallback_InboxNotFound_Cov2(t *testing.T) { gin.SetMode(gin.TestMode) db := newWebhookLookupTestDB(t) h := NewTwilioWebhookHandler(nil, db) r := gin.New() r.POST("/webhooks/twilio/callback", h.HandleTwilioCallback) w := httptest.NewRecorder() form := url.Values{ "To": []string{"+15551234567"}, "AccountSid": []string{"AC123"}, "MessageSid": []string{"SM123"}, "MessageStatus": []string{"sent"}, } req, _ := http.NewRequest("POST", "/webhooks/twilio/callback", strings.NewReader(form.Encode())) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") r.ServeHTTP(w, req) assert.Equal(t, http.StatusNoContent, w.Code) } func TestTwilioWebhookHandleDeliveryStatus_NoPhone_Cov2(t *testing.T) { gin.SetMode(gin.TestMode) db := newWebhookLookupTestDB(t) h := NewTwilioWebhookHandler(nil, db) r := gin.New() r.POST("/webhooks/twilio/status", h.HandleTwilioDeliveryStatus) w := httptest.NewRecorder() form := url.Values{ "MessageSid": []string{"SM123"}, "MessageStatus": []string{"delivered"}, } req, _ := http.NewRequest("POST", "/webhooks/twilio/status", strings.NewReader(form.Encode())) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") r.ServeHTTP(w, req) assert.Equal(t, http.StatusNoContent, w.Code) } func TestTwilioNormalizePhone_Cov2(t *testing.T) { assert.Equal(t, "+15551234567", normalizeTwilioPhone("15551234567")) assert.Equal(t, "+15551234567", normalizeTwilioPhone("+15551234567")) assert.Equal(t, "", normalizeTwilioPhone(" ")) } func TestTwilioMapMessageStatus_Cov2(t *testing.T) { tests := []struct { status string want model.MessageStatus ok bool }{ {"sent", model.MessageStatusSent, true}, {"queued", model.MessageStatusSent, true}, {"accepted", model.MessageStatusSent, true}, {"sending", model.MessageStatusSent, true}, {"delivered", model.MessageStatusDelivered, true}, {"read", model.MessageStatusRead, true}, {"undelivered", model.MessageStatusFailed, true}, {"failed", model.MessageStatusFailed, true}, {"unknown", "", false}, } for _, tt := range tests { got, ok := mapTwilioMessageStatus(tt.status) assert.Equal(t, tt.ok, ok, "status: %s", tt.status) assert.Equal(t, tt.want, got, "status: %s", tt.status) } } func TestTwilioExternalError_Cov2(t *testing.T) { // Failed with error message assert.Equal(t, "30007 - Carrier violation", twilioExternalError("30007", "Carrier violation", "failed")) // Undelivered with error message assert.Equal(t, "30008 - Rejected", twilioExternalError("30008", "Rejected", "undelivered")) // Failed without error message assert.Equal(t, "Twilio delivery failed with error code 30007", twilioExternalError("30007", "", "failed")) // Non-failed status → empty assert.Equal(t, "", twilioExternalError("30007", "msg", "delivered")) // No error code → empty assert.Equal(t, "", twilioExternalError("", "msg", "failed")) } // ============================================================ // whatsapp_webhook.go — WithWorkerPool, WithSearchIndexer, UpdateMessageStatus adapter // ============================================================ func TestWhatsAppWebhookWithWorkerPool_Cov2(t *testing.T) { db := newWebhookLookupTestDB(t) h := NewWhatsAppWebhookHandler(nil, nil, db) wp := worker.NewWorkerPoolWithOptions(db, worker.WithQueues("low")) result := h.WithWorkerPool(wp) assert.NotNil(t, result) } func TestWhatsAppWebhookWithWorkerPool_NilReceiver_Cov2(t *testing.T) { var h *WhatsAppWebhookHandler wp := worker.NewWorkerPoolWithOptions(newWebhookLookupTestDB(t), worker.WithQueues("low")) result := h.WithWorkerPool(wp) assert.Nil(t, result) } func TestWhatsAppWebhookWithSearchIndexer_Cov2(t *testing.T) { db := newWebhookLookupTestDB(t) h := NewWhatsAppWebhookHandler(nil, nil, db) result := h.WithSearchIndexer(&recordingIncomingSearchIndexer{}) assert.NotNil(t, result) } func TestWhatsAppWebhookWithSearchIndexer_NilReceiver_Cov2(t *testing.T) { var h *WhatsAppWebhookHandler result := h.WithSearchIndexer(&recordingIncomingSearchIndexer{}) assert.Nil(t, result) } func TestWhatsAppPersisterAdapter_Cov2(t *testing.T) { db := newWebhookLookupTestDB(t) persister := NewIncomingPersister(db) adapter := whatsAppPersisterAdapter{persister: persister} // UpdateMessageStatus with non-existent source ID — should not error err := adapter.UpdateMessageStatus(context.Background(), &model.Inbox{}, "nonexistent", model.MessageStatusRead, nil) assert.NoError(t, err) // UpdateMessageStatusWithError err = adapter.UpdateMessageStatusWithError(context.Background(), &model.Inbox{}, "nonexistent", model.MessageStatusFailed, nil, "test error") assert.NoError(t, err) // PersistIncoming with valid msg to exercise the adapter delegation path msg := &channel.IncomingMessage{ ChannelType: channel.ChannelTelegram, SourceID: "wa-adapter-1", SenderID: "wa-user", SenderName: "WA User", SenderType: channel.SenderContact, Content: "adapter test", ContentType: channel.ContentText, } _, err = adapter.PersistIncoming(context.Background(), &model.Inbox{AccountID: 1}, msg) // With nil inbox ID=0, this may error during conversation creation — just ensure no panic _ = err } // ============================================================ // shopify_webhook.go — findShopifyHookByDomain // ============================================================ func TestShopifyWebhookFindHookByDomain_Cov2(t *testing.T) { db := newWebhookLookupTestDB(t) settings, _ := json.Marshal(model.ShopifySettings{ShopDomain: "store1.myshopify.com"}) hook := model.IntegrationHook{ AccountID: 1, HookType: model.HookTypeShopify, Status: model.HookStatusActive, AccessToken: "token", Settings: settings, } require.NoError(t, db.Create(&hook).Error) h := NewShopifyWebhookHandler(db, "secret") found, err := h.findShopifyHookByDomain(context.Background(), "store1.myshopify.com") require.NoError(t, err) assert.NotNil(t, found) assert.Equal(t, hook.ID, found.ID) } func TestShopifyWebhookFindHookByDomain_NotFound_Cov2(t *testing.T) { db := newWebhookLookupTestDB(t) h := NewShopifyWebhookHandler(db, "secret") found, err := h.findShopifyHookByDomain(context.Background(), "nonexistent.myshopify.com") // When no hooks match, shopifyHooks returns empty list with nil error, // and findShopifyHookByDomain returns nil, nil assert.NoError(t, err) assert.Nil(t, found) } func TestShopifyWebhookFindHookByDomain_NilDB_Cov2(t *testing.T) { h := NewShopifyWebhookHandler(nil, "secret") _, err := h.findShopifyHookByDomain(context.Background(), "store.myshopify.com") assert.Error(t, err) } func TestShopifyWebhookHandle_EmptyBody_Cov2(t *testing.T) { gin.SetMode(gin.TestMode) db := newWebhookLookupTestDB(t) h := NewShopifyWebhookHandler(db, "secret") r := gin.New() r.POST("/webhooks/shopify", h.HandleShopifyWebhook) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/webhooks/shopify", strings.NewReader("")) req.Header.Set("X-Shopify-Hmac-SHA256", shopifyHMAC("secret", []byte(""))) req.Header.Set("X-Shopify-Topic", "orders/create") r.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) } func TestShopifyWebhookHandle_InvalidJSON_Cov2(t *testing.T) { gin.SetMode(gin.TestMode) db := newWebhookLookupTestDB(t) h := NewShopifyWebhookHandler(db, "secret") r := gin.New() r.POST("/webhooks/shopify", h.HandleShopifyWebhook) body := []byte(`{invalid}`) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/webhooks/shopify", bytes.NewReader(body)) req.Header.Set("X-Shopify-Hmac-SHA256", shopifyHMAC("secret", body)) r.ServeHTTP(w, req) assert.Equal(t, http.StatusBadRequest, w.Code) } func TestShopifyWebhookHandle_NoSecret_Cov2(t *testing.T) { gin.SetMode(gin.TestMode) db := newWebhookLookupTestDB(t) h := NewShopifyWebhookHandler(db, "") r := gin.New() r.POST("/webhooks/shopify", h.HandleShopifyWebhook) // Without SHOPIFY_CLIENT_SECRET env var and empty secret t.Setenv("SHOPIFY_CLIENT_SECRET", "") w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/webhooks/shopify", strings.NewReader(`{}`)) r.ServeHTTP(w, req) assert.Equal(t, http.StatusUnauthorized, w.Code) } func TestShopifyWebhookHandle_ReadBodyFail_Cov2(t *testing.T) { gin.SetMode(gin.TestMode) db := newWebhookLookupTestDB(t) h := NewShopifyWebhookHandler(db, "secret") r := gin.New() r.POST("/webhooks/shopify", h.HandleShopifyWebhook) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/webhooks/shopify", &failingReader{}) r.ServeHTTP(w, req) assert.Equal(t, http.StatusBadRequest, w.Code) } // ============================================================ // Helpers // ============================================================ // failingReader is an io.Reader that always fails. type failingReader struct{} func (f *failingReader) Read(p []byte) (int, error) { return 0, http.ErrBodyReadAfterClose } // ============================================================ // Additional incoming_persister helper coverage // ============================================================ func TestMapIncomingContentType_Cov2(t *testing.T) { tests := []struct { ct channel.ContentType want string }{ {channel.ContentImage, string(model.MessageContentTypeImage)}, {channel.ContentAudio, string(model.MessageContentTypeAudio)}, {channel.ContentVideo, string(model.MessageContentTypeVideo)}, {channel.ContentFile, string(model.MessageContentTypeFile)}, {channel.ContentLocation, string(model.MessageContentTypeLocation)}, {channel.ContentText, string(model.MessageContentTypeText)}, {channel.ContentType("unknown"), string(model.MessageContentTypeText)}, } for _, tt := range tests { got := mapIncomingContentType(tt.ct) assert.Equal(t, tt.want, got) } } func TestMergeChannelConfig_Cov2(t *testing.T) { base := channel.ChannelConfig{"key1": "val1", "key2": "val2"} extra := map[string]interface{}{"key2": "override", "key3": "val3", "empty": ""} merged := mergeChannelConfig(base, extra) assert.Equal(t, "val1", merged["key1"]) assert.Equal(t, "override", merged["key2"]) assert.Equal(t, "val3", merged["key3"]) _, hasEmpty := merged["empty"] assert.False(t, hasEmpty) } func TestMustJSON_Cov2(t *testing.T) { // Nil → empty JSON j := mustJSON(nil) assert.Equal(t, `{}`, string(j)) // datatypes.JSON marshals nil as {"result":null}? Actually nil → "{}" // Valid value j = mustJSON(map[string]string{"key": "val"}) assert.Contains(t, string(j), "key") // Value that can't be marshaled (channel/func) j = mustJSON(make(chan int)) assert.Equal(t, "{}", string(j)) } func TestValidateIncomingMessage_Cov2(t *testing.T) { db := newWebhookLookupTestDB(t) p := NewIncomingPersister(db) // Nil persister assert.NoError(t, validateIncomingMessage(nil, &model.Inbox{}, &channel.IncomingMessage{})) // Nil inbox assert.NoError(t, validateIncomingMessage(p, nil, &channel.IncomingMessage{})) // Nil msg assert.NoError(t, validateIncomingMessage(p, &model.Inbox{}, nil)) // Missing source_id err := validateIncomingMessage(p, &model.Inbox{}, &channel.IncomingMessage{SenderID: "s1", Content: "hello"}) assert.Error(t, err) // Missing sender_id and conversation_id err = validateIncomingMessage(p, &model.Inbox{}, &channel.IncomingMessage{SourceID: "src1", Content: "hello"}) assert.Error(t, err) // Missing content and attachments err = validateIncomingMessage(p, &model.Inbox{}, &channel.IncomingMessage{SourceID: "src1", SenderID: "s1"}) assert.Error(t, err) // Valid with conversation_id as fallback for sender_id err = validateIncomingMessage(p, &model.Inbox{}, &channel.IncomingMessage{SourceID: "src1", ConversationID: "conv1", Content: "hello"}) assert.NoError(t, err) // Fully valid err = validateIncomingMessage(p, &model.Inbox{}, &channel.IncomingMessage{SourceID: "src1", SenderID: "s1", Content: "hello"}) assert.NoError(t, err) } func TestPersistIncoming_NilDB_Cov2(t *testing.T) { p := &IncomingPersister{} result, err := p.PersistIncoming(context.Background(), &model.Inbox{}, &channel.IncomingMessage{}) assert.Nil(t, result) assert.NoError(t, err) } func TestPerformPersistIncoming_NilParams_Cov2(t *testing.T) { p := &IncomingPersister{} result, err := p.performPersistIncoming(context.Background(), nil, nil) assert.Nil(t, result) assert.NoError(t, err) } // ============================================================ // Twilio callback with successful inbox lookup // ============================================================ func TestTwilioWebhookHandleCallback_Success_Cov2(t *testing.T) { gin.SetMode(gin.TestMode) db := newWebhookLookupTestDB(t) inbox := seedWebhookInbox(t, db, "twilio_sms") twilioChannel := channelmodel.ChannelTwilioSMS{ AccountID: inbox.AccountID, InboxID: inbox.ID, AccountSID: "AC123", PhoneNumber: "+15551234567", } require.NoError(t, db.Create(&twilioChannel).Error) h := NewTwilioWebhookHandler(nil, db) r := gin.New() r.POST("/webhooks/twilio/callback", h.HandleTwilioCallback) w := httptest.NewRecorder() form := url.Values{ "To": []string{"+15551234567"}, "AccountSid": []string{"AC123"}, "MessageSid": []string{"SM123"}, "MessageStatus": []string{"sent"}, "Body": []string{"hello"}, "From": []string{"+15557654321"}, } req, _ := http.NewRequest("POST", "/webhooks/twilio/callback", strings.NewReader(form.Encode())) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") r.ServeHTTP(w, req) // twilioWebhook is nil, so ProcessInboundSMS will fail, but handler returns 204 assert.Equal(t, http.StatusNoContent, w.Code) } func TestTwilioWebhookHandleCallback_MessagingServiceSid_Cov2(t *testing.T) { gin.SetMode(gin.TestMode) db := newWebhookLookupTestDB(t) inbox := seedWebhookInbox(t, db, "twilio_sms") twilioChannel := channelmodel.ChannelTwilioSMS{ AccountID: inbox.AccountID, InboxID: inbox.ID, AccountSID: "AC456", PhoneNumber: "+15559998888", MessagingServiceSID: "MG789", } require.NoError(t, db.Create(&twilioChannel).Error) h := NewTwilioWebhookHandler(nil, db) r := gin.New() r.POST("/webhooks/twilio/callback", h.HandleTwilioCallback) w := httptest.NewRecorder() form := url.Values{ "MessagingServiceSid": []string{"MG789"}, "MessageSid": []string{"SM456"}, "MessageStatus": []string{"sent"}, "Body": []string{"test"}, "From": []string{"+15551112222"}, } req, _ := http.NewRequest("POST", "/webhooks/twilio/callback", strings.NewReader(form.Encode())) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") r.ServeHTTP(w, req) assert.Equal(t, http.StatusNoContent, w.Code) } func TestTwilioWebhookHandleDeliveryStatus_MessagingServiceSid_Cov2(t *testing.T) { gin.SetMode(gin.TestMode) db := newWebhookLookupTestDB(t) inbox := seedWebhookInbox(t, db, "twilio_sms") twilioChannel := channelmodel.ChannelTwilioSMS{ AccountID: inbox.AccountID, InboxID: inbox.ID, AccountSID: "AC789", PhoneNumber: "+15550000000", MessagingServiceSID: "MG111", } require.NoError(t, db.Create(&twilioChannel).Error) h := NewTwilioWebhookHandler(nil, db) r := gin.New() r.POST("/webhooks/twilio/status", h.HandleTwilioDeliveryStatus) w := httptest.NewRecorder() form := url.Values{ "MessagingServiceSid": []string{"MG111"}, "MessageSid": []string{"SM789"}, "MessageStatus": []string{"delivered"}, } req, _ := http.NewRequest("POST", "/webhooks/twilio/status", strings.NewReader(form.Encode())) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") r.ServeHTTP(w, req) assert.Equal(t, http.StatusNoContent, w.Code) } func TestTwilioWebhookHandleDeliveryStatus_AccountSidFrom_Cov2(t *testing.T) { gin.SetMode(gin.TestMode) db := newWebhookLookupTestDB(t) inbox := seedWebhookInbox(t, db, "twilio_sms") twilioChannel := channelmodel.ChannelTwilioSMS{ AccountID: inbox.AccountID, InboxID: inbox.ID, AccountSID: "AC222", PhoneNumber: "+15553334444", } require.NoError(t, db.Create(&twilioChannel).Error) h := NewTwilioWebhookHandler(nil, db) r := gin.New() r.POST("/webhooks/twilio/status", h.HandleTwilioDeliveryStatus) w := httptest.NewRecorder() form := url.Values{ "AccountSid": []string{"AC222"}, "From": []string{"+15553334444"}, "MessageSid": []string{"SM999"}, "MessageStatus": []string{"failed"}, "ErrorCode": []string{"30007"}, "ErrorMessage": []string{"Carrier violation"}, } req, _ := http.NewRequest("POST", "/webhooks/twilio/status", strings.NewReader(form.Encode())) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") r.ServeHTTP(w, req) assert.Equal(t, http.StatusNoContent, w.Code) } // ============================================================ // Telegram webhook with successful inbox lookup // ============================================================ func TestTelegramWebhookHandle_Success_Cov2(t *testing.T) { gin.SetMode(gin.TestMode) db := newWebhookLookupTestDB(t) inbox := seedWebhookInbox(t, db, "telegram") telChannel := channelmodel.ChannelTelegram{ AccountID: inbox.AccountID, InboxID: inbox.ID, BotToken: "test-bot-token", } require.NoError(t, db.Create(&telChannel).Error) telProvider := channelprovider.NewTelegramProvider() telWebhook := telegramchannel.NewWebhookHandler(telProvider) h := NewTelegramWebhookHandler(telProvider, telWebhook, db) r := gin.New() r.POST("/webhooks/telegram/:bot_token", h.HandleTelegramWebhook) w := httptest.NewRecorder() body := `{"update_id":1,"message":{"message_id":1,"from":{"id":123,"first_name":"Test"},"chat":{"id":123,"first_name":"Test"},"date":1710000000,"text":"hello telegram"}}` req, _ := http.NewRequest("POST", "/webhooks/telegram/test-bot-token", strings.NewReader(body)) r.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) } // ============================================================ // LINE webhook with inbox lookup and valid body // ============================================================ func TestLineWebhookHandle_InboxNotFound_Cov2(t *testing.T) { gin.SetMode(gin.TestMode) db := newWebhookLookupTestDB(t) lineRepo := linechannel.NewRepository(db) lineSvc := linechannel.NewLineService(lineRepo) h := NewLineWebhookHandler(nil, nil, lineSvc, db) r := gin.New() r.POST("/webhooks/line/:line_channel_id", h.HandleLineWebhook) w := httptest.NewRecorder() body := `{"events":[]}` req, _ := http.NewRequest("POST", "/webhooks/line/nonexistent-channel-id", strings.NewReader(body)) r.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) } func TestLineWebhookHandle_ValidInbox_Cov2(t *testing.T) { gin.SetMode(gin.TestMode) db := newWebhookLookupTestDB(t) inbox := seedWebhookInbox(t, db, "line") lineChan := channelmodel.ChannelLINE{ AccountID: inbox.AccountID, InboxID: inbox.ID, ChannelID: "line-channel-123", } require.NoError(t, db.Create(&lineChan).Error) lineRepo := linechannel.NewRepository(db) lineSvc := linechannel.NewLineService(lineRepo) h := NewLineWebhookHandler(nil, nil, lineSvc, db) r := gin.New() r.POST("/webhooks/line/:line_channel_id", h.HandleLineWebhook) w := httptest.NewRecorder() body := `{"events":[]}` req, _ := http.NewRequest("POST", "/webhooks/line/line-channel-123", strings.NewReader(body)) r.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) } func TestLineWebhookHandle_InvalidJSON_Cov2(t *testing.T) { gin.SetMode(gin.TestMode) db := newWebhookLookupTestDB(t) inbox := seedWebhookInbox(t, db, "line") lineChan := channelmodel.ChannelLINE{ AccountID: inbox.AccountID, InboxID: inbox.ID, ChannelID: "line-channel-456", } require.NoError(t, db.Create(&lineChan).Error) h := NewLineWebhookHandler(nil, nil, nil, db) r := gin.New() r.POST("/webhooks/line/:line_channel_id", h.HandleLineWebhook) w := httptest.NewRecorder() body := `{invalid json` req, _ := http.NewRequest("POST", "/webhooks/line/line-channel-456", strings.NewReader(body)) r.ServeHTTP(w, req) assert.Equal(t, http.StatusBadRequest, w.Code) } // ============================================================ // Facebook HandleFacebookWebhook with valid inbox but nil providers // ============================================================ func TestFacebookWebhookHandle_ValidInbox_NilProviders_Cov2(t *testing.T) { gin.SetMode(gin.TestMode) db := newWebhookLookupTestDB(t) inbox := seedWebhookInbox(t, db, "facebook") fbChannel := channelmodel.ChannelFacebook{ AccountID: 1, InboxID: inbox.ID, PageID: "fb-page-webhook-test", } require.NoError(t, db.Create(&fbChannel).Error) h := NewFacebookWebhookHandler(nil, nil, db) r := gin.New() r.POST("/webhooks/facebook/:page_id", h.HandleFacebookWebhook) w := httptest.NewRecorder() body := []byte(`{"object":"page","entry":[{"id":"fb-page-webhook-test","time":1,"messaging":[{"sender":{"id":"user1"},"recipient":{"id":"fb-page-webhook-test"},"timestamp":1,"message":{"mid":"mid-fb-1","text":"hello fb"}}]}]}`) req, _ := http.NewRequest("POST", "/webhooks/facebook/fb-page-webhook-test", bytes.NewReader(body)) r.ServeHTTP(w, req) // Should return 200 (FB always gets 200) assert.Equal(t, http.StatusOK, w.Code) } func TestFacebookWebhookHandle_InvalidPayload_Cov2(t *testing.T) { gin.SetMode(gin.TestMode) db := newWebhookLookupTestDB(t) inbox := seedWebhookInbox(t, db, "facebook") fbChannel := channelmodel.ChannelFacebook{ AccountID: 1, InboxID: inbox.ID, PageID: "fb-page-invalid-payload", } require.NoError(t, db.Create(&fbChannel).Error) h := NewFacebookWebhookHandler(nil, nil, db) r := gin.New() r.POST("/webhooks/facebook/:page_id", h.HandleFacebookWebhook) w := httptest.NewRecorder() body := []byte(`{invalid json}`) req, _ := http.NewRequest("POST", "/webhooks/facebook/fb-page-invalid-payload", bytes.NewReader(body)) r.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) } // ============================================================ // Email webhook with full pipeline (nil pipeline → parse error path) // ============================================================ func TestEmailWebhookHandle_BodyReadError_Cov2(t *testing.T) { gin.SetMode(gin.TestMode) db := newWebhookLookupTestDB(t) inbox := seedWebhookInbox(t, db, "email") h := NewEmailWebhookHandler(nil, nil, db) r := gin.New() r.POST("/webhooks/email/:inbox_id", h.HandleEmailWebhook) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/webhooks/email/"+itoa(int(inbox.ID)), &failingReader{}) r.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) } // ============================================================ // Instagram webhook with non-instagram object // ============================================================ func TestInstagramWebhookHandle_NonInstagramObject_Cov2(t *testing.T) { gin.SetMode(gin.TestMode) db := newWebhookLookupTestDB(t) t.Setenv("INSTAGRAM_APP_SECRET", "ig-secret") h := NewFacebookWebhookHandler(nil, nil, db) r := gin.New() r.POST("/webhooks/instagram", h.HandleInstagramWebhook) body := []byte(`{"object":"page","entry":[{"id":"x","time":1,"messaging":[]}]}`) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/webhooks/instagram", bytes.NewReader(body)) req.Header.Set("X-Hub-Signature-256", metaSignature("ig-secret", body)) r.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) } func TestInstagramWebhookHandle_EmptyEvents_Cov2(t *testing.T) { gin.SetMode(gin.TestMode) db := newWebhookLookupTestDB(t) t.Setenv("INSTAGRAM_APP_SECRET", "ig-secret") h := NewFacebookWebhookHandler(nil, nil, db) r := gin.New() r.POST("/webhooks/instagram", h.HandleInstagramWebhook) body := []byte(`{"object":"instagram","entry":[]}`) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/webhooks/instagram", bytes.NewReader(body)) r.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) } // ============================================================ // TikTok webhook verification via path param // ============================================================ func TestTikTokWebhookHandle_MissingBusinessID_Cov2(t *testing.T) { gin.SetMode(gin.TestMode) t.Setenv("TIKTOK_APP_SECRET", "tiktok-secret") db := newWebhookLookupTestDB(t) h := NewTikTokWebhookHandler(nil, nil, db) r := gin.New() r.POST("/webhooks/tiktok", h.HandleTikTokWebhook) // Body without biz_id and no path param body := []byte(`{"type":"message.received","data":{"message_id":"x"}}`) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/webhooks/tiktok", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Tiktok-Signature", tiktokSignature("tiktok-secret", time.Now().Unix(), body)) r.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) } // ============================================================ // Shopify webhook with event processing (hook found) // ============================================================ func TestShopifyWebhookHandle_EventProcessing_Cov2(t *testing.T) { gin.SetMode(gin.TestMode) db := newWebhookLookupTestDB(t) settings, _ := json.Marshal(model.ShopifySettings{ShopDomain: "event-store.myshopify.com"}) hook := model.IntegrationHook{ AccountID: 1, HookType: model.HookTypeShopify, Status: model.HookStatusActive, AccessToken: "token", Settings: settings, } require.NoError(t, db.Create(&hook).Error) h := NewShopifyWebhookHandler(db, "secret") r := gin.New() r.POST("/webhooks/shopify", h.HandleShopifyWebhook) body := []byte(`{"id":12345,"email":"test@example.com"}`) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/webhooks/shopify", bytes.NewReader(body)) req.Header.Set("X-Shopify-Hmac-SHA256", shopifyHMAC("secret", body)) req.Header.Set("X-Shopify-Topic", "orders/create") req.Header.Set("X-Shopify-Shop-Domain", "event-store.myshopify.com") r.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) } // ============================================================ // IncomingPersister index helpers // ============================================================ func TestIndexContact_NilIndexer_Cov2(t *testing.T) { p := &IncomingPersister{} // nil indexer → should not panic p.indexContact(context.Background(), &model.Contact{}) } func TestIndexConversation_NilIndexer_Cov2(t *testing.T) { p := &IncomingPersister{} p.indexConversation(context.Background(), &model.Conversation{}) } func TestIndexMessage_NilIndexer_Cov2(t *testing.T) { p := &IncomingPersister{} p.indexMessage(context.Background(), &model.Message{}) } func TestIndexIncomingResult_NilIndexer_Cov2(t *testing.T) { p := &IncomingPersister{} result := &IncomingPersistResult{ Contact: &model.Contact{}, Conversation: &model.Conversation{}, Message: &model.Message{}, } p.indexIncomingResult(context.Background(), result) } func TestIndexIncomingResult_WithIndexer_Cov2(t *testing.T) { db := newWebhookLookupTestDB(t) indexer := &recordingIncomingSearchIndexer{} p := NewIncomingPersister(db).SetSearchIndexer(indexer) result := &IncomingPersistResult{ Contact: &model.Contact{Name: "test"}, Conversation: &model.Conversation{}, Message: &model.Message{Content: "test"}, } p.indexIncomingResult(context.Background(), result) assert.Len(t, indexer.contacts, 1) assert.Len(t, indexer.conversations, 1) assert.Len(t, indexer.messages, 1) } func TestDeliveryStatusContactID_NilDB_Cov2(t *testing.T) { p := &IncomingPersister{} id, err := p.deliveryStatusContactID(context.Background(), nil, &model.Message{Base: model.Base{ID: 1}}) _ = err _ = id } // itoa is a small helper to avoid importing strconv just for this func itoa(n int) string { if n == 0 { return "0" } var buf [20]byte pos := len(buf) neg := n < 0 if neg { n = -n } for n > 0 { pos-- buf[pos] = byte('0' + n%10) n /= 10 } if neg { pos-- buf[pos] = '-' } return string(buf[pos:]) }