veza/veza-backend-api/internal/handlers/bitrate_handler_test.go
2025-12-16 11:23:49 -05:00

596 lines
21 KiB
Go

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
}