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.
1056 lines
31 KiB
Go
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
|
|
}
|