package services import ( "context" "fmt" "testing" "time" "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/zap/zaptest" "gorm.io/driver/sqlite" "gorm.io/gorm" "veza-backend-api/internal/models" ) func setupTestPlaybackAnalyticsServiceDB(t *testing.T) (*gorm.DB, *PlaybackAnalyticsService) { db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) require.NoError(t, err) db.Exec("PRAGMA foreign_keys = ON") err = db.AutoMigrate(&models.User{}, &models.Track{}, &models.PlaybackAnalytics{}) require.NoError(t, err) logger := zaptest.NewLogger(t) service := NewPlaybackAnalyticsService(db, logger) return db, service } func TestNewPlaybackAnalyticsService(t *testing.T) { db, _ := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) logger := zaptest.NewLogger(t) service := NewPlaybackAnalyticsService(db, logger) assert.NotNil(t, service) assert.Equal(t, db, service.db) assert.NotNil(t, service.logger) } func TestNewPlaybackAnalyticsService_NilLogger(t *testing.T) { db, _ := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) service := NewPlaybackAnalyticsService(db, nil) assert.NotNil(t, service) assert.NotNil(t, service.logger) } func TestPlaybackAnalyticsService_CalculateCompletionRate(t *testing.T) { _, service := setupTestPlaybackAnalyticsServiceDB(t) // Test normal rate := service.CalculateCompletionRate(90, 180) assert.Equal(t, 50.0, rate) // Test 100% rate = service.CalculateCompletionRate(180, 180) assert.Equal(t, 100.0, rate) // Test 0% rate = service.CalculateCompletionRate(0, 180) assert.Equal(t, 0.0, rate) // Test > 100% (should be capped) rate = service.CalculateCompletionRate(200, 180) assert.Equal(t, 100.0, rate) // Test avec duration = 0 rate = service.CalculateCompletionRate(100, 0) assert.Equal(t, 0.0, rate) // Test avec playTime négatif rate = service.CalculateCompletionRate(-10, 180) assert.Equal(t, 0.0, rate) } func TestPlaybackAnalyticsService_RecordPlayback_Success(t *testing.T) { db, service := setupTestPlaybackAnalyticsServiceDB(t) ctx := context.Background() // Créer user et track 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) // Enregistrer analytics now := time.Now() analytics := &models.PlaybackAnalytics{ TrackID: trackID, UserID: userID, PlayTime: 120, PauseCount: 3, SeekCount: 5, StartedAt: now, EndedAt: &now, } err := service.RecordPlayback(ctx, analytics) assert.NoError(t, err) assert.NotZero(t, analytics.ID) // Use InDelta for floating point comparison (120/180 * 100 = 66.66666666666666) assert.InDelta(t, 66.67, analytics.CompletionRate, 0.01) // 120/180 * 100 } func TestPlaybackAnalyticsService_RecordPlayback_InvalidTrackID(t *testing.T) { _, service := setupTestPlaybackAnalyticsServiceDB(t) ctx := context.Background() analytics := &models.PlaybackAnalytics{ TrackID: uuid.Nil, UserID: uuid.New(), PlayTime: 120, StartedAt: time.Now(), } err := service.RecordPlayback(ctx, analytics) assert.Error(t, err) assert.Contains(t, err.Error(), "invalid track ID") } func TestPlaybackAnalyticsService_RecordPlayback_InvalidUserID(t *testing.T) { _, service := setupTestPlaybackAnalyticsServiceDB(t) ctx := context.Background() analytics := &models.PlaybackAnalytics{ TrackID: uuid.New(), UserID: uuid.Nil, PlayTime: 120, StartedAt: time.Now(), } err := service.RecordPlayback(ctx, analytics) assert.Error(t, err) assert.Contains(t, err.Error(), "invalid user ID") } func TestPlaybackAnalyticsService_RecordPlayback_TrackNotFound(t *testing.T) { _, service := setupTestPlaybackAnalyticsServiceDB(t) ctx := context.Background() analytics := &models.PlaybackAnalytics{ TrackID: uuid.New(), UserID: uuid.New(), PlayTime: 120, StartedAt: time.Now(), } err := service.RecordPlayback(ctx, analytics) assert.Error(t, err) assert.Contains(t, err.Error(), "track not found") } func TestPlaybackAnalyticsService_RecordPlayback_InvalidCompletionRate(t *testing.T) { db, service := setupTestPlaybackAnalyticsServiceDB(t) ctx := context.Background() 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) analytics := &models.PlaybackAnalytics{ TrackID: trackID, UserID: userID, PlayTime: 120, CompletionRate: 150.0, // > 100 StartedAt: time.Now(), } err := service.RecordPlayback(ctx, analytics) assert.Error(t, err) assert.Contains(t, err.Error(), "invalid completion rate") } func TestPlaybackAnalyticsService_RecordPlayback_ZeroStartedAt(t *testing.T) { db, service := setupTestPlaybackAnalyticsServiceDB(t) ctx := context.Background() 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) analytics := &models.PlaybackAnalytics{ TrackID: trackID, UserID: userID, PlayTime: 120, StartedAt: time.Time{}, // Zero time } err := service.RecordPlayback(ctx, analytics) assert.Error(t, err) assert.Contains(t, err.Error(), "started_at is required") } func TestPlaybackAnalyticsService_GetTrackStats(t *testing.T) { db, service := setupTestPlaybackAnalyticsServiceDB(t) ctx := context.Background() // Créer 5 utilisateurs distincts pour satisfaire le seuil k-anonymity (>=5 listeners) userIDs := make([]uuid.UUID, 5) for i := 0; i < 5; i++ { userIDs[i] = uuid.New() user := &models.User{ ID: userIDs[i], Username: fmt.Sprintf("testuser%d", i), Email: fmt.Sprintf("test%d@example.com", i), IsActive: true, } db.Create(user) } trackID := uuid.New() track := &models.Track{ ID: trackID, UserID: userIDs[0], Title: "Test Track", FilePath: "/test.mp3", FileSize: 1024, Format: "MP3", Duration: 180, IsPublic: true, Status: models.TrackStatusCompleted, } db.Create(track) // Créer 5 sessions, une par utilisateur distinct // PlayTime: 120+180+90+100+110 = 600, PauseCount: 2+1+3+2+2 = 10, SeekCount: 3+1+5+2+4 = 15 // CompletionRate: 66.67+100+50+80+95 = 391.67 => avg = 78.334 // Sessions >= 90% completion: 100, 95 => 2/5 = 40% now := time.Now() sessions := []*models.PlaybackAnalytics{ {TrackID: trackID, UserID: userIDs[0], PlayTime: 120, PauseCount: 2, SeekCount: 3, CompletionRate: 66.67, StartedAt: now}, {TrackID: trackID, UserID: userIDs[1], PlayTime: 180, PauseCount: 1, SeekCount: 1, CompletionRate: 100.0, StartedAt: now}, {TrackID: trackID, UserID: userIDs[2], PlayTime: 90, PauseCount: 3, SeekCount: 5, CompletionRate: 50.0, StartedAt: now}, {TrackID: trackID, UserID: userIDs[3], PlayTime: 100, PauseCount: 2, SeekCount: 2, CompletionRate: 80.0, StartedAt: now}, {TrackID: trackID, UserID: userIDs[4], PlayTime: 110, PauseCount: 2, SeekCount: 4, CompletionRate: 95.0, StartedAt: now}, } for _, session := range sessions { db.Create(session) } stats, err := service.GetTrackStats(ctx, trackID) require.NoError(t, err) assert.Equal(t, int64(5), stats.TotalSessions) assert.Equal(t, int64(600), stats.TotalPlayTime) // 120+180+90+100+110 assert.Equal(t, 120.0, stats.AveragePlayTime) // 600 / 5 assert.Equal(t, int64(10), stats.TotalPauses) // 2+1+3+2+2 assert.Equal(t, 2.0, stats.AveragePauses) // 10 / 5 assert.Equal(t, int64(15), stats.TotalSeeks) // 3+1+5+2+4 assert.Equal(t, 3.0, stats.AverageSeeks) // 15 / 5 assert.InDelta(t, 78.33, stats.AverageCompletion, 0.1) // (66.67+100+50+80+95) / 5 assert.InDelta(t, 40.0, stats.CompletionRate, 0.01) // 2 sessions with >= 90% / 5 } func TestPlaybackAnalyticsService_GetTrackStats_NoSessions(t *testing.T) { db, service := setupTestPlaybackAnalyticsServiceDB(t) ctx := context.Background() 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) stats, err := service.GetTrackStats(ctx, trackID) require.NoError(t, err) assert.Equal(t, int64(0), stats.TotalSessions) assert.Equal(t, int64(0), stats.TotalPlayTime) assert.Equal(t, 0.0, stats.AveragePlayTime) } func TestPlaybackAnalyticsService_GetTrackStats_TrackNotFound(t *testing.T) { _, service := setupTestPlaybackAnalyticsServiceDB(t) ctx := context.Background() _, err := service.GetTrackStats(ctx, uuid.New()) assert.Error(t, err) assert.Contains(t, err.Error(), "track not found") } func TestPlaybackAnalyticsService_GetUserStats(t *testing.T) { db, service := setupTestPlaybackAnalyticsServiceDB(t) ctx := context.Background() userID := uuid.New() user := &models.User{ID: userID, Username: "testuser", Email: "test@example.com", IsActive: true} db.Create(user) track1ID := uuid.New() track2ID := uuid.New() track1 := &models.Track{ID: track1ID, UserID: userID, Title: "Track 1", FilePath: "/1.mp3", FileSize: 1024, Format: "MP3", Duration: 180, IsPublic: true, Status: models.TrackStatusCompleted} track2 := &models.Track{ID: track2ID, UserID: userID, Title: "Track 2", FilePath: "/2.mp3", FileSize: 1024, Format: "MP3", Duration: 120, IsPublic: true, Status: models.TrackStatusCompleted} db.Create(track1) db.Create(track2) now := time.Now() sessions := []*models.PlaybackAnalytics{ {TrackID: track1ID, UserID: userID, PlayTime: 120, PauseCount: 2, SeekCount: 3, CompletionRate: 66.67, StartedAt: now}, {TrackID: track2ID, UserID: userID, PlayTime: 100, PauseCount: 1, SeekCount: 2, CompletionRate: 83.33, StartedAt: now}, } for _, session := range sessions { db.Create(session) } stats, err := service.GetUserStats(ctx, userID) require.NoError(t, err) assert.Equal(t, int64(2), stats.TotalSessions) assert.Equal(t, int64(220), stats.TotalPlayTime) // 120 + 100 assert.Equal(t, 110.0, stats.AveragePlayTime) // 220 / 2 assert.Equal(t, int64(3), stats.TotalPauses) // 2 + 1 assert.Equal(t, 1.5, stats.AveragePauses) // 3 / 2 assert.Equal(t, int64(5), stats.TotalSeeks) // 3 + 2 assert.Equal(t, 2.5, stats.AverageSeeks) // 5 / 2 } func TestPlaybackAnalyticsService_GetUserStats_UserNotFound(t *testing.T) { _, service := setupTestPlaybackAnalyticsServiceDB(t) ctx := context.Background() _, err := service.GetUserStats(ctx, uuid.New()) assert.Error(t, err) assert.Contains(t, err.Error(), "user not found") } func TestPlaybackAnalyticsService_GetSessionsByDateRange(t *testing.T) { db, service := setupTestPlaybackAnalyticsServiceDB(t) ctx := context.Background() 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) // Créer des sessions à différentes dates baseTime := time.Date(2024, 1, 15, 12, 0, 0, 0, time.UTC) sessions := []*models.PlaybackAnalytics{ {TrackID: trackID, UserID: userID, PlayTime: 120, StartedAt: baseTime.AddDate(0, 0, -2), CreatedAt: baseTime.AddDate(0, 0, -2)}, // 2 jours avant {TrackID: trackID, UserID: userID, PlayTime: 180, StartedAt: baseTime.AddDate(0, 0, -1), CreatedAt: baseTime.AddDate(0, 0, -1)}, // 1 jour avant {TrackID: trackID, UserID: userID, PlayTime: 90, StartedAt: baseTime, CreatedAt: baseTime}, // Aujourd'hui {TrackID: trackID, UserID: userID, PlayTime: 100, StartedAt: baseTime.AddDate(0, 0, 1), CreatedAt: baseTime.AddDate(0, 0, 1)}, // 1 jour après } for _, session := range sessions { db.Create(session) } // Récupérer les sessions des 3 derniers jours startDate := baseTime.AddDate(0, 0, -2) endDate := baseTime result, err := service.GetSessionsByDateRange(ctx, trackID, startDate, endDate) require.NoError(t, err) // Devrait retourner 3 sessions (2 jours avant, 1 jour avant, aujourd'hui) assert.Len(t, result, 3) } func TestPlaybackAnalyticsService_GetSessionsByDateRange_InvalidTrackID(t *testing.T) { _, service := setupTestPlaybackAnalyticsServiceDB(t) ctx := context.Background() startDate := time.Now().AddDate(0, 0, -7) endDate := time.Now() _, err := service.GetSessionsByDateRange(ctx, uuid.Nil, startDate, endDate) assert.Error(t, err) assert.Contains(t, err.Error(), "invalid track ID") } // Tests pour TrackCompletion (T0366) func TestPlaybackAnalyticsService_TrackCompletion_Success(t *testing.T) { db, service := setupTestPlaybackAnalyticsServiceDB(t) ctx := context.Background() // Créer user et track // Créer user et track 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) // Créer une session d'analytics now := time.Now() analytics := &models.PlaybackAnalytics{ TrackID: trackID, UserID: userID, PlayTime: 171, // 95% de 180 secondes PauseCount: 2, SeekCount: 3, CompletionRate: 0, // Sera calculé StartedAt: now, } db.Create(analytics) // Tester le tracking de completion err := service.TrackCompletion(ctx, analytics, 180) require.NoError(t, err) // Vérifier que le completion rate a été calculé assert.InDelta(t, 95.0, analytics.CompletionRate, 0.1) // Vérifier que EndedAt a été défini (completion ≥95%) assert.NotNil(t, analytics.EndedAt) // Vérifier dans la base de données var updatedAnalytics models.PlaybackAnalytics db.First(&updatedAnalytics, analytics.ID) assert.InDelta(t, 95.0, updatedAnalytics.CompletionRate, 0.1) assert.NotNil(t, updatedAnalytics.EndedAt) } func TestPlaybackAnalyticsService_TrackCompletion_NotCompleted(t *testing.T) { db, service := setupTestPlaybackAnalyticsServiceDB(t) ctx := context.Background() 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) now := time.Now() analytics := &models.PlaybackAnalytics{ TrackID: trackID, UserID: userID, PlayTime: 90, // 50% de 180 secondes PauseCount: 2, SeekCount: 3, CompletionRate: 0, StartedAt: now, } db.Create(analytics) err := service.TrackCompletion(ctx, analytics, 180) require.NoError(t, err) // Vérifier que le completion rate a été calculé assert.InDelta(t, 50.0, analytics.CompletionRate, 0.1) // Vérifier que EndedAt n'a PAS été défini (<95%) assert.Nil(t, analytics.EndedAt) // Vérifier dans la base de données var updatedAnalytics models.PlaybackAnalytics db.First(&updatedAnalytics, analytics.ID) assert.InDelta(t, 50.0, updatedAnalytics.CompletionRate, 0.1) assert.Nil(t, updatedAnalytics.EndedAt) } func TestPlaybackAnalyticsService_TrackCompletion_Exactly95(t *testing.T) { db, service := setupTestPlaybackAnalyticsServiceDB(t) ctx := context.Background() 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) now := time.Now() analytics := &models.PlaybackAnalytics{ TrackID: trackID, UserID: userID, PlayTime: 171, // Exactement 95% (171/180 = 0.95) PauseCount: 2, SeekCount: 3, CompletionRate: 0, StartedAt: now, } db.Create(analytics) err := service.TrackCompletion(ctx, analytics, 180) require.NoError(t, err) // Vérifier que EndedAt a été défini (≥95%) assert.NotNil(t, analytics.EndedAt) } func TestPlaybackAnalyticsService_TrackCompletion_100Percent(t *testing.T) { db, service := setupTestPlaybackAnalyticsServiceDB(t) ctx := context.Background() 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) now := time.Now() analytics := &models.PlaybackAnalytics{ TrackID: trackID, UserID: userID, PlayTime: 180, // 100% PauseCount: 2, SeekCount: 3, CompletionRate: 0, StartedAt: now, } db.Create(analytics) err := service.TrackCompletion(ctx, analytics, 180) require.NoError(t, err) assert.Equal(t, 100.0, analytics.CompletionRate) assert.NotNil(t, analytics.EndedAt) } func TestPlaybackAnalyticsService_TrackCompletion_NilAnalytics(t *testing.T) { _, service := setupTestPlaybackAnalyticsServiceDB(t) ctx := context.Background() err := service.TrackCompletion(ctx, nil, 180) assert.Error(t, err) assert.Contains(t, err.Error(), "analytics cannot be nil") } func TestPlaybackAnalyticsService_TrackCompletion_NotSaved(t *testing.T) { _, service := setupTestPlaybackAnalyticsServiceDB(t) ctx := context.Background() analytics := &models.PlaybackAnalytics{ ID: uuid.Nil, // Non sauvegardé TrackID: uuid.New(), UserID: uuid.New(), PlayTime: 90, StartedAt: time.Now(), } err := service.TrackCompletion(ctx, analytics, 180) assert.Error(t, err) assert.Contains(t, err.Error(), "analytics must be saved") } func TestPlaybackAnalyticsService_TrackCompletion_InvalidDuration(t *testing.T) { db, service := setupTestPlaybackAnalyticsServiceDB(t) ctx := context.Background() 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) now := time.Now() analytics := &models.PlaybackAnalytics{ TrackID: trackID, UserID: userID, PlayTime: 90, StartedAt: now, } db.Create(analytics) err := service.TrackCompletion(ctx, analytics, 0) assert.Error(t, err) assert.Contains(t, err.Error(), "invalid track duration") } func TestPlaybackAnalyticsService_UpdatePlaybackProgress_Success(t *testing.T) { db, service := setupTestPlaybackAnalyticsServiceDB(t) ctx := context.Background() 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) now := time.Now() analytics := &models.PlaybackAnalytics{ TrackID: trackID, UserID: userID, PlayTime: 50, StartedAt: now, } db.Create(analytics) // Mettre à jour le progrès err := service.UpdatePlaybackProgress(ctx, analytics.ID, 171, 180) require.NoError(t, err) // Vérifier que le progrès a été mis à jour var updatedAnalytics models.PlaybackAnalytics db.First(&updatedAnalytics, analytics.ID) assert.Equal(t, 171, updatedAnalytics.PlayTime) assert.InDelta(t, 95.0, updatedAnalytics.CompletionRate, 0.1) assert.NotNil(t, updatedAnalytics.EndedAt) } func TestPlaybackAnalyticsService_UpdatePlaybackProgress_AnalyticsNotFound(t *testing.T) { _, service := setupTestPlaybackAnalyticsServiceDB(t) ctx := context.Background() err := service.UpdatePlaybackProgress(ctx, uuid.New(), 90, 180) assert.Error(t, err) assert.Contains(t, err.Error(), "analytics not found") } func TestPlaybackAnalyticsService_UpdatePlaybackProgress_InvalidParams(t *testing.T) { _, service := setupTestPlaybackAnalyticsServiceDB(t) ctx := context.Background() // Test avec analytics ID invalide err := service.UpdatePlaybackProgress(ctx, uuid.Nil, 90, 180) assert.Error(t, err) assert.Contains(t, err.Error(), "invalid analytics ID") // Test avec play time négatif err = service.UpdatePlaybackProgress(ctx, uuid.New(), -10, 180) assert.Error(t, err) assert.Contains(t, err.Error(), "invalid play time") // Test avec duration invalide err = service.UpdatePlaybackProgress(ctx, uuid.New(), 90, 0) assert.Error(t, err) assert.Contains(t, err.Error(), "invalid track duration") } // Tests pour les optimisations de performance (T0381) func TestPlaybackAnalyticsService_NewPlaybackAnalyticsServiceWithCache(t *testing.T) { db, _ := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) logger := zaptest.NewLogger(t) // Créer un mock cache service (simplifié pour les tests) // Note: Dans un vrai test, on utiliserait un vrai client Redis ou un mock service := NewPlaybackAnalyticsService(db, logger) assert.NotNil(t, service) assert.Nil(t, service.cache) // Pas de cache par défaut assert.Equal(t, 100, service.batchSize) assert.Equal(t, 5*time.Minute, service.cacheTTL) } func TestPlaybackAnalyticsService_SetBatchSize(t *testing.T) { _, service := setupTestPlaybackAnalyticsServiceDB(t) // Test avec une taille valide service.SetBatchSize(50) assert.Equal(t, 50, service.batchSize) // Test avec une taille invalide (devrait garder la valeur précédente) service.SetBatchSize(0) assert.Equal(t, 50, service.batchSize) // Devrait rester à 50 // Test avec une taille négative service.SetBatchSize(-10) assert.Equal(t, 50, service.batchSize) // Devrait rester à 50 } func TestPlaybackAnalyticsService_RecordPlaybackBatch(t *testing.T) { db, service := setupTestPlaybackAnalyticsServiceDB(t) ctx := context.Background() // Créer user et track userID := uuid.New() user := &models.User{ID: userID, Username: "testuser", Slug: "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) // Créer plusieurs analytics now := time.Now() analyticsList := []*models.PlaybackAnalytics{ {TrackID: trackID, UserID: userID, PlayTime: 120, PauseCount: 1, SeekCount: 2, StartedAt: now}, {TrackID: trackID, UserID: userID, PlayTime: 180, PauseCount: 0, SeekCount: 0, StartedAt: now}, {TrackID: trackID, UserID: userID, PlayTime: 90, PauseCount: 2, SeekCount: 3, StartedAt: now}, } err := service.RecordPlaybackBatch(ctx, analyticsList) require.NoError(t, err) // Vérifier que tous les analytics ont été enregistrés var count int64 db.Model(&models.PlaybackAnalytics{}).Where("track_id = ?", trackID).Count(&count) assert.Equal(t, int64(3), count) } func TestPlaybackAnalyticsService_RecordPlaybackBatch_EmptyList(t *testing.T) { _, service := setupTestPlaybackAnalyticsServiceDB(t) ctx := context.Background() err := service.RecordPlaybackBatch(ctx, []*models.PlaybackAnalytics{}) assert.Error(t, err) assert.Contains(t, err.Error(), "analytics list cannot be empty") } func TestPlaybackAnalyticsService_RecordPlaybackBatch_InvalidData(t *testing.T) { _, service := setupTestPlaybackAnalyticsServiceDB(t) ctx := context.Background() now := time.Now() analyticsList := []*models.PlaybackAnalytics{ {TrackID: uuid.Nil, UserID: uuid.New(), PlayTime: 120, StartedAt: now}, // TrackID invalide } err := service.RecordPlaybackBatch(ctx, analyticsList) assert.Error(t, err) assert.Contains(t, err.Error(), "invalid track ID") } func TestPlaybackAnalyticsService_GetSessionsByDateRangePaginated(t *testing.T) { db, service := setupTestPlaybackAnalyticsServiceDB(t) ctx := context.Background() // Créer user et track userID := uuid.New() user := &models.User{ID: userID, Username: "testuser", Slug: "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) // Créer 10 sessions now := time.Now() for i := 0; i < 10; i++ { analytics := &models.PlaybackAnalytics{ TrackID: trackID, UserID: userID, PlayTime: 120 + i*10, StartedAt: now.Add(time.Duration(i) * time.Hour), CreatedAt: now.Add(time.Duration(i) * time.Hour), } db.Create(analytics) } startDate := now.Add(-1 * time.Hour) endDate := now.Add(12 * time.Hour) // Page 1, 5 éléments par page result, err := service.GetSessionsByDateRangePaginated(ctx, trackID, startDate, endDate, 1, 5) require.NoError(t, err) assert.Equal(t, 5, len(result)) // Page 2, 5 éléments par page result2, err := service.GetSessionsByDateRangePaginated(ctx, trackID, startDate, endDate, 2, 5) require.NoError(t, err) assert.Equal(t, 5, len(result2)) // Vérifier qu'il n'y a pas de doublons ids1 := make(map[uuid.UUID]bool) for _, s := range result { ids1[s.ID] = true } for _, s := range result2 { assert.False(t, ids1[s.ID], "Duplicate ID found") } } func TestPlaybackAnalyticsService_GetSessionsByDateRangePaginatedResult(t *testing.T) { db, service := setupTestPlaybackAnalyticsServiceDB(t) ctx := context.Background() // Créer user et track userID := uuid.New() user := &models.User{ID: userID, Username: "testuser", Slug: "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) // Créer 25 sessions now := time.Now() for i := 0; i < 25; i++ { analytics := &models.PlaybackAnalytics{ TrackID: trackID, UserID: userID, PlayTime: 120 + i*10, StartedAt: now.Add(time.Duration(i) * time.Hour), CreatedAt: now.Add(time.Duration(i) * time.Hour), } db.Create(analytics) } startDate := now.Add(-1 * time.Hour) endDate := now.Add(26 * time.Hour) // Tester avec pagination result, err := service.GetSessionsByDateRangePaginatedResult(ctx, trackID, startDate, endDate, 1, 10) require.NoError(t, err) assert.Equal(t, int64(25), result.Total) assert.Equal(t, 1, result.Page) assert.Equal(t, 10, result.PageSize) assert.Equal(t, 3, result.TotalPages) // 25 / 10 = 2.5, arrondi à 3 assert.Equal(t, 10, len(result.Data)) } func TestPlaybackAnalyticsService_GetSessionsByDateRangePaginatedResult_DefaultValues(t *testing.T) { db, service := setupTestPlaybackAnalyticsServiceDB(t) ctx := context.Background() // Créer user et track userID := uuid.New() user := &models.User{ID: userID, Username: "testuser", Slug: "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) now := time.Now() for i := 0; i < 25; i++ { analytics := &models.PlaybackAnalytics{ TrackID: trackID, UserID: userID, PlayTime: 120 + i*10, StartedAt: now.Add(time.Duration(i) * time.Hour), CreatedAt: now.Add(time.Duration(i) * time.Hour), } db.Create(analytics) } startDate := now.Add(-1 * time.Hour) endDate := now.Add(26 * time.Hour) // Tester avec page = 0 (devrait devenir 1) result, err := service.GetSessionsByDateRangePaginatedResult(ctx, trackID, startDate, endDate, 0, 0) require.NoError(t, err) assert.Equal(t, 1, result.Page) assert.Equal(t, 50, result.PageSize) // Taille par défaut // Tester avec pageSize > 1000 (devrait être limité à 1000) result2, err := service.GetSessionsByDateRangePaginatedResult(ctx, trackID, startDate, endDate, 1, 2000) require.NoError(t, err) assert.Equal(t, 1000, result2.PageSize) // Limite maximale } func TestPlaybackAnalyticsService_GetSessionsByDateRangePaginated_NoPagination(t *testing.T) { db, service := setupTestPlaybackAnalyticsServiceDB(t) ctx := context.Background() // Créer user et track userID := uuid.New() user := &models.User{ID: userID, Username: "testuser", Slug: "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) // Créer 5 sessions now := time.Now() for i := 0; i < 5; i++ { analytics := &models.PlaybackAnalytics{ TrackID: trackID, UserID: userID, PlayTime: 120, StartedAt: now.Add(time.Duration(i) * time.Hour), CreatedAt: now.Add(time.Duration(i) * time.Hour), } db.Create(analytics) } startDate := now.Add(-1 * time.Hour) endDate := now.Add(6 * time.Hour) // Tester sans pagination (pageSize = 0) result, err := service.GetSessionsByDateRangePaginated(ctx, trackID, startDate, endDate, 0, 100) require.NoError(t, err) assert.Equal(t, 5, len(result)) // Devrait retourner toutes les sessions }