veza/veza-backend-api/internal/services/playback_abtest_service_test.go
2025-12-03 20:29:37 +01:00

578 lines
17 KiB
Go

package services
import (
"context"
"github.com/google/uuid"
"math"
"testing"
"time"
"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 setupTestPlaybackABTestServiceDB(t *testing.T) (*gorm.DB, *PlaybackABTestService) {
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 := NewPlaybackABTestService(db, logger)
return db, service
}
func TestNewPlaybackABTestService(t *testing.T) {
db, _ := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
logger := zaptest.NewLogger(t)
service := NewPlaybackABTestService(db, logger)
assert.NotNil(t, service)
assert.Equal(t, db, service.db)
assert.NotNil(t, service.logger)
}
func TestNewPlaybackABTestService_NilLogger(t *testing.T) {
db, _ := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
service := NewPlaybackABTestService(db, nil)
assert.NotNil(t, service)
assert.NotNil(t, service.logger)
}
func TestPlaybackABTestService_CompareVariants_EmptyVariantNames(t *testing.T) {
_, service := setupTestPlaybackABTestServiceDB(t)
ctx := context.Background()
filterA := VariantFilter{}
filterB := VariantFilter{}
result, err := service.CompareVariants(ctx, "", "B", filterA, filterB)
assert.Error(t, err)
assert.Contains(t, err.Error(), "variant names cannot be empty")
assert.Nil(t, result)
result, err = service.CompareVariants(ctx, "A", "", filterA, filterB)
assert.Error(t, err)
assert.Contains(t, err.Error(), "variant names cannot be empty")
assert.Nil(t, result)
}
func TestPlaybackABTestService_CompareVariants_NoData(t *testing.T) {
db, service := setupTestPlaybackABTestServiceDB(t)
ctx := context.Background()
// Créer user et track
userID := uuid.New()
trackID := uuid.New()
user := &models.User{ID: userID, Username: "testuser", Slug: "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)
filterA := VariantFilter{TrackID: &trackID}
filterB := VariantFilter{TrackID: &trackID}
result, err := service.CompareVariants(ctx, "A", "B", filterA, filterB)
require.NoError(t, err)
assert.NotNil(t, result)
assert.Equal(t, "A", result.VariantA.VariantName)
assert.Equal(t, "B", result.VariantB.VariantName)
assert.Equal(t, int64(0), result.VariantA.TotalSessions)
assert.Equal(t, int64(0), result.VariantB.TotalSessions)
assert.NotNil(t, result.Significance)
}
func TestPlaybackABTestService_CompareVariants_WithData(t *testing.T) {
db, service := setupTestPlaybackABTestServiceDB(t)
ctx := context.Background()
// Créer users et track
user1ID := uuid.New()
user2ID := uuid.New()
trackID := uuid.New()
user1 := &models.User{ID: user1ID, Username: "user1", Slug: "user1", Email: "user1@example.com", IsActive: true}
user2 := &models.User{ID: user2ID, Username: "user2", Slug: "user2", Email: "user2@example.com", IsActive: true}
db.Create(user1)
db.Create(user2)
track := &models.Track{
ID: trackID,
UserID: user1ID,
Title: "Test Track",
FilePath: "/test.mp3",
FileSize: 1024,
Format: "MP3",
Duration: 180,
IsPublic: true,
Status: models.TrackStatusCompleted,
}
db.Create(track)
now := time.Now()
// Variant A: High completion
for i := 0; i < 10; i++ {
db.Create(&models.PlaybackAnalytics{
TrackID: trackID,
UserID: user1ID,
PlayTime: 180,
PauseCount: 0,
SeekCount: 0,
CompletionRate: 100.0,
StartedAt: now,
CreatedAt: now,
})
}
// Variant B: Lower completion
for i := 0; i < 10; i++ {
db.Create(&models.PlaybackAnalytics{
TrackID: trackID,
UserID: user2ID,
PlayTime: 90,
PauseCount: 2,
SeekCount: 1,
CompletionRate: 50.0,
StartedAt: now,
CreatedAt: now,
})
}
filterA := VariantFilter{TrackID: &trackID, UserIDs: []uuid.UUID{user1ID}}
filterB := VariantFilter{TrackID: &trackID, UserIDs: []uuid.UUID{user2ID}}
result, err := service.CompareVariants(ctx, "A", "B", filterA, filterB)
require.NoError(t, err)
assert.NotNil(t, result)
assert.Equal(t, "A", result.VariantA.VariantName)
assert.Equal(t, "B", result.VariantB.VariantName)
assert.Equal(t, int64(10), result.VariantA.TotalSessions)
assert.Equal(t, int64(10), result.VariantB.TotalSessions)
assert.Equal(t, 100.0, result.VariantA.AverageCompletion)
assert.Equal(t, 50.0, result.VariantB.AverageCompletion)
assert.NotNil(t, result.Significance)
assert.NotNil(t, result.Difference)
assert.NotNil(t, result.PercentageChange)
}
func TestPlaybackABTestService_CalculateVariantStats(t *testing.T) {
_, service := setupTestPlaybackABTestServiceDB(t)
analytics := []models.PlaybackAnalytics{
{PlayTime: 180, PauseCount: 0, SeekCount: 0, CompletionRate: 100.0},
{PlayTime: 180, PauseCount: 1, SeekCount: 0, CompletionRate: 95.0},
{PlayTime: 90, PauseCount: 2, SeekCount: 1, CompletionRate: 50.0},
}
stats := service.calculateVariantStats("TestVariant", analytics)
assert.NotNil(t, stats)
assert.Equal(t, "TestVariant", stats.VariantName)
assert.Equal(t, int64(3), stats.TotalSessions)
assert.InDelta(t, 150.0, stats.AveragePlayTime, 0.1) // (180 + 180 + 90) / 3
assert.InDelta(t, 81.67, stats.AverageCompletion, 0.1) // (100 + 95 + 50) / 3
assert.Equal(t, 1.0, stats.AveragePauses) // (0 + 1 + 2) / 3
assert.InDelta(t, 0.33, stats.AverageSeeks, 0.1) // (0 + 0 + 1) / 3
}
func TestPlaybackABTestService_CalculateVariantStats_Empty(t *testing.T) {
_, service := setupTestPlaybackABTestServiceDB(t)
analytics := []models.PlaybackAnalytics{}
stats := service.calculateVariantStats("EmptyVariant", analytics)
assert.NotNil(t, stats)
assert.Equal(t, "EmptyVariant", stats.VariantName)
assert.Equal(t, int64(0), stats.TotalSessions)
}
func TestPlaybackABTestService_CalculateStatisticalSignificance(t *testing.T) {
_, service := setupTestPlaybackABTestServiceDB(t)
// Variant A: High completion (tous à 100%)
analyticsA := []models.PlaybackAnalytics{
{CompletionRate: 100.0},
{CompletionRate: 100.0},
{CompletionRate: 100.0},
{CompletionRate: 100.0},
{CompletionRate: 100.0},
}
// Variant B: Lower completion (tous à 50%)
analyticsB := []models.PlaybackAnalytics{
{CompletionRate: 50.0},
{CompletionRate: 50.0},
{CompletionRate: 50.0},
{CompletionRate: 50.0},
{CompletionRate: 50.0},
}
significance := service.calculateStatisticalSignificance(analyticsA, analyticsB)
assert.NotNil(t, significance)
assert.GreaterOrEqual(t, significance.PValue, 0.0)
assert.LessOrEqual(t, significance.PValue, 1.0)
assert.Greater(t, significance.ConfidenceLevel, 0.0)
// EffectSize peut être 0 si les écarts-types sont 0 (toutes les valeurs identiques)
// Dans ce cas, on vérifie juste qu'il n'est pas NaN
assert.False(t, math.IsNaN(significance.EffectSize))
assert.False(t, math.IsInf(significance.EffectSize, 0))
}
func TestPlaybackABTestService_CalculateStatisticalSignificance_Empty(t *testing.T) {
_, service := setupTestPlaybackABTestServiceDB(t)
analyticsA := []models.PlaybackAnalytics{}
analyticsB := []models.PlaybackAnalytics{}
significance := service.calculateStatisticalSignificance(analyticsA, analyticsB)
assert.NotNil(t, significance)
assert.Equal(t, 1.0, significance.PValue)
assert.False(t, significance.IsSignificant)
}
func TestPlaybackABTestService_CalculateMeanAndStdDev(t *testing.T) {
_, service := setupTestPlaybackABTestServiceDB(t)
data := []float64{10.0, 20.0, 30.0, 40.0, 50.0}
mean, stdDev := service.calculateMeanAndStdDev(data)
assert.Equal(t, 30.0, mean)
assert.Greater(t, stdDev, 0.0)
}
func TestPlaybackABTestService_CalculateMeanAndStdDev_Empty(t *testing.T) {
_, service := setupTestPlaybackABTestServiceDB(t)
data := []float64{}
mean, stdDev := service.calculateMeanAndStdDev(data)
assert.Equal(t, 0.0, mean)
assert.Equal(t, 0.0, stdDev)
}
func TestPlaybackABTestService_DetermineWinner(t *testing.T) {
_, service := setupTestPlaybackABTestServiceDB(t)
statsA := &VariantStats{CompletionRate: 80.0}
statsB := &VariantStats{CompletionRate: 90.0}
significance := &StatisticalSignificance{IsSignificant: true, PValue: 0.01}
winner := service.determineWinner(statsA, statsB, significance)
assert.Equal(t, "B", winner)
// Test avec A gagnant
statsA2 := &VariantStats{CompletionRate: 90.0}
statsB2 := &VariantStats{CompletionRate: 80.0}
winner2 := service.determineWinner(statsA2, statsB2, significance)
assert.Equal(t, "A", winner2)
// Test non significatif
significance2 := &StatisticalSignificance{IsSignificant: false, PValue: 0.1}
winner3 := service.determineWinner(statsA, statsB, significance2)
assert.Equal(t, "inconclusive", winner3)
}
func TestPlaybackABTestService_GenerateRecommendation(t *testing.T) {
_, service := setupTestPlaybackABTestServiceDB(t)
statsA := &VariantStats{CompletionRate: 80.0}
statsB := &VariantStats{CompletionRate: 90.0}
significance := &StatisticalSignificance{IsSignificant: true, PValue: 0.01}
recommendation := service.generateRecommendation(statsA, statsB, significance)
assert.NotEmpty(t, recommendation)
assert.Contains(t, recommendation, "variant B")
assert.Contains(t, recommendation, "significativement meilleur")
// Test avec A gagnant
statsA2 := &VariantStats{CompletionRate: 90.0}
statsB2 := &VariantStats{CompletionRate: 80.0}
recommendation2 := service.generateRecommendation(statsA2, statsB2, significance)
assert.Contains(t, recommendation2, "variant A")
// Test non significatif
significance2 := &StatisticalSignificance{IsSignificant: false, PValue: 0.1}
recommendation3 := service.generateRecommendation(statsA, statsB, significance2)
assert.Contains(t, recommendation3, "pas statistiquement significatifs")
}
func TestPlaybackABTestService_GetAnalyticsForVariant(t *testing.T) {
db, service := setupTestPlaybackABTestServiceDB(t)
ctx := context.Background()
// Créer user et track
userID := uuid.New()
trackID := uuid.New()
user := &models.User{ID: userID, Username: "testuser", Slug: "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)
now := time.Now()
analytics := &models.PlaybackAnalytics{
TrackID: trackID,
UserID: userID,
PlayTime: 180,
PauseCount: 0,
SeekCount: 0,
CompletionRate: 100.0,
StartedAt: now,
CreatedAt: now,
}
db.Create(analytics)
filter := VariantFilter{TrackID: &trackID}
result, err := service.getAnalyticsForVariant(ctx, filter)
require.NoError(t, err)
assert.NotNil(t, result)
assert.Equal(t, 1, len(result))
}
func TestPlaybackABTestService_GetAnalyticsForVariant_WithDateFilter(t *testing.T) {
db, service := setupTestPlaybackABTestServiceDB(t)
ctx := context.Background()
// Créer user et track
userID := uuid.New()
trackID := uuid.New()
user := &models.User{ID: userID, Username: "testuser", Slug: "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)
now := time.Now()
yesterday := now.AddDate(0, 0, -1)
tomorrow := now.AddDate(0, 0, 1)
// Analytics créé aujourd'hui
analytics := &models.PlaybackAnalytics{
TrackID: trackID,
UserID: userID,
PlayTime: 180,
PauseCount: 0,
SeekCount: 0,
CompletionRate: 100.0,
StartedAt: now,
CreatedAt: now,
}
db.Create(analytics)
// Filtrer par date (hier à demain) - devrait inclure l'analytics
filter := VariantFilter{
TrackID: &trackID,
StartDate: &yesterday,
EndDate: &tomorrow,
}
result, err := service.getAnalyticsForVariant(ctx, filter)
require.NoError(t, err)
assert.Equal(t, 1, len(result))
// Filtrer par date (avant-hier à hier) - ne devrait pas inclure l'analytics
dayBeforeYesterday := now.AddDate(0, 0, -2)
filter2 := VariantFilter{
TrackID: &trackID,
StartDate: &dayBeforeYesterday,
EndDate: &yesterday,
}
result2, err := service.getAnalyticsForVariant(ctx, filter2)
require.NoError(t, err)
assert.Equal(t, 0, len(result2))
}
func TestPlaybackABTestService_GetAnalyticsForVariant_WithUserFilter(t *testing.T) {
db, service := setupTestPlaybackABTestServiceDB(t)
ctx := context.Background()
// Créer users et track
user1ID := uuid.New()
user2ID := uuid.New()
trackID := uuid.New()
user1 := &models.User{ID: user1ID, Username: "user1", Slug: "user1", Email: "user1@example.com", IsActive: true}
user2 := &models.User{ID: user2ID, Username: "user2", Slug: "user2", Email: "user2@example.com", IsActive: true}
db.Create(user1)
db.Create(user2)
track := &models.Track{
ID: trackID,
UserID: user1ID,
Title: "Test Track",
FilePath: "/test.mp3",
FileSize: 1024,
Format: "MP3",
Duration: 180,
IsPublic: true,
Status: models.TrackStatusCompleted,
}
db.Create(track)
now := time.Now()
analytics1 := &models.PlaybackAnalytics{
TrackID: trackID,
UserID: user1ID,
PlayTime: 180,
CompletionRate: 100.0,
StartedAt: now,
CreatedAt: now,
}
analytics2 := &models.PlaybackAnalytics{
TrackID: trackID,
UserID: user2ID,
PlayTime: 90,
CompletionRate: 50.0,
StartedAt: now,
CreatedAt: now,
}
db.Create(analytics1)
db.Create(analytics2)
// Filtrer par user 1 seulement
filter := VariantFilter{
TrackID: &trackID,
UserIDs: []uuid.UUID{user1ID},
}
result, err := service.getAnalyticsForVariant(ctx, filter)
require.NoError(t, err)
assert.Equal(t, 1, len(result))
assert.Equal(t, user1ID, result[0].UserID)
}
func TestPlaybackABTestService_SafePercentageChange(t *testing.T) {
_, service := setupTestPlaybackABTestServiceDB(t)
// Test normal
result := service.safePercentageChange(100.0, 120.0)
assert.Equal(t, 20.0, result)
// Test avec base zéro et courant non-zéro
result2 := service.safePercentageChange(0.0, 100.0)
assert.True(t, math.IsInf(result2, 1))
// Test avec base zéro et courant zéro
result3 := service.safePercentageChange(0.0, 0.0)
assert.Equal(t, 0.0, result3)
// Test négatif
result4 := service.safePercentageChange(100.0, 80.0)
assert.Equal(t, -20.0, result4)
}
func TestPlaybackABTestService_CompareVariants_WithDateRange(t *testing.T) {
db, service := setupTestPlaybackABTestServiceDB(t)
ctx := context.Background()
// Créer user et track
userID := uuid.New()
trackID := uuid.New()
user := &models.User{ID: userID, Username: "testuser", Slug: "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)
now := time.Now()
weekAgo := now.AddDate(0, 0, -7)
threeDaysAgo := now.AddDate(0, 0, -3)
// Analytics pour variant A (il y a une semaine)
analyticsA := &models.PlaybackAnalytics{
TrackID: trackID,
UserID: userID,
PlayTime: 180,
CompletionRate: 100.0,
StartedAt: weekAgo,
CreatedAt: weekAgo,
}
db.Create(analyticsA)
// Analytics pour variant B (il y a 3 jours)
analyticsB := &models.PlaybackAnalytics{
TrackID: trackID,
UserID: userID,
PlayTime: 90,
CompletionRate: 50.0,
StartedAt: threeDaysAgo,
CreatedAt: threeDaysAgo,
}
db.Create(analyticsB)
// Filtrer variant A par période (il y a 8 jours à 6 jours)
eightDaysAgo := now.AddDate(0, 0, -8)
sixDaysAgo := now.AddDate(0, 0, -6)
filterA := VariantFilter{
TrackID: &trackID,
StartDate: &eightDaysAgo,
EndDate: &sixDaysAgo,
}
// Filtrer variant B par période (il y a 4 jours à 2 jours)
fourDaysAgo := now.AddDate(0, 0, -4)
twoDaysAgo := now.AddDate(0, 0, -2)
filterB := VariantFilter{
TrackID: &trackID,
StartDate: &fourDaysAgo,
EndDate: &twoDaysAgo,
}
result, err := service.CompareVariants(ctx, "A", "B", filterA, filterB)
require.NoError(t, err)
assert.NotNil(t, result)
assert.Equal(t, int64(1), result.VariantA.TotalSessions)
assert.Equal(t, int64(1), result.VariantB.TotalSessions)
}