- 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%)
435 lines
13 KiB
Go
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)
|
|
}
|
|
}
|
|
|