//go:build integration // +build integration package integration import ( "bytes" "context" "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/middleware" "veza-backend-api/internal/models" "veza-backend-api/internal/services" ) // mockPaymentProvider returns fixed payment_id for CreatePayment (no real Hyperswitch call) type mockPaymentProvider struct { paymentID string clientSecret string } func (m *mockPaymentProvider) CreatePayment(_ context.Context, _ int64, _ string, _ string, _ string, _ map[string]string) (string, string, error) { return m.paymentID, m.clientSecret, nil } func (m *mockPaymentProvider) GetPayment(_ context.Context, _ string) (string, error) { return "succeeded", nil } // mockTransferService records CreateTransfer calls type mockTransferService struct { calls []struct { SellerID uuid.UUID Amount int64 Currency string OrderID string } } func (m *mockTransferService) CreateTransfer(_ context.Context, sellerUserID uuid.UUID, amount int64, currency, orderID string) error { m.calls = append(m.calls, struct { SellerID uuid.UUID Amount int64 Currency string OrderID string }{sellerUserID, amount, currency, orderID}) return nil } // testAuthMiddleware reads X-User-ID header and sets user_id in context (for integration tests) type testAuthMiddleware struct{} func (t *testAuthMiddleware) RequireAuth() gin.HandlerFunc { return func(c *gin.Context) { if userIDStr := c.GetHeader("X-User-ID"); userIDStr != "" { if userID, err := uuid.Parse(userIDStr); err == nil { c.Set("user_id", userID) c.Next() return } } c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) } } func (t *testAuthMiddleware) RequireContentCreatorRole() gin.HandlerFunc { return t.RequireAuth() } func (t *testAuthMiddleware) RequireOwnershipOrAdmin(_ string, _ middleware.ResourceOwnerResolver) gin.HandlerFunc { return t.RequireAuth() } func (t *testAuthMiddleware) RequirePermission(_ string) gin.HandlerFunc { return t.RequireAuth() } func (t *testAuthMiddleware) RequireAdmin() gin.HandlerFunc { return t.RequireAuth() } func (t *testAuthMiddleware) OptionalAuth() gin.HandlerFunc { return func(c *gin.Context) { c.Next() } } func (t *testAuthMiddleware) RefreshToken() gin.HandlerFunc { return func(c *gin.Context) { c.AbortWithStatus(http.StatusNotImplemented) } } func setupPaymentFlowRouter(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()) cfg.AuthMiddlewareOverride = &testAuthMiddleware{} apiRouter := api.NewAPIRouter(vezaDB, cfg) require.NoError(t, apiRouter.Setup(router)) return router } // TestPaymentFlow_E2E_CartCheckoutWebhook verifies the full payment flow: // add to cart -> checkout -> webhook payment_succeeded -> order completed, license created, transfer initiated func TestPaymentFlow_E2E_CartCheckoutWebhook(t *testing.T) { db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) require.NoError(t, err) require.NoError(t, db.AutoMigrate( &models.User{}, &models.Track{}, &marketplace.Product{}, &marketplace.Order{}, &marketplace.OrderItem{}, &marketplace.License{}, &marketplace.SellerTransfer{}, )) // CartItem uses gen_random_uuid() (PostgreSQL) — create manually for SQLite require.NoError(t, db.Exec(` CREATE TABLE IF NOT EXISTS cart_items ( id TEXT PRIMARY KEY, user_id TEXT NOT NULL, product_id TEXT NOT NULL, quantity INTEGER DEFAULT 1, created_at DATETIME, updated_at DATETIME ) `).Error) db.Callback().Create().Before("gorm:create").Register("generate_uuid_cart_item", func(d *gorm.DB) { if item, ok := d.Statement.Dest.(*marketplace.CartItem); ok && item.ID == uuid.Nil { item.ID = uuid.New() } }) 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) mockPay := &mockPaymentProvider{paymentID: "pay_mock_123", clientSecret: "pi_secret_xxx"} mockTransfer := &mockTransferService{} storageService := services.NewTrackStorageService("uploads/test", false, zap.NewNop()) marketService := marketplace.NewService(db, zap.NewNop(), storageService, marketplace.WithPaymentProvider(mockPay), marketplace.WithHyperswitchConfig(true, "/purchases"), marketplace.WithTransferService(mockTransfer, 0.10), ) router := setupPaymentFlowRouter(t, db, marketService) // Step 1: Add to cart addCartBody, _ := json.Marshal(map[string]interface{}{ "product_id": product.ID.String(), "quantity": 1, }) req := httptest.NewRequest(http.MethodPost, "/api/v1/commerce/cart/items", bytes.NewReader(addCartBody)) req.Header.Set("Content-Type", "application/json") req.Header.Set("X-User-ID", buyerID.String()) w := httptest.NewRecorder() router.ServeHTTP(w, req) require.Equal(t, http.StatusCreated, w.Code, "add to cart must succeed: %s", w.Body.String()) // Step 2: Checkout req = httptest.NewRequest(http.MethodPost, "/api/v1/commerce/cart/checkout", bytes.NewReader([]byte("{}"))) req.Header.Set("Content-Type", "application/json") req.Header.Set("X-User-ID", buyerID.String()) w = httptest.NewRecorder() router.ServeHTTP(w, req) require.Equal(t, http.StatusCreated, w.Code, "checkout must succeed: %s", w.Body.String()) var checkoutResp map[string]interface{} require.NoError(t, json.Unmarshal(w.Body.Bytes(), &checkoutResp)) data, ok := checkoutResp["data"].(map[string]interface{}) require.True(t, ok) orderData, ok := data["order"].(map[string]interface{}) require.True(t, ok) orderIDStr, ok := orderData["id"].(string) require.True(t, ok) assert.NotEmpty(t, data["client_secret"], "client_secret must be present") assert.Equal(t, "pay_mock_123", data["payment_id"], "payment_id must match mock") // Step 3: Simulate webhook payment_succeeded payload, _ := json.Marshal(map[string]string{ "payment_id": "pay_mock_123", "status": "succeeded", }) signature := computeWebhookSignature(payload, "test-secret-at-least-32-chars-long") 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 must succeed: %s", w.Body.String()) // Assertions: order completed, license created, transfer initiated orderID, _ := uuid.Parse(orderIDStr) var order marketplace.Order require.NoError(t, db.First(&order, orderID).Error) assert.Equal(t, "completed", order.Status) var licenses []marketplace.License require.NoError(t, db.Where("order_id = ?", orderID).Find(&licenses).Error) assert.Len(t, licenses, 1, "must have 1 license") assert.Len(t, mockTransfer.calls, 1, "transfer must be initiated") assert.Equal(t, sellerID, mockTransfer.calls[0].SellerID) }