feat(webhook): implement instagram and shopify ingress
This commit is contained in:
@@ -1,8 +1,17 @@
|
||||
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"
|
||||
@@ -22,12 +31,26 @@ func newWebhookLookupTestDB(t *testing.T) *gorm.DB {
|
||||
&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()
|
||||
|
||||
@@ -140,3 +163,110 @@ func TestTikTokWebhookLookupInboxByBusinessIDAndPayloadExtractor(t *testing.T) {
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user