package handlers import ( "bytes" "context" "encoding/json" "net/http" "net/http/httptest" "testing" "github.com/google/uuid" "github.com/gin-gonic/gin" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "go.uber.org/zap/zaptest" "gorm.io/driver/sqlite" "gorm.io/gorm" "veza-backend-api/internal/models" "veza-backend-api/internal/services" "go.uber.org/zap" ) // MockBitrateAdaptationService est un mock du service d'adaptation de bitrate type MockBitrateAdaptationService struct { mock.Mock } func (m *MockBitrateAdaptationService) AdaptBitrate(ctx context.Context, trackID uuid.UUID, userID uuid.UUID, currentBitrate int, bandwidth int64, bufferLevel float64) (int, error) { args := m.Called(ctx, trackID, userID, currentBitrate, bandwidth, bufferLevel) return args.Int(0), args.Error(1) } func setupTestBitrateHandlerRouter(adaptationService *services.BitrateAdaptationService, logger *zap.Logger) *gin.Engine { gin.SetMode(gin.TestMode) router := gin.New() handler := NewBitrateHandler(adaptationService, logger) // Route protégée (nécessite authentification) protected := router.Group("/api/v1/tracks") protected.Use(func(c *gin.Context) { // Simuler le middleware d'authentification // Use a fixed UUID for testing consistency if needed, or random uid := uuid.New() c.Set("user_id", uid) c.Next() }) { protected.POST("/:id/bitrate/adapt", handler.AdaptBitrate) } return router } func TestNewBitrateHandler(t *testing.T) { db, _ := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) logger := zaptest.NewLogger(t) bandwidthService := services.NewBandwidthDetectionService(logger) adaptationService := services.NewBitrateAdaptationService(db, bandwidthService, logger) handler := NewBitrateHandler(adaptationService, logger) assert.NotNil(t, handler) assert.Equal(t, adaptationService, handler.adaptationService) } func TestBitrateHandler_AdaptBitrate_Success(t *testing.T) { db, _ := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) db.Exec("PRAGMA foreign_keys = ON") db.AutoMigrate(&models.User{}, &models.Track{}, &models.BitrateAdaptationLog{}) userID := uuid.New() trackID := uuid.New() // Create test user and track user := &models.User{ID: userID, Username: "testuser", Email: "test@example.com", IsActive: true} db.Create(user) track := &models.Track{ID: trackID, UserID: userID, Title: "Test Track", FilePath: "/test.mp3", FileSize: 1024, Format: "MP3", Duration: 180, IsPublic: true, Status: models.TrackStatusCompleted} db.Create(track) logger := zaptest.NewLogger(t) bandwidthService := services.NewBandwidthDetectionService(logger) adaptationService := services.NewBitrateAdaptationService(db, bandwidthService, logger) // Custom router setup to inject the specific user ID gin.SetMode(gin.TestMode) router := gin.New() handler := NewBitrateHandler(adaptationService, logger) protected := router.Group("/api/v1/tracks") protected.Use(func(c *gin.Context) { c.Set("user_id", userID) c.Next() }) protected.POST("/:id/bitrate/adapt", handler.AdaptBitrate) // Créer la requête reqBody := AdaptBitrateRequest{ CurrentBitrate: 128, Bandwidth: 10485760, // 10 Mbps BufferLevel: 0.5, } jsonBody, _ := json.Marshal(reqBody) req, _ := http.NewRequest("POST", "/api/v1/tracks/"+trackID.String()+"/bitrate/adapt", bytes.NewBuffer(jsonBody)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() router.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) var response map[string]interface{} json.Unmarshal(w.Body.Bytes(), &response) assert.Contains(t, response, "recommended_bitrate") assert.Equal(t, float64(320), response["recommended_bitrate"]) } func TestBitrateHandler_AdaptBitrate_InvalidTrackID(t *testing.T) { db, _ := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) logger := zaptest.NewLogger(t) bandwidthService := services.NewBandwidthDetectionService(logger) adaptationService := services.NewBitrateAdaptationService(db, bandwidthService, logger) router := setupTestBitrateHandlerRouter(adaptationService, logger) reqBody := AdaptBitrateRequest{ CurrentBitrate: 128, Bandwidth: 10485760, BufferLevel: 0.5, } jsonBody, _ := json.Marshal(reqBody) req, _ := http.NewRequest("POST", "/api/v1/tracks/invalid/bitrate/adapt", bytes.NewBuffer(jsonBody)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() router.ServeHTTP(w, req) assert.Equal(t, http.StatusBadRequest, w.Code) var response map[string]interface{} json.Unmarshal(w.Body.Bytes(), &response) // MOD-P2-003: Format AppError standardisé if errorObj, ok := response["error"].(map[string]interface{}); ok { if message, ok := errorObj["message"].(string); ok { assert.Contains(t, message, "invalid track id") } } else { assert.Contains(t, response["error"], "invalid track id") } } func TestBitrateHandler_AdaptBitrate_Unauthorized(t *testing.T) { db, _ := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) logger := zaptest.NewLogger(t) bandwidthService := services.NewBandwidthDetectionService(logger) adaptationService := services.NewBitrateAdaptationService(db, bandwidthService, logger) gin.SetMode(gin.TestMode) router := gin.New() handler := NewBitrateHandler(adaptationService, logger) // Route sans middleware d'authentification router.POST("/api/v1/tracks/:id/bitrate/adapt", handler.AdaptBitrate) reqBody := AdaptBitrateRequest{ CurrentBitrate: 128, Bandwidth: 10485760, BufferLevel: 0.5, } jsonBody, _ := json.Marshal(reqBody) trackID := uuid.New() req, _ := http.NewRequest("POST", "/api/v1/tracks/"+trackID.String()+"/bitrate/adapt", bytes.NewBuffer(jsonBody)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() router.ServeHTTP(w, req) // MOD-P2-003: AppError peut retourner 401 ou 403 selon le code d'erreur // ErrCodeUnauthorized (1004) mappe vers 401, mais vérifions le status code réel assert.Contains(t, []int{http.StatusUnauthorized, http.StatusForbidden}, w.Code, "Expected 401 or 403 for unauthorized") var response map[string]interface{} json.Unmarshal(w.Body.Bytes(), &response) // MOD-P2-003: Format AppError standardisé if errorObj, ok := response["error"].(map[string]interface{}); ok { if message, ok := errorObj["message"].(string); ok { assert.Contains(t, []string{"unauthorized", "Unauthorized"}, message) } } else { assert.Contains(t, []string{"unauthorized", "Unauthorized"}, response["error"].(string)) } } func TestBitrateHandler_AdaptBitrate_InvalidJSON(t *testing.T) { db, _ := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) logger := zaptest.NewLogger(t) bandwidthService := services.NewBandwidthDetectionService(logger) adaptationService := services.NewBitrateAdaptationService(db, bandwidthService, logger) router := setupTestBitrateHandlerRouter(adaptationService, logger) trackID := uuid.New() // JSON invalide req, _ := http.NewRequest("POST", "/api/v1/tracks/"+trackID.String()+"/bitrate/adapt", bytes.NewBuffer([]byte("invalid json"))) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() router.ServeHTTP(w, req) assert.Equal(t, http.StatusBadRequest, w.Code) } func TestBitrateHandler_AdaptBitrate_MissingFields(t *testing.T) { db, _ := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) logger := zaptest.NewLogger(t) bandwidthService := services.NewBandwidthDetectionService(logger) adaptationService := services.NewBitrateAdaptationService(db, bandwidthService, logger) router := setupTestBitrateHandlerRouter(adaptationService, logger) // Requête avec champs manquants reqBody := map[string]interface{}{ "current_bitrate": 128, // bandwidth manquant "buffer_level": 0.5, } jsonBody, _ := json.Marshal(reqBody) trackID := uuid.New() req, _ := http.NewRequest("POST", "/api/v1/tracks/"+trackID.String()+"/bitrate/adapt", bytes.NewBuffer(jsonBody)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() router.ServeHTTP(w, req) assert.Equal(t, http.StatusBadRequest, w.Code) } func TestBitrateHandler_AdaptBitrate_InvalidBufferLevel(t *testing.T) { db, _ := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) db.Exec("PRAGMA foreign_keys = ON") db.AutoMigrate(&models.User{}, &models.Track{}, &models.BitrateAdaptationLog{}) userID := uuid.New() user := &models.User{ID: userID, Username: "testuser", Email: "test@example.com", IsActive: true} db.Create(user) trackID := uuid.New() track := &models.Track{ID: trackID, UserID: userID, Title: "Test Track", FilePath: "/test.mp3", FileSize: 1024, Format: "MP3", Duration: 180, IsPublic: true, Status: models.TrackStatusCompleted} db.Create(track) logger := zaptest.NewLogger(t) bandwidthService := services.NewBandwidthDetectionService(logger) adaptationService := services.NewBitrateAdaptationService(db, bandwidthService, logger) // Custom router gin.SetMode(gin.TestMode) router := gin.New() handler := NewBitrateHandler(adaptationService, logger) protected := router.Group("/api/v1/tracks") protected.Use(func(c *gin.Context) { c.Set("user_id", userID) c.Next() }) protected.POST("/:id/bitrate/adapt", handler.AdaptBitrate) // Buffer level invalide (> 1.0) reqBody := AdaptBitrateRequest{ CurrentBitrate: 128, Bandwidth: 10485760, BufferLevel: 1.5, // Invalide } jsonBody, _ := json.Marshal(reqBody) req, _ := http.NewRequest("POST", "/api/v1/tracks/"+trackID.String()+"/bitrate/adapt", bytes.NewBuffer(jsonBody)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() router.ServeHTTP(w, req) assert.Equal(t, http.StatusBadRequest, w.Code) var response map[string]interface{} json.Unmarshal(w.Body.Bytes(), &response) // MOD-P2-003: Format AppError standardisé if errorObj, ok := response["error"].(map[string]interface{}); ok { if message, ok := errorObj["message"].(string); ok { assert.Contains(t, message, "invalid buffer level") } } else { assert.Contains(t, response["error"], "invalid buffer level") } } func TestBitrateHandler_AdaptBitrate_DecreaseBitrate(t *testing.T) { db, _ := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) db.Exec("PRAGMA foreign_keys = ON") db.AutoMigrate(&models.User{}, &models.Track{}, &models.BitrateAdaptationLog{}) userID := uuid.New() user := &models.User{ID: userID, Username: "testuser", Email: "test@example.com", IsActive: true} db.Create(user) trackID := uuid.New() track := &models.Track{ID: trackID, UserID: userID, Title: "Test Track", FilePath: "/test.mp3", FileSize: 1024, Format: "MP3", Duration: 180, IsPublic: true, Status: models.TrackStatusCompleted} db.Create(track) logger := zaptest.NewLogger(t) bandwidthService := services.NewBandwidthDetectionService(logger) adaptationService := services.NewBitrateAdaptationService(db, bandwidthService, logger) // Custom router gin.SetMode(gin.TestMode) router := gin.New() handler := NewBitrateHandler(adaptationService, logger) protected := router.Group("/api/v1/tracks") protected.Use(func(c *gin.Context) { c.Set("user_id", userID) c.Next() }) protected.POST("/:id/bitrate/adapt", handler.AdaptBitrate) // Bande passante faible qui devrait réduire le bitrate reqBody := AdaptBitrateRequest{ CurrentBitrate: 320, Bandwidth: 307200, // 300 kbps BufferLevel: 0.5, } jsonBody, _ := json.Marshal(reqBody) req, _ := http.NewRequest("POST", "/api/v1/tracks/"+trackID.String()+"/bitrate/adapt", bytes.NewBuffer(jsonBody)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() router.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) var response map[string]interface{} json.Unmarshal(w.Body.Bytes(), &response) assert.Contains(t, response, "recommended_bitrate") assert.Equal(t, float64(192), response["recommended_bitrate"]) } func TestBitrateHandler_AdaptBitrate_LowBuffer(t *testing.T) { db, _ := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) db.Exec("PRAGMA foreign_keys = ON") db.AutoMigrate(&models.User{}, &models.Track{}, &models.BitrateAdaptationLog{}) userID := uuid.New() user := &models.User{ID: userID, Username: "testuser", Email: "test@example.com", IsActive: true} db.Create(user) trackID := uuid.New() track := &models.Track{ID: trackID, UserID: userID, Title: "Test Track", FilePath: "/test.mp3", FileSize: 1024, Format: "MP3", Duration: 180, IsPublic: true, Status: models.TrackStatusCompleted} db.Create(track) logger := zaptest.NewLogger(t) bandwidthService := services.NewBandwidthDetectionService(logger) adaptationService := services.NewBitrateAdaptationService(db, bandwidthService, logger) // Custom router gin.SetMode(gin.TestMode) router := gin.New() handler := NewBitrateHandler(adaptationService, logger) protected := router.Group("/api/v1/tracks") protected.Use(func(c *gin.Context) { c.Set("user_id", userID) c.Next() }) protected.POST("/:id/bitrate/adapt", handler.AdaptBitrate) // Buffer faible qui devrait empêcher l'augmentation reqBody := AdaptBitrateRequest{ CurrentBitrate: 128, Bandwidth: 10485760, // 10 Mbps (recommandation: 320) BufferLevel: 0.15, // < 20%, devrait empêcher l'augmentation } jsonBody, _ := json.Marshal(reqBody) req, _ := http.NewRequest("POST", "/api/v1/tracks/"+trackID.String()+"/bitrate/adapt", bytes.NewBuffer(jsonBody)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() router.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) var response map[string]interface{} json.Unmarshal(w.Body.Bytes(), &response) assert.Contains(t, response, "recommended_bitrate") // Le bitrate devrait rester à 128 car le buffer est faible assert.Equal(t, float64(128), response["recommended_bitrate"]) } func setupTestBitrateHandlerRouterWithAnalytics(adaptationService *services.BitrateAdaptationService, logger *zap.Logger) *gin.Engine { gin.SetMode(gin.TestMode) router := gin.New() handler := NewBitrateHandler(adaptationService, logger) // Route pour analytics (pas besoin d'authentification pour analytics) router.GET("/api/v1/tracks/:id/bitrate/analytics", handler.GetAnalytics) return router } func TestBitrateHandler_GetAnalytics_Success(t *testing.T) { db, _ := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) db.Exec("PRAGMA foreign_keys = ON") db.AutoMigrate(&models.User{}, &models.Track{}, &models.BitrateAdaptationLog{}) userID := uuid.New() trackID := uuid.New() // Créer test user et track user := &models.User{ID: userID, Username: "testuser", Email: "test@example.com", IsActive: true} db.Create(user) track := &models.Track{ID: trackID, UserID: userID, Title: "Test Track", FilePath: "/test.mp3", FileSize: 1024, Format: "MP3", Duration: 180, IsPublic: true, Status: models.TrackStatusCompleted} db.Create(track) // Créer quelques logs d'adaptation log1 := &models.BitrateAdaptationLog{ TrackID: trackID, UserID: userID, OldBitrate: 128, NewBitrate: 192, Reason: models.BitrateReasonNetworkFast, NetworkBandwidth: intPtr(1048576), } db.Create(log1) log2 := &models.BitrateAdaptationLog{ TrackID: trackID, UserID: userID, OldBitrate: 192, NewBitrate: 128, Reason: models.BitrateReasonNetworkSlow, NetworkBandwidth: intPtr(307200), } db.Create(log2) log3 := &models.BitrateAdaptationLog{ TrackID: trackID, UserID: userID, OldBitrate: 128, NewBitrate: 192, Reason: models.BitrateReasonBufferLow, NetworkBandwidth: nil, } db.Create(log3) logger := zaptest.NewLogger(t) bandwidthService := services.NewBandwidthDetectionService(logger) adaptationService := services.NewBitrateAdaptationService(db, bandwidthService, logger) router := setupTestBitrateHandlerRouterWithAnalytics(adaptationService, logger) req, _ := http.NewRequest("GET", "/api/v1/tracks/"+trackID.String()+"/bitrate/analytics", nil) w := httptest.NewRecorder() router.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) var response map[string]interface{} json.Unmarshal(w.Body.Bytes(), &response) assert.Contains(t, response, "analytics") analytics := response["analytics"].(map[string]interface{}) assert.Equal(t, float64(3), analytics["total_adaptations"]) reasons := analytics["reasons"].(map[string]interface{}) assert.Equal(t, float64(1), reasons[string(models.BitrateReasonNetworkFast)]) assert.Equal(t, float64(1), reasons[string(models.BitrateReasonNetworkSlow)]) assert.Equal(t, float64(1), reasons[string(models.BitrateReasonBufferLow)]) // Vérifier que adaptations_over_time existe assert.Contains(t, analytics, "adaptations_over_time") } func TestBitrateHandler_GetAnalytics_InvalidTrackID(t *testing.T) { db, _ := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) logger := zaptest.NewLogger(t) bandwidthService := services.NewBandwidthDetectionService(logger) adaptationService := services.NewBitrateAdaptationService(db, bandwidthService, logger) router := setupTestBitrateHandlerRouterWithAnalytics(adaptationService, logger) req, _ := http.NewRequest("GET", "/api/v1/tracks/invalid/bitrate/analytics", nil) w := httptest.NewRecorder() router.ServeHTTP(w, req) assert.Equal(t, http.StatusBadRequest, w.Code) var response map[string]interface{} json.Unmarshal(w.Body.Bytes(), &response) // MOD-P2-003: Format AppError standardisé if errorObj, ok := response["error"].(map[string]interface{}); ok { if message, ok := errorObj["message"].(string); ok { assert.Contains(t, message, "invalid track id") } } else { assert.Contains(t, response["error"], "invalid track id") } } func TestBitrateHandler_GetAnalytics_NoAdaptations(t *testing.T) { db, _ := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) db.Exec("PRAGMA foreign_keys = ON") db.AutoMigrate(&models.User{}, &models.Track{}, &models.BitrateAdaptationLog{}) userID := uuid.New() trackID := uuid.New() user := &models.User{ID: userID, Username: "testuser", Email: "test@example.com", IsActive: true} db.Create(user) track := &models.Track{ID: trackID, UserID: userID, Title: "Test Track", FilePath: "/test.mp3", FileSize: 1024, Format: "MP3", Duration: 180, IsPublic: true, Status: models.TrackStatusCompleted} db.Create(track) logger := zaptest.NewLogger(t) bandwidthService := services.NewBandwidthDetectionService(logger) adaptationService := services.NewBitrateAdaptationService(db, bandwidthService, logger) router := setupTestBitrateHandlerRouterWithAnalytics(adaptationService, logger) req, _ := http.NewRequest("GET", "/api/v1/tracks/"+trackID.String()+"/bitrate/analytics", nil) w := httptest.NewRecorder() router.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) var response map[string]interface{} json.Unmarshal(w.Body.Bytes(), &response) analytics := response["analytics"].(map[string]interface{}) assert.Equal(t, float64(0), analytics["total_adaptations"]) reasons := analytics["reasons"].(map[string]interface{}) assert.Empty(t, reasons) } func TestBitrateHandler_GetAnalytics_ZeroTrackID(t *testing.T) { db, _ := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) logger := zaptest.NewLogger(t) bandwidthService := services.NewBandwidthDetectionService(logger) adaptationService := services.NewBitrateAdaptationService(db, bandwidthService, logger) router := setupTestBitrateHandlerRouterWithAnalytics(adaptationService, logger) // Using a Nil UUID to simulate "zero" or invalid specific UUID req, _ := http.NewRequest("GET", "/api/v1/tracks/"+uuid.Nil.String()+"/bitrate/analytics", nil) w := httptest.NewRecorder() router.ServeHTTP(w, req) // It might be 400 or 404 or 500 depending on handler implementation, but here likely 400 // In original test it was testing "0" which was invalid parse. uuid.Nil is valid UUID but might be rejected by logic. // But here the handler parses it. If it parses successfully, it goes to logic. // Let's check the original test expectation: 400. // If I pass uuid.Nil, it parses. // I should probably pass "00000000-0000-0000-0000-000000000000" (Nil). // The handler checks: if err != nil ... // If I pass "0", uuid.Parse returns error, so 400. // So I can keep passing "0" string if I want to test parse error. // Or use uuid.Nil if I want to test logic error. // The original test used "0" which fails parsing for UUID. // So I will use "0" string which causes uuid.Parse to fail. req, _ = http.NewRequest("GET", "/api/v1/tracks/0/bitrate/analytics", nil) w = httptest.NewRecorder() router.ServeHTTP(w, req) assert.Equal(t, http.StatusBadRequest, w.Code) var response map[string]interface{} json.Unmarshal(w.Body.Bytes(), &response) // MOD-P2-003: Format AppError standardisé - vérifier error.message if errorObj, ok := response["error"].(map[string]interface{}); ok { if message, ok := errorObj["message"].(string); ok { assert.Contains(t, message, "invalid track id") } else { t.Errorf("Expected error.message to be a string, got %T", errorObj["message"]) } } else { // Fallback pour compatibilité avec ancien format assert.Contains(t, response["error"], "invalid track id") } } func intPtr(i int) *int { return &i }