Files
gochat/internal/handler/webhook/webhook_lookup_test.go
T

273 lines
8.1 KiB
Go

package webhook
import (
"bytes"
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/gin-gonic/gin"
"github.com/gochat/gochat/internal/model"
channelmodel "github.com/gochat/gochat/internal/model/channel"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
func newWebhookLookupTestDB(t *testing.T) *gorm.DB {
t.Helper()
db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{})
if err != nil {
t.Fatalf("open sqlite: %v", err)
}
if err := db.AutoMigrate(
&model.Inbox{},
&channelmodel.ChannelTelegram{},
&channelmodel.ChannelLINE{},
&channelmodel.ChannelTwilioSMS{},
&channelmodel.ChannelTikTok{},
&channelmodel.ChannelInstagram{},
&model.IntegrationHook{},
); err != nil {
t.Fatalf("migrate webhook lookup models: %v", err)
}
return db
}
func shopifyHMAC(secret string, body []byte) string {
mac := hmac.New(sha256.New, []byte(secret))
mac.Write(body)
return base64.StdEncoding.EncodeToString(mac.Sum(nil))
}
func metaSignature(secret string, body []byte) string {
mac := hmac.New(sha256.New, []byte(secret))
mac.Write(body)
return "sha256=" + hex.EncodeToString(mac.Sum(nil))
}
func seedWebhookInbox(t *testing.T, db *gorm.DB, channelType string) model.Inbox {
t.Helper()
inbox := model.Inbox{
AccountID: 1,
Name: channelType + " inbox",
ChannelType: channelType,
ChannelID: 1,
Enabled: true,
}
if err := db.Create(&inbox).Error; err != nil {
t.Fatalf("create inbox: %v", err)
}
return inbox
}
func TestTelegramWebhookLookupInboxByBotToken(t *testing.T) {
db := newWebhookLookupTestDB(t)
inbox := seedWebhookInbox(t, db, "telegram")
channel := channelmodel.ChannelTelegram{
AccountID: 1,
InboxID: inbox.ID,
BotToken: "123:secret-token",
BotName: "support_bot",
}
if err := db.Create(&channel).Error; err != nil {
t.Fatalf("create telegram channel: %v", err)
}
h := NewTelegramWebhookHandler(nil, nil, db)
found, err := h.lookupInbox("123:secret-token")
if err != nil {
t.Fatalf("lookup inbox: %v", err)
}
if found.ID != inbox.ID || found.ChannelType != "telegram" {
t.Fatalf("unexpected inbox: id=%d type=%s", found.ID, found.ChannelType)
}
}
func TestLineWebhookLookupInboxByLineChannelID(t *testing.T) {
db := newWebhookLookupTestDB(t)
inbox := seedWebhookInbox(t, db, "line")
channel := channelmodel.ChannelLINE{
AccountID: 1,
InboxID: inbox.ID,
ChannelID: "line-channel-1",
Name: "LINE OA",
}
if err := db.Create(&channel).Error; err != nil {
t.Fatalf("create line channel: %v", err)
}
h := NewLineWebhookHandler(nil, nil, nil, db)
found, err := h.lookupInboxByLineChannelID("line-channel-1")
if err != nil {
t.Fatalf("lookup inbox: %v", err)
}
if found.ID != inbox.ID || found.ChannelType != "line" {
t.Fatalf("unexpected inbox: id=%d type=%s", found.ID, found.ChannelType)
}
}
func TestTwilioWebhookLookupInboxByPhoneNumber(t *testing.T) {
db := newWebhookLookupTestDB(t)
inbox := seedWebhookInbox(t, db, "twilio_sms")
channel := channelmodel.ChannelTwilioSMS{
AccountID: 1,
InboxID: inbox.ID,
AccountSID: "AC123",
PhoneNumber: "+15551234567",
MessagingServiceSID: "MG123",
}
if err := db.Create(&channel).Error; err != nil {
t.Fatalf("create twilio channel: %v", err)
}
h := NewTwilioWebhookHandler(nil, db)
found, err := h.lookupInboxByPhoneNumber("+15551234567")
if err != nil {
t.Fatalf("lookup inbox: %v", err)
}
if found.ID != inbox.ID || found.ChannelType != "twilio_sms" {
t.Fatalf("unexpected inbox: id=%d type=%s", found.ID, found.ChannelType)
}
}
func TestTikTokWebhookLookupInboxByBusinessIDAndPayloadExtractor(t *testing.T) {
db := newWebhookLookupTestDB(t)
inbox := seedWebhookInbox(t, db, "tiktok")
channel := channelmodel.ChannelTikTok{
AccountID: 1,
InboxID: inbox.ID,
TikTokBusinessID: "biz-123",
WebhookVerifyToken: "verify-token",
}
if err := db.Create(&channel).Error; err != nil {
t.Fatalf("create tiktok channel: %v", err)
}
h := NewTikTokWebhookHandler(nil, nil, db)
found, err := h.lookupInboxByBusinessID("biz-123")
if err != nil {
t.Fatalf("lookup inbox: %v", err)
}
if found.ID != inbox.ID || found.ChannelType != "tiktok" {
t.Fatalf("unexpected inbox: id=%d type=%s", found.ID, found.ChannelType)
}
if got := extractTikTokBusinessID([]byte(`{"data":{"business_id":"biz-123"}}`)); got != "biz-123" {
t.Fatalf("unexpected extracted business id: %s", got)
}
}
func TestShopifyWebhookShopRedactDeletesMatchingHook(t *testing.T) {
gin.SetMode(gin.TestMode)
db := newWebhookLookupTestDB(t)
settings, _ := json.Marshal(model.ShopifySettings{ShopDomain: "store.myshopify.com"})
hook := model.IntegrationHook{
AccountID: 1,
HookType: model.HookTypeShopify,
Status: model.HookStatusActive,
AccessToken: "access-token",
Settings: settings,
}
if err := db.Create(&hook).Error; err != nil {
t.Fatalf("create shopify hook: %v", err)
}
body := []byte(`{"shop_domain":"store.myshopify.com"}`)
h := NewShopifyWebhookHandler(db, "client-secret")
r := gin.New()
r.POST("/webhooks/shopify", h.HandleShopifyWebhook)
req := httptest.NewRequest(http.MethodPost, "/webhooks/shopify", bytes.NewReader(body))
req.Header.Set("X-Shopify-Hmac-SHA256", shopifyHMAC("client-secret", body))
req.Header.Set("X-Shopify-Topic", "shop/redact")
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Fatalf("expected 200, got %d", w.Code)
}
var count int64
if err := db.Model(&model.IntegrationHook{}).Where("id = ?", hook.ID).Count(&count).Error; err != nil {
t.Fatalf("count hook: %v", err)
}
if count != 0 {
t.Fatalf("expected shopify hook to be deleted, count=%d", count)
}
}
func TestShopifyWebhookRejectsInvalidHMAC(t *testing.T) {
gin.SetMode(gin.TestMode)
db := newWebhookLookupTestDB(t)
h := NewShopifyWebhookHandler(db, "client-secret")
r := gin.New()
r.POST("/webhooks/shopify", h.HandleShopifyWebhook)
req := httptest.NewRequest(http.MethodPost, "/webhooks/shopify", bytes.NewReader([]byte(`{}`)))
req.Header.Set("X-Shopify-Hmac-SHA256", "invalid")
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
if w.Code != http.StatusUnauthorized {
t.Fatalf("expected 401, got %d", w.Code)
}
}
func TestInstagramWebhookVerificationUsesChatwootGlobalTokens(t *testing.T) {
gin.SetMode(gin.TestMode)
t.Setenv("INSTAGRAM_VERIFY_TOKEN", "verify-me")
h := NewFacebookWebhookHandler(nil, nil, nil)
r := gin.New()
r.GET("/webhooks/instagram", h.HandleInstagramVerification)
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/webhooks/instagram?hub.verify_token=verify-me&hub.challenge=challenge-1", nil)
r.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Fatalf("expected 200, got %d", w.Code)
}
if w.Body.String() != "challenge-1" {
t.Fatalf("unexpected challenge body: %q", w.Body.String())
}
}
func TestInstagramWebhookEventsVerifySignatureAndResolveInbox(t *testing.T) {
gin.SetMode(gin.TestMode)
db := newWebhookLookupTestDB(t)
inbox := seedWebhookInbox(t, db, "instagram")
channel := channelmodel.ChannelInstagram{
AccountID: 1,
InboxID: inbox.ID,
InstagramAccountID: "ig-123",
InstagramBusinessAccountID: "ig-business-123",
PageAccessToken: "page-token",
ConnectedFBPageID: "page-123",
InstagramAccountName: "gochat",
}
if err := db.Create(&channel).Error; err != nil {
t.Fatalf("create instagram channel: %v", err)
}
t.Setenv("INSTAGRAM_APP_SECRET", "ig-secret")
body := []byte(`{"object":"instagram","entry":[{"id":"ig-123","time":1,"messaging":[{"sender":{"id":"user-1"},"recipient":{"id":"ig-123"},"timestamp":1,"message":{"mid":"mid-1","text":"hello"}}]}]}`)
h := NewFacebookWebhookHandler(nil, nil, db)
r := gin.New()
r.POST("/webhooks/instagram", h.HandleInstagramWebhook)
req := httptest.NewRequest(http.MethodPost, "/webhooks/instagram", bytes.NewReader(body))
req.Header.Set("X-Hub-Signature-256", metaSignature("ig-secret", body))
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Fatalf("expected 200, got %d body=%s", w.Code, w.Body.String())
}
}