448 lines
14 KiB
Go
448 lines
14 KiB
Go
package services
|
|
|
|
import (
|
|
"context"
|
|
"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 setupTestPlaybackRetentionServiceDB(t *testing.T) (*gorm.DB, *PlaybackRetentionService) {
|
|
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 := NewPlaybackRetentionService(db, logger)
|
|
|
|
return db, service
|
|
}
|
|
|
|
func TestNewPlaybackRetentionService(t *testing.T) {
|
|
db, _ := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
|
|
logger := zaptest.NewLogger(t)
|
|
|
|
service := NewPlaybackRetentionService(db, logger)
|
|
|
|
assert.NotNil(t, service)
|
|
assert.Equal(t, db, service.db)
|
|
assert.NotNil(t, service.logger)
|
|
}
|
|
|
|
func TestNewPlaybackRetentionService_NilLogger(t *testing.T) {
|
|
db, _ := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
|
|
|
|
service := NewPlaybackRetentionService(db, nil)
|
|
|
|
assert.NotNil(t, service)
|
|
assert.NotNil(t, service.logger)
|
|
}
|
|
|
|
func TestPlaybackRetentionService_AnalyzeRetention_NoSessions(t *testing.T) {
|
|
db, service := setupTestPlaybackRetentionServiceDB(t)
|
|
ctx := context.Background()
|
|
|
|
// Créer user et track
|
|
userID := uuid.New()
|
|
trackID := uuid.New()
|
|
user := &models.User{ID: userID, Username: "testuser", Email: "test@example.com", IsActive: true}
|
|
db.Create(user)
|
|
track := &models.Track{
|
|
ID: trackID,
|
|
UserID: userID,
|
|
Title: "Test Track",
|
|
FilePath: "/test.mp3",
|
|
FileSize: 1024,
|
|
Format: "MP3",
|
|
Duration: 180,
|
|
IsPublic: true,
|
|
Status: models.TrackStatusCompleted,
|
|
}
|
|
db.Create(track)
|
|
|
|
result, err := service.AnalyzeRetention(ctx, trackID, 10)
|
|
|
|
require.NoError(t, err)
|
|
assert.NotNil(t, result)
|
|
assert.Equal(t, trackID, result.TrackID)
|
|
assert.Equal(t, 180, result.TrackDuration)
|
|
assert.Equal(t, int64(0), result.TotalSessions)
|
|
assert.Len(t, result.SegmentRetentions, 10)
|
|
assert.Len(t, result.ExitPoints, 0)
|
|
}
|
|
|
|
func TestPlaybackRetentionService_AnalyzeRetention_InvalidTrackID(t *testing.T) {
|
|
_, service := setupTestPlaybackRetentionServiceDB(t)
|
|
ctx := context.Background()
|
|
|
|
result, err := service.AnalyzeRetention(ctx, uuid.Nil, 10)
|
|
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "invalid track ID")
|
|
assert.Nil(t, result)
|
|
}
|
|
|
|
func TestPlaybackRetentionService_AnalyzeRetention_TrackNotFound(t *testing.T) {
|
|
_, service := setupTestPlaybackRetentionServiceDB(t)
|
|
ctx := context.Background()
|
|
|
|
result, err := service.AnalyzeRetention(ctx, uuid.New(), 10)
|
|
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "track not found")
|
|
assert.Nil(t, result)
|
|
}
|
|
|
|
func TestPlaybackRetentionService_AnalyzeRetention_WithSessions(t *testing.T) {
|
|
db, service := setupTestPlaybackRetentionServiceDB(t)
|
|
ctx := context.Background()
|
|
|
|
// Créer user et track
|
|
userID := uuid.New()
|
|
trackID := uuid.New()
|
|
user := &models.User{ID: userID, Username: "testuser", Email: "test@example.com", IsActive: true}
|
|
db.Create(user)
|
|
track := &models.Track{
|
|
ID: trackID,
|
|
UserID: userID,
|
|
Title: "Test Track",
|
|
FilePath: "/test.mp3",
|
|
FileSize: 1024,
|
|
Format: "MP3",
|
|
Duration: 180, // 3 minutes
|
|
IsPublic: true,
|
|
Status: models.TrackStatusCompleted,
|
|
}
|
|
db.Create(track)
|
|
|
|
// Créer des analytics avec différents taux de complétion
|
|
now := time.Now()
|
|
analytics1 := &models.PlaybackAnalytics{
|
|
TrackID: trackID,
|
|
UserID: userID,
|
|
PlayTime: 90, // 50% de 180
|
|
PauseCount: 2,
|
|
SeekCount: 1,
|
|
CompletionRate: 50.0,
|
|
StartedAt: now,
|
|
CreatedAt: now,
|
|
}
|
|
analytics2 := &models.PlaybackAnalytics{
|
|
TrackID: trackID,
|
|
UserID: userID,
|
|
PlayTime: 135, // 75% de 180
|
|
PauseCount: 1,
|
|
SeekCount: 0,
|
|
CompletionRate: 75.0,
|
|
StartedAt: now,
|
|
CreatedAt: now,
|
|
}
|
|
analytics3 := &models.PlaybackAnalytics{
|
|
TrackID: trackID,
|
|
UserID: userID,
|
|
PlayTime: 180, // 100% de 180
|
|
PauseCount: 0,
|
|
SeekCount: 0,
|
|
CompletionRate: 100.0,
|
|
StartedAt: now,
|
|
CreatedAt: now,
|
|
}
|
|
db.Create(analytics1)
|
|
db.Create(analytics2)
|
|
db.Create(analytics3)
|
|
|
|
result, err := service.AnalyzeRetention(ctx, trackID, 10)
|
|
|
|
require.NoError(t, err)
|
|
assert.NotNil(t, result)
|
|
assert.Equal(t, trackID, result.TrackID)
|
|
assert.Equal(t, 180, result.TrackDuration)
|
|
assert.Equal(t, int64(3), result.TotalSessions)
|
|
assert.Len(t, result.SegmentRetentions, 10)
|
|
assert.Greater(t, len(result.ExitPoints), 0)
|
|
assert.NotZero(t, result.EngagementMetrics.EngagementScore)
|
|
}
|
|
|
|
func TestPlaybackRetentionService_CalculateSegmentRetention(t *testing.T) {
|
|
_, service := setupTestPlaybackRetentionServiceDB(t)
|
|
|
|
// Créer des analytics avec différents taux de complétion
|
|
analytics := []models.PlaybackAnalytics{
|
|
{PlayTime: 90, CompletionRate: 50.0}, // 50% de 180
|
|
{PlayTime: 135, CompletionRate: 75.0}, // 75% de 180
|
|
{PlayTime: 180, CompletionRate: 100.0}, // 100% de 180
|
|
}
|
|
|
|
retentions := service.calculateSegmentRetention(analytics, 180, 10)
|
|
|
|
assert.Len(t, retentions, 10)
|
|
|
|
// Vérifier que le premier segment (0-10%) a 100% de rétention (toutes les sessions commencent)
|
|
assert.Equal(t, 100.0, retentions[0].RetentionRate)
|
|
|
|
// Vérifier que le segment 5 (50-60%) a 100% de rétention (toutes les sessions atteignent 50%)
|
|
assert.Equal(t, 100.0, retentions[5].RetentionRate)
|
|
|
|
// Vérifier que le segment 8 (80-90%) a moins de rétention (seulement 2 sessions atteignent 80%)
|
|
assert.Less(t, retentions[8].RetentionRate, 100.0)
|
|
}
|
|
|
|
func TestPlaybackRetentionService_IdentifyExitPoints(t *testing.T) {
|
|
_, service := setupTestPlaybackRetentionServiceDB(t)
|
|
|
|
// Créer des analytics avec différents points de sortie
|
|
analytics := []models.PlaybackAnalytics{
|
|
{PlayTime: 45, CompletionRate: 25.0}, // Sortie à 25%
|
|
{PlayTime: 45, CompletionRate: 25.0}, // Sortie à 25%
|
|
{PlayTime: 90, CompletionRate: 50.0}, // Sortie à 50%
|
|
{PlayTime: 135, CompletionRate: 75.0}, // Sortie à 75%
|
|
{PlayTime: 180, CompletionRate: 100.0}, // Complétion à 100%
|
|
}
|
|
|
|
exitPoints := service.identifyExitPoints(analytics, 10)
|
|
|
|
assert.NotNil(t, exitPoints)
|
|
assert.Greater(t, len(exitPoints), 0)
|
|
assert.LessOrEqual(t, len(exitPoints), 5) // Maximum 5 points de sortie
|
|
|
|
// Vérifier que les points de sortie sont triés par taux de sortie décroissant
|
|
for i := 0; i < len(exitPoints)-1; i++ {
|
|
assert.GreaterOrEqual(t, exitPoints[i].ExitRate, exitPoints[i+1].ExitRate)
|
|
}
|
|
}
|
|
|
|
func TestPlaybackRetentionService_AnalyzeEngagement(t *testing.T) {
|
|
_, service := setupTestPlaybackRetentionServiceDB(t)
|
|
|
|
// Créer des analytics avec différents niveaux d'engagement
|
|
analytics := []models.PlaybackAnalytics{
|
|
{PlayTime: 18, CompletionRate: 10.0, PauseCount: 5, SeekCount: 3}, // Faible engagement (<25%)
|
|
{PlayTime: 90, CompletionRate: 50.0, PauseCount: 2, SeekCount: 1}, // Engagement moyen
|
|
{PlayTime: 135, CompletionRate: 75.0, PauseCount: 1, SeekCount: 0}, // Engagement élevé (>=75%)
|
|
{PlayTime: 180, CompletionRate: 100.0, PauseCount: 0, SeekCount: 0}, // Engagement très élevé (>=75%)
|
|
}
|
|
|
|
metrics := service.analyzeEngagement(analytics)
|
|
|
|
assert.NotNil(t, metrics)
|
|
assert.InDelta(t, 58.75, metrics.AverageCompletion, 0.1) // (10 + 50 + 75 + 100) / 4 = 58.75
|
|
assert.InDelta(t, 58.75, metrics.OverallRetentionRate, 0.1) // Même valeur que AverageCompletion
|
|
assert.Equal(t, 50.0, metrics.HighEngagementRate) // 2 sessions sur 4 avec >=75%
|
|
assert.Equal(t, 25.0, metrics.LowEngagementRate) // 1 session sur 4 avec <25% (10% < 25%)
|
|
assert.Equal(t, 2.0, metrics.AveragePauses) // (5 + 2 + 1 + 0) / 4
|
|
assert.Equal(t, 1.0, metrics.AverageSeeks) // (3 + 1 + 0 + 0) / 4
|
|
assert.Greater(t, metrics.EngagementScore, 0.0)
|
|
assert.LessOrEqual(t, metrics.EngagementScore, 100.0)
|
|
}
|
|
|
|
func TestPlaybackRetentionService_AnalyzeRetention_DefaultSegmentCount(t *testing.T) {
|
|
db, service := setupTestPlaybackRetentionServiceDB(t)
|
|
ctx := context.Background()
|
|
|
|
// Créer user et track
|
|
userID := uuid.New()
|
|
trackID := uuid.New()
|
|
user := &models.User{ID: userID, Username: "testuser", Email: "test@example.com", IsActive: true}
|
|
db.Create(user)
|
|
track := &models.Track{
|
|
ID: trackID,
|
|
UserID: userID,
|
|
Title: "Test Track",
|
|
FilePath: "/test.mp3",
|
|
FileSize: 1024,
|
|
Format: "MP3",
|
|
Duration: 180,
|
|
IsPublic: true,
|
|
Status: models.TrackStatusCompleted,
|
|
}
|
|
db.Create(track)
|
|
|
|
// Utiliser 0 pour le segmentCount (devrait utiliser la valeur par défaut de 10)
|
|
result, err := service.AnalyzeRetention(ctx, trackID, 0)
|
|
|
|
require.NoError(t, err)
|
|
assert.NotNil(t, result)
|
|
assert.Len(t, result.SegmentRetentions, 10) // Valeur par défaut
|
|
}
|
|
|
|
func TestPlaybackRetentionService_AnalyzeRetention_MaxSegmentCount(t *testing.T) {
|
|
db, service := setupTestPlaybackRetentionServiceDB(t)
|
|
ctx := context.Background()
|
|
|
|
// Créer user et track
|
|
userID := uuid.New()
|
|
trackID := uuid.New()
|
|
user := &models.User{ID: userID, Username: "testuser", Email: "test@example.com", IsActive: true}
|
|
db.Create(user)
|
|
track := &models.Track{
|
|
ID: trackID,
|
|
UserID: userID,
|
|
Title: "Test Track",
|
|
FilePath: "/test.mp3",
|
|
FileSize: 1024,
|
|
Format: "MP3",
|
|
Duration: 180,
|
|
IsPublic: true,
|
|
Status: models.TrackStatusCompleted,
|
|
}
|
|
db.Create(track)
|
|
|
|
// Utiliser un nombre très élevé (devrait être limité à 100)
|
|
result, err := service.AnalyzeRetention(ctx, trackID, 200)
|
|
|
|
require.NoError(t, err)
|
|
assert.NotNil(t, result)
|
|
assert.Len(t, result.SegmentRetentions, 100) // Maximum
|
|
}
|
|
|
|
func TestPlaybackRetentionService_AnalyzeRetention_InvalidDuration(t *testing.T) {
|
|
db, service := setupTestPlaybackRetentionServiceDB(t)
|
|
ctx := context.Background()
|
|
|
|
// Créer user et track avec durée invalide
|
|
userID := uuid.New()
|
|
trackID := uuid.New()
|
|
user := &models.User{ID: userID, Username: "testuser", Email: "test@example.com", IsActive: true}
|
|
db.Create(user)
|
|
track := &models.Track{
|
|
ID: trackID,
|
|
UserID: userID,
|
|
Title: "Test Track",
|
|
FilePath: "/test.mp3",
|
|
FileSize: 1024,
|
|
Format: "MP3",
|
|
Duration: 0, // Durée invalide
|
|
IsPublic: true,
|
|
Status: models.TrackStatusCompleted,
|
|
}
|
|
db.Create(track)
|
|
|
|
result, err := service.AnalyzeRetention(ctx, trackID, 10)
|
|
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "invalid duration")
|
|
assert.Nil(t, result)
|
|
}
|
|
|
|
func TestPlaybackRetentionService_AnalyzeEngagement_Empty(t *testing.T) {
|
|
_, service := setupTestPlaybackRetentionServiceDB(t)
|
|
|
|
analytics := []models.PlaybackAnalytics{}
|
|
metrics := service.analyzeEngagement(analytics)
|
|
|
|
assert.Equal(t, EngagementMetrics{}, metrics)
|
|
}
|
|
|
|
func TestPlaybackRetentionService_CalculateSegmentRetention_AllComplete(t *testing.T) {
|
|
_, service := setupTestPlaybackRetentionServiceDB(t)
|
|
|
|
// Toutes les sessions complètent le track
|
|
analytics := []models.PlaybackAnalytics{
|
|
{PlayTime: 180, CompletionRate: 100.0},
|
|
{PlayTime: 180, CompletionRate: 100.0},
|
|
{PlayTime: 180, CompletionRate: 100.0},
|
|
}
|
|
|
|
retentions := service.calculateSegmentRetention(analytics, 180, 10)
|
|
|
|
// Tous les segments devraient avoir 100% de rétention
|
|
for _, retention := range retentions {
|
|
assert.Equal(t, 100.0, retention.RetentionRate)
|
|
}
|
|
}
|
|
|
|
func TestPlaybackRetentionService_CalculateSegmentRetention_EarlyExits(t *testing.T) {
|
|
_, service := setupTestPlaybackRetentionServiceDB(t)
|
|
|
|
// Toutes les sessions sortent tôt
|
|
analytics := []models.PlaybackAnalytics{
|
|
{PlayTime: 18, CompletionRate: 10.0}, // 10% de 180
|
|
{PlayTime: 18, CompletionRate: 10.0},
|
|
{PlayTime: 18, CompletionRate: 10.0},
|
|
}
|
|
|
|
retentions := service.calculateSegmentRetention(analytics, 180, 10)
|
|
|
|
// Le premier segment devrait avoir 100% de rétention
|
|
assert.Equal(t, 100.0, retentions[0].RetentionRate)
|
|
|
|
// Les segments suivants devraient avoir 0% de rétention
|
|
for i := 2; i < len(retentions); i++ {
|
|
assert.Equal(t, 0.0, retentions[i].RetentionRate)
|
|
}
|
|
}
|
|
|
|
func TestPlaybackRetentionService_IdentifyExitPoints_MultipleExits(t *testing.T) {
|
|
_, service := setupTestPlaybackRetentionServiceDB(t)
|
|
|
|
// Créer des analytics avec plusieurs sorties au même point
|
|
analytics := []models.PlaybackAnalytics{
|
|
{PlayTime: 45, CompletionRate: 25.0}, // 3 sorties à 25%
|
|
{PlayTime: 45, CompletionRate: 25.0},
|
|
{PlayTime: 45, CompletionRate: 25.0},
|
|
{PlayTime: 90, CompletionRate: 50.0}, // 1 sortie à 50%
|
|
{PlayTime: 180, CompletionRate: 100.0}, // 1 complétion
|
|
}
|
|
|
|
exitPoints := service.identifyExitPoints(analytics, 10)
|
|
|
|
assert.NotNil(t, exitPoints)
|
|
// Le point de sortie à 25% devrait être le premier (plus de sorties)
|
|
if len(exitPoints) > 0 {
|
|
assert.GreaterOrEqual(t, exitPoints[0].ExitCount, int64(3))
|
|
}
|
|
}
|
|
|
|
func TestPlaybackRetentionService_AnalyzeEngagement_HighEngagement(t *testing.T) {
|
|
_, service := setupTestPlaybackRetentionServiceDB(t)
|
|
|
|
// Toutes les sessions ont un engagement élevé
|
|
analytics := []models.PlaybackAnalytics{
|
|
{PlayTime: 180, CompletionRate: 100.0, PauseCount: 0, SeekCount: 0},
|
|
{PlayTime: 180, CompletionRate: 100.0, PauseCount: 0, SeekCount: 0},
|
|
{PlayTime: 180, CompletionRate: 100.0, PauseCount: 0, SeekCount: 0},
|
|
}
|
|
|
|
metrics := service.analyzeEngagement(analytics)
|
|
|
|
assert.Equal(t, 100.0, metrics.AverageCompletion)
|
|
assert.Equal(t, 100.0, metrics.OverallRetentionRate)
|
|
assert.Equal(t, 100.0, metrics.HighEngagementRate) // Toutes >= 75%
|
|
assert.Equal(t, 0.0, metrics.LowEngagementRate) // Aucune < 25%
|
|
assert.Equal(t, 0.0, metrics.AveragePauses)
|
|
assert.Equal(t, 0.0, metrics.AverageSeeks)
|
|
assert.Greater(t, metrics.EngagementScore, 90.0) // Score élevé
|
|
}
|
|
|
|
func TestPlaybackRetentionService_AnalyzeEngagement_LowEngagement(t *testing.T) {
|
|
_, service := setupTestPlaybackRetentionServiceDB(t)
|
|
|
|
// Toutes les sessions ont un engagement faible
|
|
analytics := []models.PlaybackAnalytics{
|
|
{PlayTime: 18, CompletionRate: 10.0, PauseCount: 10, SeekCount: 5},
|
|
{PlayTime: 18, CompletionRate: 10.0, PauseCount: 10, SeekCount: 5},
|
|
{PlayTime: 18, CompletionRate: 10.0, PauseCount: 10, SeekCount: 5},
|
|
}
|
|
|
|
metrics := service.analyzeEngagement(analytics)
|
|
|
|
assert.Equal(t, 10.0, metrics.AverageCompletion)
|
|
assert.Equal(t, 0.0, metrics.HighEngagementRate) // Aucune >= 75%
|
|
assert.Equal(t, 100.0, metrics.LowEngagementRate) // Toutes < 25%
|
|
assert.Equal(t, 10.0, metrics.AveragePauses)
|
|
assert.Equal(t, 5.0, metrics.AverageSeeks)
|
|
assert.Less(t, metrics.EngagementScore, 50.0) // Score faible
|
|
}
|