veza/veza-backend-api/tests/performance/upload_endpoints_performance_test.go
senke f71d6add4b [BE-TEST-015] be-test: Add load tests for upload endpoints
- Created k6 load test script for concurrent and chunked uploads
- Added Go performance tests for upload endpoints
- Updated README with usage instructions for upload load tests
- Tests cover simple upload, chunked upload (initiate/chunk/complete), and batch upload
- Performance thresholds defined for upload operations

Phase: PHASE-5
Priority: P2
Progress: 136/267 (50.94%)
2025-12-25 01:55:22 +01:00

435 lines
13 KiB
Go

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