veza/veza-backend-api/tests/integration/webhook_security_test.go
senke 7cb4ef56e1
Some checks failed
Backend API CI / test-unit (push) Failing after 0s
Backend API CI / test-integration (push) Failing after 0s
feat(v0.912): Cashflow - payment E2E integration tests
- Add MarketplaceServiceOverride and AuthMiddlewareOverride to config for tests
- Wire overrides in routes_webhooks and routes_marketplace (authForMarketplaceInterface)
- payment_flow_test: cart -> checkout -> webhook -> order completed, license, transfer
- webhook_idempotency_test: 3 identical webhooks -> 1 order, 1 license
- webhook_security_test: empty secret 500, invalid sig 401, valid sig 200
- refund_flow_test: completed order -> refund -> order refunded, license revoked
- Shared computeWebhookSignature helper in webhook_test_helpers.go
- SetMaxOpenConns(1) for sqlite :memory: in idempotency test to avoid flakiness

Ref: docs/ROADMAP_V09XX_TO_V1.md v0.912 Cashflow
2026-02-27 20:00:51 +01:00

126 lines
4.1 KiB
Go

//go:build integration
// +build integration
package integration
import (
"bytes"
"net/http"
"net/http/httptest"
"os"
"testing"
"time"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/zap"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
"veza-backend-api/internal/api"
"veza-backend-api/internal/config"
"veza-backend-api/internal/core/marketplace"
"veza-backend-api/internal/database"
"veza-backend-api/internal/metrics"
"veza-backend-api/internal/models"
)
func setupWebhookSecurityRouter(t *testing.T, webhookSecret string) *gin.Engine {
t.Helper()
os.Setenv("ENABLE_CLAMAV", "false")
os.Setenv("CLAMAV_REQUIRED", "false")
gin.SetMode(gin.TestMode)
router := gin.New()
gormDB, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
require.NoError(t, err)
require.NoError(t, gormDB.AutoMigrate(
&models.User{},
&models.Track{},
&marketplace.Product{},
&marketplace.Order{},
&marketplace.OrderItem{},
&marketplace.License{},
&marketplace.SellerTransfer{},
))
sqlDB, err := gormDB.DB()
require.NoError(t, err)
vezaDB := &database.Database{
DB: sqlDB,
GormDB: gormDB,
Logger: zap.NewNop(),
}
cfg := &config.Config{
HyperswitchWebhookSecret: webhookSecret,
JWTSecret: "test-jwt-secret-key-minimum-32-characters-long",
JWTIssuer: "veza-api",
JWTAudience: "veza-app",
Logger: zap.NewNop(),
RedisClient: nil,
ErrorMetrics: metrics.NewErrorMetrics(),
UploadDir: "uploads/test",
Env: "development",
Database: vezaDB,
CORSOrigins: []string{"*"},
HandlerTimeout: 30 * time.Second,
RateLimitLimit: 100,
RateLimitWindow: 60,
AuthRateLimitLoginAttempts: 10,
AuthRateLimitLoginWindow: 15,
}
require.NoError(t, cfg.InitServicesForTest())
require.NoError(t, cfg.InitMiddlewaresForTest())
apiRouter := api.NewAPIRouter(vezaDB, cfg)
require.NoError(t, apiRouter.Setup(router))
return router
}
// TestWebhookSecurity_EmptySecret_Returns500 verifies that when HyperswitchWebhookSecret
// is empty, the webhook handler returns 500 (VEZA-SEC-005).
func TestWebhookSecurity_EmptySecret_Returns500(t *testing.T) {
router := setupWebhookSecurityRouter(t, "")
req := httptest.NewRequest(http.MethodPost, "/api/v1/webhooks/hyperswitch", bytes.NewReader([]byte(`{"test": true}`)))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusInternalServerError, w.Code, "empty webhook secret must return 500")
}
// TestWebhookSecurity_InvalidSignature_Returns401 verifies that with a configured secret,
// invalid or missing signature returns 401.
func TestWebhookSecurity_InvalidSignature_Returns401(t *testing.T) {
router := setupWebhookSecurityRouter(t, "test-secret-at-least-32-chars-long")
req := httptest.NewRequest(http.MethodPost, "/api/v1/webhooks/hyperswitch", bytes.NewReader([]byte(`{"payment_id":"pay_123","status":"succeeded"}`)))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("x-webhook-signature-512", "invalid_signature")
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusUnauthorized, w.Code, "invalid signature must return 401")
}
// TestWebhookSecurity_ValidSignature_Returns200 verifies that with a configured secret
// and valid HMAC-SHA512 signature, the webhook returns 200.
func TestWebhookSecurity_ValidSignature_Returns200(t *testing.T) {
secret := "test-secret-at-least-32-chars-long"
payload := []byte(`{"payment_id":"pay_nonexistent","status":"succeeded"}`)
signature := computeWebhookSignature(payload, secret)
router := setupWebhookSecurityRouter(t, secret)
req := httptest.NewRequest(http.MethodPost, "/api/v1/webhooks/hyperswitch", bytes.NewReader(payload))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("x-webhook-signature-512", signature)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code, "valid signature must return 200")
}