veza/veza-backend-api/internal/services/playback_analytics_service_test.go
senke a1000ce7fb style(backend): gofmt -w on 85 files (whitespace only)
backend-ci.yml's `test -z "$(gofmt -l .)"` strict gate (added in
13c21ac11) failed on a backlog of unformatted files. None of the
85 files in this commit had been edited since the gate was added
because no push touched veza-backend-api/** in between, so the
gate never fired until today's CI fixes triggered it.

The diff is exclusively whitespace alignment in struct literals
and trailing-space comments. `go build ./...` and the full test
suite (with VEZA_SKIP_INTEGRATION=1 -short) pass identically.
2026-04-14 12:22:14 +02:00

1056 lines
31 KiB
Go

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
}