package whatsapp import ( "bytes" "crypto/hmac" "crypto/sha256" "encoding/base64" "encoding/hex" "errors" "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" "github.com/gochat/gochat/internal/security" "gorm.io/driver/sqlite" "gorm.io/gorm" ) func newWhatsAppWebhookTestDB(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.ChannelWhatsApp{}); err != nil { t.Fatalf("migrate whatsapp models: %v", err) } return db } func TestWhatsAppWebhookLookupByVerifyToken(t *testing.T) { db := newWhatsAppWebhookTestDB(t) inbox := model.Inbox{AccountID: 1, Name: "wa", ChannelType: "whatsapp", ChannelID: 1, Enabled: true} if err := db.Create(&inbox).Error; err != nil { t.Fatalf("create inbox: %v", err) } channel := channelmodel.ChannelWhatsApp{ AccountID: 1, InboxID: inbox.ID, PhoneNumber: "+15551234567", PhoneNumberID: "phone-number-id", AccessToken: "access-token", Provider: "whatsapp_cloud", WebhookVerifyToken: "verify-token", ProviderConfig: `{"app_secret":"app-secret"}`, BusinessAccountID: "waba-id", WhatsAppAccountName: "WA", } if err := db.Create(&channel).Error; err != nil { t.Fatalf("create whatsapp channel: %v", err) } repo := NewRepository(db) provider := NewWhatsAppProvider(nil, repo, nil) h := NewWebhookHandler(provider) found, err := h.lookupByVerifyToken("verify-token") if err != nil { t.Fatalf("lookup verify token: %v", err) } if found.ID != channel.ID { t.Fatalf("unexpected channel id: %d", found.ID) } if secret := resolveCloudAppSecret(found); secret != "app-secret" { t.Fatalf("unexpected app secret: %q", secret) } } func TestWhatsAppWebhookLookupUsesDigestForEncryptedToken(t *testing.T) { db, err := gorm.Open(sqlite.Open("file:encrypted-whatsapp?mode=memory&cache=shared"), &gorm.Config{}) if err != nil { t.Fatal(err) } if err := db.AutoMigrate(&model.Inbox{}, &channelmodel.ChannelWhatsApp{}); err != nil { t.Fatal(err) } key := base64.StdEncoding.EncodeToString([]byte("11111111111111111111111111111111")) encryptor, err := security.NewEncryptor(security.EncryptionConfig{Enabled: true, AESKey: key, KeyVersion: 1}) if err != nil { t.Fatal(err) } if err := security.RegisterGORMEncryption(db, encryptor); err != nil { t.Fatal(err) } inbox := model.Inbox{AccountID: 1, Name: "encrypted-wa", ChannelType: "whatsapp", ChannelID: 1, Enabled: true} if err := db.Create(&inbox).Error; err != nil { t.Fatal(err) } record := channelmodel.ChannelWhatsApp{AccountID: 1, InboxID: inbox.ID, PhoneNumber: "+15551234568", AccessToken: "access-token", WebhookVerifyToken: "verify-token"} if err := db.Create(&record).Error; err != nil { t.Fatal(err) } handler := NewWebhookHandler(NewWhatsAppProvider(nil, NewRepository(db), nil)) found, err := handler.lookupByVerifyToken("verify-token") if err != nil { t.Fatal(err) } if found.ID != record.ID || found.WebhookVerifyToken != "verify-token" { t.Fatalf("unexpected encrypted lookup result: %#v", found) } } func TestWhatsAppCloudSignatureUsesAppSecret(t *testing.T) { gin.SetMode(gin.TestMode) body := []byte(`{"object":"whatsapp_business_account"}`) mac := hmac.New(sha256.New, []byte("app-secret")) mac.Write(body) signature := "sha256=" + hex.EncodeToString(mac.Sum(nil)) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Request = httptest.NewRequest(http.MethodPost, "/webhooks/whatsapp/phone", nil) c.Request.Header.Set("X-Hub-Signature-256", signature) h := NewWebhookHandler(nil) if err := h.verifyCloudSignature(c, body, "app-secret"); err != nil { t.Fatalf("verify signature: %v", err) } if err := h.verifyCloudSignature(c, body, "wrong-secret"); err == nil { t.Fatal("expected wrong app secret to fail") } } func TestWhatsAppWebhookRetriesWhenChannelConfigLookupFails(t *testing.T) { gin.SetMode(gin.TestMode) db := newWhatsAppWebhookTestDB(t) inbox := model.Inbox{AccountID: 1, Name: "wa-retry", ChannelType: "whatsapp", ChannelID: 1, Enabled: true} if err := db.Create(&inbox).Error; err != nil { t.Fatal(err) } channel := channelmodel.ChannelWhatsApp{ AccountID: 1, InboxID: inbox.ID, PhoneNumber: "+15550000000", PhoneNumberID: "retry-phone-id", AccessToken: "access-token", Provider: "whatsapp_cloud", } if err := db.Create(&channel).Error; err != nil { t.Fatal(err) } channelQueries := 0 temporaryErr := errors.New("temporary channel lookup failure") if err := db.Callback().Query().Before("gorm:query").Register("fail_second_channel_query", func(tx *gorm.DB) { if tx.Statement.Table == channel.TableName() { channelQueries++ if channelQueries == 2 { _ = tx.AddError(temporaryErr) } } }); err != nil { t.Fatal(err) } body := []byte(`{"object":"whatsapp_business_account","entry":[{"changes":[{"value":{"metadata":{"phone_number_id":"retry-phone-id"}}}]}]}`) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Request = httptest.NewRequest(http.MethodPost, "/webhooks/whatsapp", bytes.NewReader(body)) h := NewWebhookHandler(NewWhatsAppProvider(nil, NewRepository(db), nil)) h.HandleWebhookEvent(c) if w.Code != http.StatusServiceUnavailable { t.Fatalf("status = %d, want %d; body=%s", w.Code, http.StatusServiceUnavailable, w.Body.String()) } }