//go:build performance // +build performance package performance import ( "bytes" "encoding/json" "fmt" "io" "mime/multipart" "net/http" "net/http/httptest" "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" "go.uber.org/zap/zaptest" "gorm.io/driver/sqlite" "gorm.io/gorm" "veza-backend-api/internal/core/track" "veza-backend-api/internal/database" "veza-backend-api/internal/handlers" "veza-backend-api/internal/models" "veza-backend-api/internal/services" ) // UploadPerformanceThresholds définit les seuils de performance pour les uploads var UploadPerformanceThresholds = struct { SimpleUploadInitiate time.Duration // Initiation d'upload simple SimpleUploadComplete time.Duration // Upload simple complet ChunkedUploadInitiate time.Duration // Initiation upload chunked ChunkedUploadChunk time.Duration // Upload d'un chunk ChunkedUploadComplete time.Duration // Complétion upload chunked BatchUpload time.Duration // Upload batch }{ SimpleUploadInitiate: 100 * time.Millisecond, SimpleUploadComplete: 2000 * time.Millisecond, // 2s pour upload complet ChunkedUploadInitiate: 100 * time.Millisecond, ChunkedUploadChunk: 200 * time.Millisecond, // 200ms par chunk ChunkedUploadComplete: 3000 * time.Millisecond, // 3s pour assembler BatchUpload: 1500 * time.Millisecond, } // setupUploadTestRouter crée un router de test pour les tests d'upload func setupUploadTestRouter(t *testing.T) (*gin.Engine, *gorm.DB, uuid.UUID, 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 err = db.AutoMigrate( &models.User{}, &models.Track{}, &models.Playlist{}, &models.PlaylistTrack{}, &models.RefreshToken{}, &models.Session{}, &models.Role{}, &models.UserRole{}, &models.Permission{}, ) require.NoError(t, err) dbWrapper := &database.Database{GormDB: db} // Create test user userID := uuid.New() user := &models.User{ ID: userID, Email: "test@example.com", Username: "testuser", PasswordHash: "$2a$10$abcdefghijklmnopqrstuvwxyz1234567890", IsVerified: true, } err = db.Create(user).Error require.NoError(t, err) // Setup services uploadDir := t.TempDir() chunksDir := t.TempDir() trackService := track.NewTrackService(db, logger, uploadDir) trackUploadService := services.NewTrackUploadService(db, logger) // NewTrackChunkService takes (chunksDir string, redisClient *redis.Client, logger *zap.Logger) chunkService := services.NewTrackChunkService(chunksDir, nil, logger) // nil Redis for performance tests likeService := services.NewTrackLikeService(db, logger) streamService := services.NewStreamService("http://localhost:8082", logger) trackHandler := track.NewTrackHandler(trackService, trackUploadService, chunkService, likeService, streamService) // Setup upload handler uploadConfig := &services.UploadConfig{ MaxAudioSize: 100 * 1024 * 1024, // 100MB AllowedAudioTypes: []string{"audio/mpeg", "audio/mp3"}, ClamAVEnabled: false, // Disable for performance tests ClamAVRequired: false, ClamAVAddress: "", QuarantineDir: t.TempDir(), } uploadValidator, err := services.NewUploadValidator(uploadConfig, logger) require.NoError(t, err) auditService := services.NewAuditService(dbWrapper, logger) uploadHandler := handlers.NewUploadHandler(uploadValidator, auditService, trackUploadService, logger, 10) // Create router router := gin.New() // Mock auth middleware - set user_id in context router.Use(func(c *gin.Context) { c.Set("user_id", userID) c.Next() }) // Track routes tracksGroup := router.Group("/api/v1/tracks") { tracksGroup.POST("", trackHandler.UploadTrack) tracksGroup.POST("/initiate", trackHandler.InitiateChunkedUpload) tracksGroup.POST("/chunk", trackHandler.UploadChunk) tracksGroup.POST("/complete", trackHandler.CompleteChunkedUpload) } // Upload routes uploadsGroup := router.Group("/api/v1/uploads") { uploadsGroup.POST("", uploadHandler.UploadFile()) uploadsGroup.POST("/batch", uploadHandler.BatchUpload()) } cleanup := func() { // Cleanup handled by t.TempDir() } return router, db, userID, cleanup } // createTestFile crée un fichier de test en mémoire func createTestFile(size int) *bytes.Buffer { data := make([]byte, size) for i := range data { data[i] = byte(i % 256) } return bytes.NewBuffer(data) } // createMultipartForm crée un multipart form avec un fichier func createMultipartForm(filename string, fileData *bytes.Buffer, fields map[string]string) (string, *bytes.Buffer) { body := &bytes.Buffer{} writer := multipart.NewWriter(body) // Add file fileWriter, err := writer.CreateFormFile("file", filename) if err != nil { panic(err) } io.Copy(fileWriter, fileData) // Add fields for key, value := range fields { writer.WriteField(key, value) } writer.Close() return writer.FormDataContentType(), body } // TestPerformance_SimpleUpload teste les performances d'upload simple func TestPerformance_SimpleUpload(t *testing.T) { if testing.Short() { t.Skip("Skipping performance test in short mode") } router, _, _, cleanup := setupUploadTestRouter(t) defer cleanup() fileSize := 1024 * 1024 // 1MB fileData := createTestFile(fileSize) fields := map[string]string{ "title": "Test Track", "artist": "Test Artist", "file_type": "audio", } contentType, body := createMultipartForm("test.mp3", fileData, fields) var totalDuration time.Duration iterations := 20 for i := 0; i < iterations; i++ { req := httptest.NewRequest(http.MethodPost, "/api/v1/tracks", body) req.Header.Set("Content-Type", contentType) w := httptest.NewRecorder() start := time.Now() router.ServeHTTP(w, req) duration := time.Since(start) totalDuration += duration // Reset body for next iteration fileData = createTestFile(fileSize) _, body = createMultipartForm("test.mp3", fileData, fields) } avgDuration := totalDuration / time.Duration(iterations) t.Logf("Simple upload average response time: %v (threshold: %v)", avgDuration, UploadPerformanceThresholds.SimpleUploadComplete) assert.Less(t, avgDuration, UploadPerformanceThresholds.SimpleUploadComplete, "Simple upload should complete within threshold") } // TestPerformance_ChunkedUploadInitiate teste les performances d'initiation d'upload chunked func TestPerformance_ChunkedUploadInitiate(t *testing.T) { if testing.Short() { t.Skip("Skipping performance test in short mode") } router, _, _, cleanup := setupUploadTestRouter(t) defer cleanup() payload := map[string]interface{}{ "total_chunks": 5, "total_size": 5 * 1024 * 1024, // 5MB "filename": "test.mp3", } payloadBody, _ := json.Marshal(payload) var totalDuration time.Duration iterations := 100 for i := 0; i < iterations; i++ { req := httptest.NewRequest(http.MethodPost, "/api/v1/tracks/initiate", bytes.NewBuffer(payloadBody)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() start := time.Now() router.ServeHTTP(w, req) duration := time.Since(start) totalDuration += duration } avgDuration := totalDuration / time.Duration(iterations) t.Logf("Chunked upload initiate average response time: %v (threshold: %v)", avgDuration, UploadPerformanceThresholds.ChunkedUploadInitiate) assert.Less(t, avgDuration, UploadPerformanceThresholds.ChunkedUploadInitiate, "Chunked upload initiate should respond within threshold") } // TestPerformance_ChunkedUploadChunk teste les performances d'upload d'un chunk func TestPerformance_ChunkedUploadChunk(t *testing.T) { if testing.Short() { t.Skip("Skipping performance test in short mode") } router, _, _, cleanup := setupUploadTestRouter(t) defer cleanup() // First, initiate an upload initiatePayload := map[string]interface{}{ "total_chunks": 5, "total_size": 5 * 1024 * 1024, "filename": "test.mp3", } initiateBody, _ := json.Marshal(initiatePayload) initiateReq := httptest.NewRequest(http.MethodPost, "/api/v1/tracks/initiate", bytes.NewBuffer(initiateBody)) initiateReq.Header.Set("Content-Type", "application/json") initiateW := httptest.NewRecorder() router.ServeHTTP(initiateW, initiateReq) var initiateResp map[string]interface{} json.Unmarshal(initiateW.Body.Bytes(), &initiateResp) uploadID := initiateResp["data"].(map[string]interface{})["upload_id"].(string) chunkSize := 1024 * 1024 // 1MB per chunk chunkData := createTestFile(chunkSize) var totalDuration time.Duration iterations := 50 for i := 0; i < iterations; i++ { // Create multipart form for chunk body := &bytes.Buffer{} writer := multipart.NewWriter(body) fileWriter, _ := writer.CreateFormFile("chunk", "chunk.bin") io.Copy(fileWriter, chunkData) writer.WriteField("upload_id", uploadID) writer.WriteField("chunk_number", "1") writer.WriteField("total_chunks", "5") writer.WriteField("total_size", fmt.Sprintf("%d", 5*1024*1024)) writer.WriteField("filename", "test.mp3") writer.Close() req := httptest.NewRequest(http.MethodPost, "/api/v1/tracks/chunk", body) req.Header.Set("Content-Type", writer.FormDataContentType()) w := httptest.NewRecorder() start := time.Now() router.ServeHTTP(w, req) duration := time.Since(start) totalDuration += duration // Reset chunk data chunkData = createTestFile(chunkSize) } avgDuration := totalDuration / time.Duration(iterations) t.Logf("Chunk upload average response time: %v (threshold: %v)", avgDuration, UploadPerformanceThresholds.ChunkedUploadChunk) assert.Less(t, avgDuration, UploadPerformanceThresholds.ChunkedUploadChunk, "Chunk upload should respond within threshold") } // TestPerformance_ConcurrentUploads teste les performances avec uploads concurrents func TestPerformance_ConcurrentUploads(t *testing.T) { if testing.Short() { t.Skip("Skipping performance test in short mode") } router, _, _, cleanup := setupUploadTestRouter(t) defer cleanup() fileSize := 512 * 1024 // 512KB concurrentUploads := 10 iterations := 5 var totalDuration time.Duration for iter := 0; iter < iterations; iter++ { start := time.Now() // Simulate concurrent uploads using channels done := make(chan bool, concurrentUploads) for i := 0; i < concurrentUploads; i++ { go func(id int) { fileData := createTestFile(fileSize) fields := map[string]string{ "title": fmt.Sprintf("Test Track %d", id), "artist": "Test Artist", "file_type": "audio", } contentType, body := createMultipartForm(fmt.Sprintf("test%d.mp3", id), fileData, fields) req := httptest.NewRequest(http.MethodPost, "/api/v1/tracks", body) req.Header.Set("Content-Type", contentType) w := httptest.NewRecorder() router.ServeHTTP(w, req) done <- true }(i) } // Wait for all uploads to complete for i := 0; i < concurrentUploads; i++ { <-done } duration := time.Since(start) totalDuration += duration } avgDuration := totalDuration / time.Duration(iterations) t.Logf("Concurrent uploads (%d) average time: %v", concurrentUploads, avgDuration) // Threshold: should handle 10 concurrent uploads in reasonable time threshold := UploadPerformanceThresholds.SimpleUploadComplete * time.Duration(concurrentUploads) / 2 assert.Less(t, avgDuration, threshold, "Concurrent uploads should complete within reasonable time") } // BenchmarkSimpleUpload benchmark pour upload simple func BenchmarkSimpleUpload(b *testing.B) { gin.SetMode(gin.TestMode) logger := zap.NewNop() db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) if err != nil { b.Fatalf("Failed to open database: %v", err) } db.AutoMigrate(&models.User{}, &models.Track{}) userID := uuid.New() user := &models.User{ ID: userID, Email: "test@example.com", Username: "testuser", IsVerified: true, } db.Create(user) uploadDir := b.TempDir() chunksDir := b.TempDir() trackService := track.NewTrackService(db, logger, uploadDir) trackUploadService := services.NewTrackUploadService(db, logger) chunkService := services.NewTrackChunkService(chunksDir, nil, logger) // nil Redis for benchmarks likeService := services.NewTrackLikeService(db, logger) streamService := services.NewStreamService("http://localhost:8082", logger) trackHandler := track.NewTrackHandler(trackService, trackUploadService, chunkService, likeService, streamService) router := gin.New() router.Use(func(c *gin.Context) { c.Set("user_id", userID) c.Next() }) router.POST("/api/v1/tracks", trackHandler.UploadTrack) fileSize := 1024 * 1024 // 1MB fileData := createTestFile(fileSize) fields := map[string]string{ "title": "Test Track", "artist": "Test Artist", "file_type": "audio", } contentType, body := createMultipartForm("test.mp3", fileData, fields) b.ResetTimer() for i := 0; i < b.N; i++ { req := httptest.NewRequest(http.MethodPost, "/api/v1/tracks", body) req.Header.Set("Content-Type", contentType) w := httptest.NewRecorder() router.ServeHTTP(w, req) // Reset body for next iteration fileData = createTestFile(fileSize) _, body = createMultipartForm("test.mp3", fileData, fields) } }