1658 lines
55 KiB
Go
1658 lines
55 KiB
Go
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(), "<Response>")
|
|
}
|
|
|
|
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:])
|
|
}
|