//go:build integration // +build integration package integration import ( "bytes" "encoding/json" "net/http" "net/http/httptest" "os" "testing" "time" "github.com/gin-gonic/gin" "github.com/google/uuid" "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" "veza-backend-api/internal/services" ) func setupWebhookIdempotencyRouter(t *testing.T, db *gorm.DB, marketService *marketplace.Service) *gin.Engine { t.Helper() os.Setenv("ENABLE_CLAMAV", "false") os.Setenv("CLAMAV_REQUIRED", "false") gin.SetMode(gin.TestMode) router := gin.New() sqlDB, err := db.DB() require.NoError(t, err) vezaDB := &database.Database{ DB: sqlDB, GormDB: db, Logger: zap.NewNop(), } cfg := &config.Config{ HyperswitchWebhookSecret: "test-secret-at-least-32-chars-long", 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, MarketplaceServiceOverride: marketService, } require.NoError(t, cfg.InitServicesForTest()) require.NoError(t, cfg.InitMiddlewaresForTest()) apiRouter := api.NewAPIRouter(vezaDB, cfg) require.NoError(t, apiRouter.Setup(router)) return router } // TestWebhookIdempotency_ThreeIdenticalWebhooks_CreatesOneOrder verifies that sending // the same payment_succeeded webhook 3 times results in 1 completed order and 1 license. func TestWebhookIdempotency_ThreeIdenticalWebhooks_CreatesOneOrder(t *testing.T) { db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) require.NoError(t, err) // Ensure single connection for sqlite :memory: (each new connection gets empty DB) sqlDB, _ := db.DB() sqlDB.SetMaxOpenConns(1) require.NoError(t, db.AutoMigrate( &models.User{}, &models.Track{}, &marketplace.Product{}, &marketplace.Order{}, &marketplace.OrderItem{}, &marketplace.License{}, &marketplace.SellerTransfer{}, )) buyerID := uuid.New() sellerID := uuid.New() trackID := uuid.New() require.NoError(t, db.Create(&models.User{ID: buyerID}).Error) require.NoError(t, db.Create(&models.User{ID: sellerID}).Error) require.NoError(t, db.Create(&models.Track{ID: trackID, UserID: sellerID, FilePath: "/test.mp3"}).Error) product := &marketplace.Product{ ID: uuid.New(), SellerID: sellerID, Title: "Test Product", Price: 9.99, ProductType: "track", TrackID: &trackID, Status: marketplace.ProductStatusActive, } require.NoError(t, db.Create(product).Error) order := &marketplace.Order{ ID: uuid.New(), BuyerID: buyerID, TotalAmount: 9.99, Currency: "EUR", Status: "pending", HyperswitchPaymentID: "pay_idem_123", } require.NoError(t, db.Create(order).Error) require.NoError(t, db.Create(&marketplace.OrderItem{ ID: uuid.New(), OrderID: order.ID, ProductID: product.ID, Price: 9.99, }).Error) storageService := services.NewTrackStorageService("uploads/test", false, zap.NewNop()) marketService := marketplace.NewService(db, zap.NewNop(), storageService) router := setupWebhookIdempotencyRouter(t, db, marketService) payload, _ := json.Marshal(map[string]string{ "payment_id": "pay_idem_123", "status": "succeeded", }) signature := computeWebhookSignature(payload, "test-secret-at-least-32-chars-long") for i := 0; i < 3; i++ { 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, "webhook call %d must return 200", i+1) } var orders []marketplace.Order require.NoError(t, db.Where("hyperswitch_payment_id = ?", "pay_idem_123").Find(&orders).Error) assert.Len(t, orders, 1, "must have exactly 1 order") assert.Equal(t, "completed", orders[0].Status, "order must be completed") var licenses []marketplace.License require.NoError(t, db.Where("order_id = ?", order.ID).Find(&licenses).Error) assert.Len(t, licenses, 1, "must have exactly 1 license") }