//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") }