feat(webhook): finalize provider ingress parity

This commit is contained in:
2026-06-05 00:49:29 +08:00
parent 5b4982d87c
commit ebd8f081ca
4 changed files with 127 additions and 24 deletions
@@ -0,0 +1,59 @@
package v1
import (
"bytes"
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/gin-gonic/gin"
twitterchannel "github.com/gochat/gochat/internal/channel/twitter"
)
func TestTwitterWebhookCRCUsesConfiguredSecret(t *testing.T) {
gin.SetMode(gin.TestMode)
t.Setenv("TWITTER_CRC_SECRET", "crc-secret")
provider := twitterchannel.NewTwitterProvider(twitterchannel.TwitterOAuth2Config{})
h := NewTwitterChannelHandler(nil, provider, nil, nil)
r := gin.New()
r.GET("/webhooks/twitter", h.WebhookCRC)
req := httptest.NewRequest(http.MethodGet, "/webhooks/twitter?crc_token=crc-token", nil)
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())
}
mac := hmac.New(sha256.New, []byte("crc-secret"))
mac.Write([]byte("crc-token"))
expected := "sha256=" + base64.StdEncoding.EncodeToString(mac.Sum(nil))
var payload map[string]string
if err := json.Unmarshal(w.Body.Bytes(), &payload); err != nil {
t.Fatalf("decode crc response: %v", err)
}
if payload["response_token"] != expected {
t.Fatalf("unexpected response token: %q", payload["response_token"])
}
}
func TestTwitterWebhookEventAcknowledgesPayload(t *testing.T) {
gin.SetMode(gin.TestMode)
provider := twitterchannel.NewTwitterProvider(twitterchannel.TwitterOAuth2Config{})
h := NewTwitterChannelHandler(nil, provider, nil, nil)
r := gin.New()
r.POST("/webhooks/twitter", h.WebhookEvent)
req := httptest.NewRequest(http.MethodPost, "/webhooks/twitter", bytes.NewReader([]byte(`{"direct_message_events":[]}`)))
req.Header.Set("Content-Type", "application/json")
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())
}
}
@@ -792,6 +792,45 @@ func TestInstagramWebhookEventsVerifySignatureAndResolveInbox(t *testing.T) {
assertPersistedMessage(t, db, inbox.ID, "mid-1", "hello")
}
func TestInstagramWebhookRejectsMissingSignature(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-missing-sig","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))
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
if w.Code != http.StatusUnauthorized {
t.Fatalf("expected 401, got %d body=%s", w.Code, w.Body.String())
}
var count int64
if err := db.Model(&model.Message{}).Where("inbox_id = ? AND source_id = ?", inbox.ID, "mid-missing-sig").Count(&count).Error; err != nil {
t.Fatalf("count message: %v", err)
}
if count != 0 {
t.Fatalf("expected no persisted message, got %d", count)
}
}
func assertPersistedMessage(t *testing.T, db *gorm.DB, inboxID uint, sourceID string, content string) model.Message {
t.Helper()
var message model.Message