- Stripe Connect: onboarding, balance, SellerDashboardView - Interceptors: auth.ts, error.ts extracted, facade - Grafana: dashboards enriched (p50, top endpoints, 4xx, WS, commerce) - E2E commerce: product->order->review->invoice - SMOKE_TEST_V0602, RETROSPECTIVE_V0602, PAYOUT_MANUAL - Archive V0_602 scope, V0_603 placeholder, SCOPE_CONTROL v0.603 - Fix sanitizer regex (Go no backreferences) - Marketplace test schema: product_licenses, product_images, orders, licenses
793 lines
25 KiB
Go
793 lines
25 KiB
Go
//go:build integration || marketplace
|
|
// +build integration marketplace
|
|
|
|
package marketplace
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
"time"
|
|
|
|
"veza-backend-api/internal/core/marketplace"
|
|
"veza-backend-api/internal/database"
|
|
"veza-backend-api/internal/handlers"
|
|
"veza-backend-api/internal/models"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/google/uuid"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"go.uber.org/zap/zaptest"
|
|
"gorm.io/driver/sqlite"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
// MockStorageService is a mock implementation of StorageService
|
|
type MockStorageService struct {
|
|
GetDownloadURLFunc func(filePath string) (string, error)
|
|
}
|
|
|
|
func (m *MockStorageService) GetDownloadURL(ctx context.Context, filePath string) (string, error) {
|
|
if m.GetDownloadURLFunc != nil {
|
|
return m.GetDownloadURLFunc(filePath)
|
|
}
|
|
return fmt.Sprintf("/download/%s", filePath), nil
|
|
}
|
|
|
|
// setupMarketplaceFlowTestRouter crée un router de test avec les services nécessaires pour les tests de flux marketplace
|
|
func setupMarketplaceFlowTestRouter(t *testing.T) (*gin.Engine, *gorm.DB, *database.Database, *marketplace.Service, func()) {
|
|
gin.SetMode(gin.TestMode)
|
|
logger := zaptest.NewLogger(t)
|
|
|
|
// Setup in-memory SQLite database
|
|
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
|
|
require.NoError(t, err)
|
|
db.Exec("PRAGMA foreign_keys = ON")
|
|
|
|
// Auto-migrate models (User and Track use standard GORM)
|
|
err = db.AutoMigrate(
|
|
&models.User{},
|
|
&models.Track{},
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
// Create marketplace tables manually for SQLite compatibility (gen_random_uuid() is PostgreSQL-specific)
|
|
// Products table
|
|
err = db.Exec(`
|
|
CREATE TABLE IF NOT EXISTS products (
|
|
id TEXT PRIMARY KEY,
|
|
seller_id TEXT NOT NULL,
|
|
title TEXT NOT NULL,
|
|
description TEXT,
|
|
price REAL NOT NULL,
|
|
currency TEXT DEFAULT 'EUR',
|
|
status TEXT DEFAULT 'draft',
|
|
product_type TEXT NOT NULL,
|
|
track_id TEXT,
|
|
license_type TEXT,
|
|
bpm INTEGER,
|
|
musical_key TEXT,
|
|
category TEXT,
|
|
created_at DATETIME,
|
|
updated_at DATETIME,
|
|
deleted_at DATETIME
|
|
)
|
|
`).Error
|
|
require.NoError(t, err)
|
|
|
|
// Orders table
|
|
err = db.Exec(`
|
|
CREATE TABLE IF NOT EXISTS orders (
|
|
id TEXT PRIMARY KEY,
|
|
buyer_id TEXT NOT NULL,
|
|
total_amount REAL NOT NULL,
|
|
currency TEXT DEFAULT 'EUR',
|
|
status TEXT DEFAULT 'pending',
|
|
payment_intent TEXT,
|
|
hyperswitch_payment_id TEXT,
|
|
payment_status TEXT DEFAULT 'pending',
|
|
promo_code_id TEXT,
|
|
discount_amount_cents INTEGER DEFAULT 0,
|
|
created_at DATETIME,
|
|
updated_at DATETIME
|
|
)
|
|
`).Error
|
|
require.NoError(t, err)
|
|
|
|
// Product images table (for Preload in CreateProduct response)
|
|
err = db.Exec(`
|
|
CREATE TABLE IF NOT EXISTS product_images (
|
|
id TEXT PRIMARY KEY,
|
|
product_id TEXT NOT NULL,
|
|
url TEXT NOT NULL,
|
|
sort_order INTEGER DEFAULT 0,
|
|
created_at DATETIME,
|
|
FOREIGN KEY(product_id) REFERENCES products(id) ON DELETE CASCADE
|
|
)
|
|
`).Error
|
|
require.NoError(t, err)
|
|
|
|
// Order items table
|
|
err = db.Exec(`
|
|
CREATE TABLE IF NOT EXISTS order_items (
|
|
id TEXT PRIMARY KEY,
|
|
order_id TEXT NOT NULL,
|
|
product_id TEXT NOT NULL,
|
|
price REAL NOT NULL,
|
|
FOREIGN KEY(order_id) REFERENCES orders(id) ON DELETE CASCADE
|
|
)
|
|
`).Error
|
|
require.NoError(t, err)
|
|
|
|
// Product licenses table (license types per product)
|
|
err = db.Exec(`
|
|
CREATE TABLE IF NOT EXISTS product_licenses (
|
|
id TEXT PRIMARY KEY,
|
|
product_id TEXT NOT NULL,
|
|
license_type TEXT NOT NULL,
|
|
price_cents INTEGER NOT NULL,
|
|
terms_text TEXT,
|
|
created_at DATETIME,
|
|
FOREIGN KEY(product_id) REFERENCES products(id) ON DELETE CASCADE
|
|
)
|
|
`).Error
|
|
require.NoError(t, err)
|
|
|
|
// Licenses table (purchased licenses)
|
|
err = db.Exec(`
|
|
CREATE TABLE IF NOT EXISTS licenses (
|
|
id TEXT PRIMARY KEY,
|
|
buyer_id TEXT NOT NULL,
|
|
track_id TEXT NOT NULL,
|
|
product_id TEXT NOT NULL,
|
|
order_id TEXT NOT NULL,
|
|
type TEXT NOT NULL,
|
|
rights TEXT,
|
|
downloads_left INTEGER DEFAULT 3,
|
|
created_at DATETIME,
|
|
expires_at DATETIME,
|
|
revoked_at DATETIME
|
|
)
|
|
`).Error
|
|
require.NoError(t, err)
|
|
|
|
// Add BeforeCreate hooks for SQLite compatibility to generate UUIDs
|
|
db.Callback().Create().Before("gorm:create").Register("generate_uuid_product", func(db *gorm.DB) {
|
|
if product, ok := db.Statement.Dest.(*marketplace.Product); ok && product.ID == uuid.Nil {
|
|
product.ID = uuid.New()
|
|
}
|
|
if products, ok := db.Statement.Dest.(*[]marketplace.Product); ok {
|
|
for i := range *products {
|
|
if (*products)[i].ID == uuid.Nil {
|
|
(*products)[i].ID = uuid.New()
|
|
}
|
|
}
|
|
}
|
|
})
|
|
db.Callback().Create().Before("gorm:create").Register("generate_uuid_order", func(db *gorm.DB) {
|
|
if order, ok := db.Statement.Dest.(*marketplace.Order); ok && order.ID == uuid.Nil {
|
|
order.ID = uuid.New()
|
|
}
|
|
if orders, ok := db.Statement.Dest.(*[]marketplace.Order); ok {
|
|
for i := range *orders {
|
|
if (*orders)[i].ID == uuid.Nil {
|
|
(*orders)[i].ID = uuid.New()
|
|
}
|
|
}
|
|
}
|
|
})
|
|
db.Callback().Create().Before("gorm:create").Register("generate_uuid_order_item", func(db *gorm.DB) {
|
|
if item, ok := db.Statement.Dest.(*marketplace.OrderItem); ok && item.ID == uuid.Nil {
|
|
item.ID = uuid.New()
|
|
}
|
|
if items, ok := db.Statement.Dest.(*[]marketplace.OrderItem); ok {
|
|
for i := range *items {
|
|
if (*items)[i].ID == uuid.Nil {
|
|
(*items)[i].ID = uuid.New()
|
|
}
|
|
}
|
|
}
|
|
})
|
|
db.Callback().Create().Before("gorm:create").Register("generate_uuid_license", func(db *gorm.DB) {
|
|
if license, ok := db.Statement.Dest.(*marketplace.License); ok && license.ID == uuid.Nil {
|
|
license.ID = uuid.New()
|
|
}
|
|
if licenses, ok := db.Statement.Dest.(*[]marketplace.License); ok {
|
|
for i := range *licenses {
|
|
if (*licenses)[i].ID == uuid.Nil {
|
|
(*licenses)[i].ID = uuid.New()
|
|
}
|
|
}
|
|
}
|
|
})
|
|
|
|
// Get underlying sql.DB from GORM for raw SQL queries
|
|
sqlDB, err := db.DB()
|
|
require.NoError(t, err)
|
|
|
|
dbWrapper := &database.Database{
|
|
DB: sqlDB,
|
|
GormDB: db,
|
|
Logger: logger,
|
|
}
|
|
|
|
// Setup storage service
|
|
storageService := &MockStorageService{
|
|
GetDownloadURLFunc: func(filePath string) (string, error) {
|
|
return fmt.Sprintf("https://storage.example.com/download/%s", filePath), nil
|
|
},
|
|
}
|
|
|
|
// Setup marketplace service
|
|
marketService := marketplace.NewService(db, logger, storageService)
|
|
|
|
// Setup handlers
|
|
marketHandler := handlers.NewMarketplaceHandler(marketService, logger, "uploads")
|
|
|
|
// Create router
|
|
router := gin.New()
|
|
|
|
// Mock auth middleware - set user_id from header if present
|
|
router.Use(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()
|
|
})
|
|
|
|
// product_reviews table for review tests (v0.602 E2E)
|
|
err = db.Exec(`
|
|
CREATE TABLE IF NOT EXISTS product_reviews (
|
|
id TEXT PRIMARY KEY,
|
|
product_id TEXT NOT NULL,
|
|
buyer_id TEXT NOT NULL,
|
|
order_id TEXT NOT NULL,
|
|
rating INTEGER NOT NULL CHECK (rating >= 1 AND rating <= 5),
|
|
comment TEXT,
|
|
created_at DATETIME,
|
|
UNIQUE(product_id, buyer_id),
|
|
FOREIGN KEY(product_id) REFERENCES products(id),
|
|
FOREIGN KEY(buyer_id) REFERENCES users(id),
|
|
FOREIGN KEY(order_id) REFERENCES orders(id)
|
|
)
|
|
`).Error
|
|
require.NoError(t, err)
|
|
|
|
// Setup marketplace routes
|
|
marketplaceGroup := router.Group("/api/v1/marketplace")
|
|
{
|
|
marketplaceGroup.GET("/products", marketHandler.ListProducts)
|
|
marketplaceGroup.POST("/products", marketHandler.CreateProduct)
|
|
marketplaceGroup.PUT("/products/:id", marketHandler.UpdateProduct)
|
|
marketplaceGroup.GET("/orders", marketHandler.ListOrders)
|
|
marketplaceGroup.GET("/orders/:id", marketHandler.GetOrder)
|
|
marketplaceGroup.POST("/orders", marketHandler.CreateOrder)
|
|
marketplaceGroup.GET("/download/:product_id", marketHandler.GetDownloadURL)
|
|
marketplaceGroup.POST("/products/:id/reviews", marketHandler.CreateReview)
|
|
marketplaceGroup.GET("/products/:id/reviews", marketHandler.ListReviews)
|
|
marketplaceGroup.GET("/orders/:id/invoice", marketHandler.GetOrderInvoice)
|
|
}
|
|
|
|
cleanup := func() {
|
|
// Database will be closed automatically
|
|
}
|
|
|
|
return router, db, dbWrapper, marketService, cleanup
|
|
}
|
|
|
|
// createTestUser crée un utilisateur de test
|
|
func createTestUser(t *testing.T, db *gorm.DB, email, username string) *models.User {
|
|
user := &models.User{
|
|
ID: uuid.New(),
|
|
Username: username,
|
|
Email: email,
|
|
PasswordHash: "hashed_password",
|
|
Slug: username,
|
|
IsActive: true,
|
|
IsVerified: true,
|
|
}
|
|
err := db.Create(user).Error
|
|
require.NoError(t, err)
|
|
return user
|
|
}
|
|
|
|
// createTestTrack crée un track de test
|
|
func createTestTrack(t *testing.T, db *gorm.DB, userID uuid.UUID, title string) *models.Track {
|
|
track := &models.Track{
|
|
ID: uuid.New(),
|
|
UserID: userID,
|
|
Title: title,
|
|
Artist: "Test Artist",
|
|
FilePath: fmt.Sprintf("/tracks/%s.mp3", uuid.New().String()),
|
|
Format: "mp3",
|
|
FileSize: 5 * 1024 * 1024,
|
|
Duration: 180,
|
|
IsPublic: true,
|
|
Status: models.TrackStatusCompleted,
|
|
CreatedAt: time.Now(),
|
|
UpdatedAt: time.Now(),
|
|
}
|
|
err := db.Create(track).Error
|
|
require.NoError(t, err)
|
|
return track
|
|
}
|
|
|
|
// TestMarketplaceFlow_CompleteFlow teste le flux complet : création produit → commande → téléchargement
|
|
func TestMarketplaceFlow_CompleteFlow(t *testing.T) {
|
|
router, db, _, _, cleanup := setupMarketplaceFlowTestRouter(t)
|
|
defer cleanup()
|
|
|
|
// Create seller and buyer
|
|
seller := createTestUser(t, db, "seller@example.com", "seller")
|
|
buyer := createTestUser(t, db, "buyer@example.com", "buyer")
|
|
|
|
// Create track owned by seller
|
|
track := createTestTrack(t, db, seller.ID, "My Awesome Track")
|
|
|
|
// Step 1: Seller creates a product
|
|
createProductReq := handlers.CreateProductRequest{
|
|
Title: "My Product",
|
|
Description: "A great track for sale",
|
|
Price: 9.99,
|
|
ProductType: "track",
|
|
TrackID: track.ID.String(),
|
|
LicenseType: "standard",
|
|
}
|
|
body, _ := json.Marshal(createProductReq)
|
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/marketplace/products", bytes.NewBuffer(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("X-User-ID", seller.ID.String())
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusCreated, w.Code)
|
|
|
|
var productResponse map[string]interface{}
|
|
err := json.Unmarshal(w.Body.Bytes(), &productResponse)
|
|
require.NoError(t, err)
|
|
assert.True(t, productResponse["success"].(bool))
|
|
|
|
// Extract product ID from response
|
|
productData, ok := productResponse["data"].(map[string]interface{})
|
|
require.True(t, ok)
|
|
productIDStr, ok := productData["id"].(string)
|
|
require.True(t, ok)
|
|
productID, err := uuid.Parse(productIDStr)
|
|
require.NoError(t, err)
|
|
|
|
// Step 2: Buyer creates an order for the product
|
|
createOrderReq := handlers.CreateOrderRequest{
|
|
Items: []struct {
|
|
ProductID string `json:"product_id" binding:"required" validate:"required,uuid"`
|
|
}{
|
|
{ProductID: productID.String()},
|
|
},
|
|
}
|
|
body, _ = json.Marshal(createOrderReq)
|
|
req = httptest.NewRequest(http.MethodPost, "/api/v1/marketplace/orders", bytes.NewBuffer(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("X-User-ID", buyer.ID.String())
|
|
w = httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusCreated, w.Code)
|
|
|
|
var orderResponse map[string]interface{}
|
|
err = json.Unmarshal(w.Body.Bytes(), &orderResponse)
|
|
require.NoError(t, err)
|
|
assert.True(t, orderResponse["success"].(bool))
|
|
|
|
// Verify order was created with correct status (data is CreateOrderResponse with nested order)
|
|
orderData, ok := orderResponse["data"].(map[string]interface{})
|
|
require.True(t, ok)
|
|
order, ok := orderData["order"].(map[string]interface{})
|
|
require.True(t, ok)
|
|
assert.Equal(t, "completed", order["status"])
|
|
|
|
// Step 3: Buyer gets download URL for the purchased product
|
|
req = httptest.NewRequest(http.MethodGet, fmt.Sprintf("/api/v1/marketplace/download/%s", productID.String()), nil)
|
|
req.Header.Set("X-User-ID", buyer.ID.String())
|
|
w = httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
|
|
var downloadResponse map[string]interface{}
|
|
err = json.Unmarshal(w.Body.Bytes(), &downloadResponse)
|
|
require.NoError(t, err)
|
|
assert.True(t, downloadResponse["success"].(bool))
|
|
|
|
// Verify download URL is present
|
|
downloadData, ok := downloadResponse["data"].(map[string]interface{})
|
|
require.True(t, ok)
|
|
assert.Contains(t, downloadData, "url")
|
|
assert.NotEmpty(t, downloadData["url"])
|
|
}
|
|
|
|
// TestMarketplaceFlow_CompleteFlowWithReviewAndInvoice tests product -> order -> review -> invoice (v0.602 E2E)
|
|
func TestMarketplaceFlow_CompleteFlowWithReviewAndInvoice(t *testing.T) {
|
|
router, db, _, _, cleanup := setupMarketplaceFlowTestRouter(t)
|
|
defer cleanup()
|
|
|
|
seller := createTestUser(t, db, "seller@example.com", "seller")
|
|
buyer := createTestUser(t, db, "buyer@example.com", "buyer")
|
|
track := createTestTrack(t, db, seller.ID, "Track For Review")
|
|
|
|
// 1. Create product
|
|
createProductReq := handlers.CreateProductRequest{
|
|
Title: "Product For Review",
|
|
Description: "Great track",
|
|
Price: 12.99,
|
|
ProductType: "track",
|
|
TrackID: track.ID.String(),
|
|
LicenseType: "standard",
|
|
}
|
|
body, _ := json.Marshal(createProductReq)
|
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/marketplace/products", bytes.NewBuffer(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("X-User-ID", seller.ID.String())
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
require.Equal(t, http.StatusCreated, w.Code)
|
|
|
|
var productResp map[string]interface{}
|
|
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &productResp))
|
|
productData := productResp["data"].(map[string]interface{})
|
|
productID := productData["id"].(string)
|
|
|
|
// 2. Create order (simulated payment completes immediately)
|
|
orderBody, _ := json.Marshal(handlers.CreateOrderRequest{
|
|
Items: []struct {
|
|
ProductID string `json:"product_id" binding:"required" validate:"required,uuid"`
|
|
}{{ProductID: productID}},
|
|
})
|
|
req = httptest.NewRequest(http.MethodPost, "/api/v1/marketplace/orders", bytes.NewBuffer(orderBody))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("X-User-ID", buyer.ID.String())
|
|
w = httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
require.Equal(t, http.StatusCreated, w.Code)
|
|
|
|
var orderResp map[string]interface{}
|
|
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &orderResp))
|
|
orderData := orderResp["data"].(map[string]interface{})
|
|
order := orderData["order"].(map[string]interface{})
|
|
orderID := order["id"].(string)
|
|
assert.Equal(t, "completed", order["status"])
|
|
|
|
// 3. Create review
|
|
reviewBody, _ := json.Marshal(map[string]interface{}{"rating": 5, "comment": "Excellent!"})
|
|
req = httptest.NewRequest(http.MethodPost, fmt.Sprintf("/api/v1/marketplace/products/%s/reviews", productID), bytes.NewBuffer(reviewBody))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("X-User-ID", buyer.ID.String())
|
|
w = httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
assert.Equal(t, http.StatusCreated, w.Code)
|
|
|
|
// 4. Download invoice (PDF)
|
|
req = httptest.NewRequest(http.MethodGet, fmt.Sprintf("/api/v1/marketplace/orders/%s/invoice", orderID), nil)
|
|
req.Header.Set("X-User-ID", buyer.ID.String())
|
|
w = httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
assert.Equal(t, "application/pdf", w.Header().Get("Content-Type"))
|
|
assert.NotEmpty(t, w.Body.Bytes())
|
|
}
|
|
|
|
// TestMarketplaceFlow_CreateProduct_Validation teste la validation lors de la création de produit
|
|
func TestMarketplaceFlow_CreateProduct_Validation(t *testing.T) {
|
|
router, db, _, _, cleanup := setupMarketplaceFlowTestRouter(t)
|
|
defer cleanup()
|
|
|
|
seller := createTestUser(t, db, "seller@example.com", "seller")
|
|
|
|
t.Run("Missing required fields", func(t *testing.T) {
|
|
createProductReq := handlers.CreateProductRequest{
|
|
Description: "A great track",
|
|
Price: 9.99,
|
|
}
|
|
body, _ := json.Marshal(createProductReq)
|
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/marketplace/products", bytes.NewBuffer(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("X-User-ID", seller.ID.String())
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusBadRequest, w.Code)
|
|
})
|
|
|
|
t.Run("Invalid price", func(t *testing.T) {
|
|
createProductReq := handlers.CreateProductRequest{
|
|
Title: "My Product",
|
|
Description: "A great track",
|
|
Price: -5.0, // Invalid negative price
|
|
ProductType: "track",
|
|
}
|
|
body, _ := json.Marshal(createProductReq)
|
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/marketplace/products", bytes.NewBuffer(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("X-User-ID", seller.ID.String())
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusBadRequest, w.Code)
|
|
})
|
|
|
|
t.Run("Invalid product type", func(t *testing.T) {
|
|
createProductReq := handlers.CreateProductRequest{
|
|
Title: "My Product",
|
|
Description: "A great track",
|
|
Price: 9.99,
|
|
ProductType: "invalid_type",
|
|
}
|
|
body, _ := json.Marshal(createProductReq)
|
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/marketplace/products", bytes.NewBuffer(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("X-User-ID", seller.ID.String())
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusBadRequest, w.Code)
|
|
})
|
|
}
|
|
|
|
// TestMarketplaceFlow_CreateOrder_Validation teste la validation lors de la création de commande
|
|
func TestMarketplaceFlow_CreateOrder_Validation(t *testing.T) {
|
|
router, db, _, _, cleanup := setupMarketplaceFlowTestRouter(t)
|
|
defer cleanup()
|
|
|
|
buyer := createTestUser(t, db, "buyer@example.com", "buyer")
|
|
|
|
t.Run("Empty items", func(t *testing.T) {
|
|
createOrderReq := handlers.CreateOrderRequest{
|
|
Items: []struct {
|
|
ProductID string `json:"product_id" binding:"required" validate:"required,uuid"`
|
|
}{},
|
|
}
|
|
body, _ := json.Marshal(createOrderReq)
|
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/marketplace/orders", bytes.NewBuffer(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("X-User-ID", buyer.ID.String())
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusBadRequest, w.Code)
|
|
})
|
|
|
|
t.Run("Non-existent product", func(t *testing.T) {
|
|
createOrderReq := handlers.CreateOrderRequest{
|
|
Items: []struct {
|
|
ProductID string `json:"product_id" binding:"required" validate:"required,uuid"`
|
|
}{
|
|
{ProductID: uuid.New().String()},
|
|
},
|
|
}
|
|
body, _ := json.Marshal(createOrderReq)
|
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/marketplace/orders", bytes.NewBuffer(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("X-User-ID", buyer.ID.String())
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusBadRequest, w.Code)
|
|
})
|
|
}
|
|
|
|
// TestMarketplaceFlow_GetDownloadURL_NoLicense teste le téléchargement sans licence
|
|
func TestMarketplaceFlow_GetDownloadURL_NoLicense(t *testing.T) {
|
|
router, db, _, _, cleanup := setupMarketplaceFlowTestRouter(t)
|
|
defer cleanup()
|
|
|
|
seller := createTestUser(t, db, "seller@example.com", "seller")
|
|
buyer := createTestUser(t, db, "buyer@example.com", "buyer")
|
|
|
|
// Create track and product
|
|
track := createTestTrack(t, db, seller.ID, "My Track")
|
|
product := &marketplace.Product{
|
|
ID: uuid.New(),
|
|
SellerID: seller.ID,
|
|
Title: "My Product",
|
|
Description: "A great track",
|
|
Price: 9.99,
|
|
ProductType: "track",
|
|
TrackID: &track.ID,
|
|
Status: marketplace.ProductStatusActive,
|
|
CreatedAt: time.Now(),
|
|
UpdatedAt: time.Now(),
|
|
}
|
|
err := db.Create(product).Error
|
|
require.NoError(t, err)
|
|
|
|
// Try to download without purchasing
|
|
req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/api/v1/marketplace/download/%s", product.ID.String()), nil)
|
|
req.Header.Set("X-User-ID", buyer.ID.String())
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusForbidden, w.Code)
|
|
}
|
|
|
|
// TestMarketplaceFlow_ListOrders teste la liste des commandes
|
|
func TestMarketplaceFlow_ListOrders(t *testing.T) {
|
|
router, db, _, _, cleanup := setupMarketplaceFlowTestRouter(t)
|
|
defer cleanup()
|
|
|
|
seller := createTestUser(t, db, "seller@example.com", "seller")
|
|
buyer := createTestUser(t, db, "buyer@example.com", "buyer")
|
|
|
|
// Create track and product
|
|
track := createTestTrack(t, db, seller.ID, "My Track")
|
|
product := &marketplace.Product{
|
|
ID: uuid.New(),
|
|
SellerID: seller.ID,
|
|
Title: "My Product",
|
|
Description: "A great track",
|
|
Price: 9.99,
|
|
ProductType: "track",
|
|
TrackID: &track.ID,
|
|
Status: marketplace.ProductStatusActive,
|
|
CreatedAt: time.Now(),
|
|
UpdatedAt: time.Now(),
|
|
}
|
|
err := db.Create(product).Error
|
|
require.NoError(t, err)
|
|
|
|
// Create order
|
|
createOrderReq := handlers.CreateOrderRequest{
|
|
Items: []struct {
|
|
ProductID string `json:"product_id" binding:"required" validate:"required,uuid"`
|
|
}{
|
|
{ProductID: product.ID.String()},
|
|
},
|
|
}
|
|
body, _ := json.Marshal(createOrderReq)
|
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/marketplace/orders", bytes.NewBuffer(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("X-User-ID", buyer.ID.String())
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusCreated, w.Code)
|
|
|
|
// List orders
|
|
req = httptest.NewRequest(http.MethodGet, "/api/v1/marketplace/orders", nil)
|
|
req.Header.Set("X-User-ID", buyer.ID.String())
|
|
w = httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
|
|
var response map[string]interface{}
|
|
err = json.Unmarshal(w.Body.Bytes(), &response)
|
|
require.NoError(t, err)
|
|
assert.True(t, response["success"].(bool))
|
|
|
|
// Verify orders list
|
|
data, ok := response["data"].([]interface{})
|
|
require.True(t, ok)
|
|
assert.GreaterOrEqual(t, len(data), 1)
|
|
}
|
|
|
|
// TestMarketplaceFlow_GetOrder teste la récupération d'une commande spécifique
|
|
func TestMarketplaceFlow_GetOrder(t *testing.T) {
|
|
router, db, _, _, cleanup := setupMarketplaceFlowTestRouter(t)
|
|
defer cleanup()
|
|
|
|
seller := createTestUser(t, db, "seller@example.com", "seller")
|
|
buyer := createTestUser(t, db, "buyer@example.com", "buyer")
|
|
|
|
// Create track and product
|
|
track := createTestTrack(t, db, seller.ID, "My Track")
|
|
product := &marketplace.Product{
|
|
ID: uuid.New(),
|
|
SellerID: seller.ID,
|
|
Title: "My Product",
|
|
Description: "A great track",
|
|
Price: 9.99,
|
|
ProductType: "track",
|
|
TrackID: &track.ID,
|
|
Status: marketplace.ProductStatusActive,
|
|
CreatedAt: time.Now(),
|
|
UpdatedAt: time.Now(),
|
|
}
|
|
err := db.Create(product).Error
|
|
require.NoError(t, err)
|
|
|
|
// Create order
|
|
createOrderReq := handlers.CreateOrderRequest{
|
|
Items: []struct {
|
|
ProductID string `json:"product_id" binding:"required" validate:"required,uuid"`
|
|
}{
|
|
{ProductID: product.ID.String()},
|
|
},
|
|
}
|
|
body, _ := json.Marshal(createOrderReq)
|
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/marketplace/orders", bytes.NewBuffer(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("X-User-ID", buyer.ID.String())
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusCreated, w.Code)
|
|
|
|
// Extract order ID (data is CreateOrderResponse with nested order)
|
|
var orderResponse map[string]interface{}
|
|
err = json.Unmarshal(w.Body.Bytes(), &orderResponse)
|
|
require.NoError(t, err)
|
|
orderData, ok := orderResponse["data"].(map[string]interface{})
|
|
require.True(t, ok)
|
|
orderObj, ok := orderData["order"].(map[string]interface{})
|
|
require.True(t, ok)
|
|
orderIDStr, ok := orderObj["id"].(string)
|
|
require.True(t, ok)
|
|
|
|
// Get order details
|
|
req = httptest.NewRequest(http.MethodGet, fmt.Sprintf("/api/v1/marketplace/orders/%s", orderIDStr), nil)
|
|
req.Header.Set("X-User-ID", buyer.ID.String())
|
|
w = httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
|
|
var response map[string]interface{}
|
|
err = json.Unmarshal(w.Body.Bytes(), &response)
|
|
require.NoError(t, err)
|
|
assert.True(t, response["success"].(bool))
|
|
|
|
// Verify order details
|
|
orderDetails, ok := response["data"].(map[string]interface{})
|
|
require.True(t, ok)
|
|
assert.Equal(t, orderIDStr, orderDetails["id"].(string))
|
|
assert.Equal(t, "completed", orderDetails["status"])
|
|
}
|
|
|
|
// TestMarketplaceFlow_CreateOrder_InactiveProduct teste la commande d'un produit inactif
|
|
func TestMarketplaceFlow_CreateOrder_InactiveProduct(t *testing.T) {
|
|
router, db, _, _, cleanup := setupMarketplaceFlowTestRouter(t)
|
|
defer cleanup()
|
|
|
|
seller := createTestUser(t, db, "seller@example.com", "seller")
|
|
buyer := createTestUser(t, db, "buyer@example.com", "buyer")
|
|
|
|
// Create track and inactive product
|
|
track := createTestTrack(t, db, seller.ID, "My Track")
|
|
product := &marketplace.Product{
|
|
ID: uuid.New(),
|
|
SellerID: seller.ID,
|
|
Title: "My Product",
|
|
Description: "A great track",
|
|
Price: 9.99,
|
|
ProductType: "track",
|
|
TrackID: &track.ID,
|
|
Status: marketplace.ProductStatusDraft, // Inactive
|
|
CreatedAt: time.Now(),
|
|
UpdatedAt: time.Now(),
|
|
}
|
|
err := db.Create(product).Error
|
|
require.NoError(t, err)
|
|
|
|
// Try to create order for inactive product
|
|
createOrderReq := handlers.CreateOrderRequest{
|
|
Items: []struct {
|
|
ProductID string `json:"product_id" binding:"required" validate:"required,uuid"`
|
|
}{
|
|
{ProductID: product.ID.String()},
|
|
},
|
|
}
|
|
body, _ := json.Marshal(createOrderReq)
|
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/marketplace/orders", bytes.NewBuffer(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("X-User-ID", buyer.ID.String())
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusBadRequest, w.Code)
|
|
}
|
|
|