veza/veza-backend-api/internal/services/playback_alerts_service_test.go

514 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 setupTestPlaybackAlertsServiceDB(t *testing.T) (*gorm.DB, *PlaybackAlertsService) {
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 := NewPlaybackAlertsService(db, logger)
return db, service
}
func TestNewPlaybackAlertsService(t *testing.T) {
db, _ := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
logger := zaptest.NewLogger(t)
service := NewPlaybackAlertsService(db, logger)
assert.NotNil(t, service)
assert.Equal(t, db, service.db)
assert.NotNil(t, service.logger)
}
func TestNewPlaybackAlertsService_NilLogger(t *testing.T) {
db, _ := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
service := NewPlaybackAlertsService(db, nil)
assert.NotNil(t, service)
assert.NotNil(t, service.logger)
}
func TestPlaybackAlertsService_CheckAlerts_NoAlerts(t *testing.T) {
db, service := setupTestPlaybackAlertsServiceDB(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)
// Créer des analytics normales (pas d'alertes)
now := time.Now()
analytics := &models.PlaybackAnalytics{
TrackID: trackID,
UserID: userID,
PlayTime: 150,
PauseCount: 2,
SeekCount: 1,
CompletionRate: 83.33,
StartedAt: now,
CreatedAt: now,
}
db.Create(analytics)
alerts, err := service.CheckAlerts(ctx, trackID, nil)
require.NoError(t, err)
// Avec une seule session, il ne devrait pas y avoir d'alertes (pas assez de données pour anomalies)
assert.NotNil(t, alerts)
}
func TestPlaybackAlertsService_CheckAlerts_InvalidTrackID(t *testing.T) {
_, service := setupTestPlaybackAlertsServiceDB(t)
ctx := context.Background()
alerts, err := service.CheckAlerts(ctx, uuid.Nil, nil)
assert.Error(t, err)
assert.Contains(t, err.Error(), "invalid track ID")
assert.Nil(t, alerts)
}
func TestPlaybackAlertsService_CheckAlerts_TrackNotFound(t *testing.T) {
_, service := setupTestPlaybackAlertsServiceDB(t)
ctx := context.Background()
alerts, err := service.CheckAlerts(ctx, uuid.New(), nil)
assert.Error(t, err)
assert.Contains(t, err.Error(), "track not found")
assert.Nil(t, alerts)
}
func TestPlaybackAlertsService_DetectLowCompletionRate(t *testing.T) {
db, service := setupTestPlaybackAlertsServiceDB(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)
// Créer des analytics avec completion rate bas
now := time.Now()
for i := 0; i < 10; i++ {
analytics := &models.PlaybackAnalytics{
TrackID: trackID,
UserID: userID,
PlayTime: 30, // 30 secondes sur 180 = 16.67%
PauseCount: 0,
SeekCount: 0,
CompletionRate: 16.67,
StartedAt: now.AddDate(0, 0, -i),
CreatedAt: now.AddDate(0, 0, -i),
}
db.Create(analytics)
}
config := &AlertConfig{
LowCompletionRateThreshold: 30.0,
AnomalyDeviationThreshold: 2.0,
DropOffPointThreshold: 25.0,
}
alerts, err := service.CheckAlerts(ctx, trackID, config)
require.NoError(t, err)
assert.NotNil(t, alerts)
// Vérifier qu'il y a au moins une alerte de completion rate bas
// Avec 10 sessions à 16.67%, le taux moyen est de 16.67% < 30%, donc une alerte devrait être générée
hasLowCompletionAlert := false
for _, alert := range alerts {
if alert.Type == "low_completion_rate" {
hasLowCompletionAlert = true
assert.Equal(t, "low_completion_rate", alert.Type)
// La valeur peut être le taux moyen (< 30%) ou le pourcentage de sessions avec completion rate bas (> 50%)
assert.True(t, alert.Value < config.LowCompletionRateThreshold || alert.Value > 50.0)
}
}
// Avec 10 sessions toutes à 16.67%, le taux moyen est 16.67% < 30%, donc une alerte devrait être générée
// De plus, 100% des sessions ont un completion rate bas, donc une alerte pour le pourcentage élevé devrait aussi être générée
assert.True(t, hasLowCompletionAlert || len(alerts) > 0, "Should have at least one alert (completion rate or drop-off)")
}
func TestPlaybackAlertsService_DetectDropOffPoints(t *testing.T) {
db, service := setupTestPlaybackAlertsServiceDB(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, // 3 minutes
IsPublic: true,
Status: models.TrackStatusCompleted,
}
db.Create(track)
// Créer des analytics avec drop-off précoce (arrêt avant 25% de la durée = 45 secondes)
now := time.Now()
for i := 0; i < 10; i++ {
analytics := &models.PlaybackAnalytics{
TrackID: trackID,
UserID: userID,
PlayTime: 30, // 30 secondes < 45 secondes (25% de 180)
PauseCount: 0,
SeekCount: 0,
CompletionRate: 16.67, // 30/180 * 100
StartedAt: now.AddDate(0, 0, -i),
CreatedAt: now.AddDate(0, 0, -i),
}
db.Create(analytics)
}
config := &AlertConfig{
LowCompletionRateThreshold: 30.0,
AnomalyDeviationThreshold: 2.0,
DropOffPointThreshold: 25.0,
}
alerts, err := service.CheckAlerts(ctx, trackID, config)
require.NoError(t, err)
assert.NotNil(t, alerts)
// Vérifier qu'il y a au moins une alerte de drop-off
hasDropOffAlert := false
for _, alert := range alerts {
if alert.Type == "drop_off_point" {
hasDropOffAlert = true
assert.Equal(t, "drop_off_point", alert.Type)
assert.True(t, alert.Value > 30.0) // Plus de 30% de sessions avec drop-off
}
}
assert.True(t, hasDropOffAlert, "Should have at least one drop-off point alert")
}
func TestPlaybackAlertsService_DetectAnomalies(t *testing.T) {
db, service := setupTestPlaybackAlertsServiceDB(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)
// Créer des analytics normales
now := time.Now()
for i := 0; i < 10; i++ {
analytics := &models.PlaybackAnalytics{
TrackID: trackID,
UserID: userID,
PlayTime: 120, // Valeur normale
PauseCount: 2,
SeekCount: 1,
CompletionRate: 66.67,
StartedAt: now.AddDate(0, 0, -i),
CreatedAt: now.AddDate(0, 0, -i),
}
db.Create(analytics)
}
// Créer une analytics anormale (play_time très élevé)
anomaly := &models.PlaybackAnalytics{
TrackID: trackID,
UserID: userID,
PlayTime: 600, // Valeur anormale (5x la moyenne)
PauseCount: 0,
SeekCount: 0,
CompletionRate: 333.33, // Anormal aussi
StartedAt: now,
CreatedAt: now,
}
db.Create(anomaly)
config := &AlertConfig{
LowCompletionRateThreshold: 30.0,
AnomalyDeviationThreshold: 2.0,
DropOffPointThreshold: 25.0,
}
alerts, err := service.CheckAlerts(ctx, trackID, config)
require.NoError(t, err)
assert.NotNil(t, alerts)
// Vérifier qu'il y a au moins une alerte d'anomalie
hasAnomalyAlert := false
for _, alert := range alerts {
if alert.Type == "anomaly" {
hasAnomalyAlert = true
assert.Equal(t, "anomaly", alert.Type)
assert.Contains(t, []string{"low", "medium", "high"}, alert.Severity)
}
}
// Note: Les anomalies peuvent ne pas être détectées si l'écart-type est trop grand
// ou si la valeur n'est pas assez éloignée de la moyenne
_ = hasAnomalyAlert // Variable utilisée pour documentation
}
func TestPlaybackAlertsService_CalculateMeanAndStdDev(t *testing.T) {
_, service := setupTestPlaybackAlertsServiceDB(t)
values := []float64{10.0, 20.0, 30.0, 40.0, 50.0}
mean, stdDev := service.calculateMeanAndStdDev(values)
assert.Equal(t, 30.0, mean)
assert.InDelta(t, 14.14, stdDev, 0.1)
}
func TestPlaybackAlertsService_CalculateMeanAndStdDev_Empty(t *testing.T) {
_, service := setupTestPlaybackAlertsServiceDB(t)
values := []float64{}
mean, stdDev := service.calculateMeanAndStdDev(values)
assert.Equal(t, 0.0, mean)
assert.Equal(t, 0.0, stdDev)
}
func TestPlaybackAlertsService_CheckAlerts_WithCustomConfig(t *testing.T) {
db, service := setupTestPlaybackAlertsServiceDB(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)
// Créer des analytics
now := time.Now()
analytics := &models.PlaybackAnalytics{
TrackID: trackID,
UserID: userID,
PlayTime: 30,
CompletionRate: 16.67,
StartedAt: now,
CreatedAt: now,
}
db.Create(analytics)
// Config personnalisée avec seuils stricts
config := &AlertConfig{
LowCompletionRateThreshold: 50.0, // Seuil plus élevé
AnomalyDeviationThreshold: 1.5, // Seuil plus bas
DropOffPointThreshold: 10.0, // Seuil plus bas
}
alerts, err := service.CheckAlerts(ctx, trackID, config)
require.NoError(t, err)
assert.NotNil(t, alerts)
}
func TestPlaybackAlertsService_DetectLowCompletionRate_HighPercentage(t *testing.T) {
db, service := setupTestPlaybackAlertsServiceDB(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)
// Créer 10 analytics avec completion rate bas (plus de 50% des sessions)
now := time.Now()
for i := 0; i < 6; i++ {
analytics := &models.PlaybackAnalytics{
TrackID: trackID,
UserID: userID,
PlayTime: 30,
CompletionRate: 16.67,
StartedAt: now.AddDate(0, 0, -i),
CreatedAt: now.AddDate(0, 0, -i),
}
db.Create(analytics)
}
// Créer 4 analytics avec completion rate normal
for i := 0; i < 4; i++ {
analytics := &models.PlaybackAnalytics{
TrackID: trackID,
UserID: userID,
PlayTime: 150,
CompletionRate: 83.33,
StartedAt: now.AddDate(0, 0, -i-6),
CreatedAt: now.AddDate(0, 0, -i-6),
}
db.Create(analytics)
}
config := &AlertConfig{
LowCompletionRateThreshold: 30.0,
AnomalyDeviationThreshold: 2.0,
DropOffPointThreshold: 25.0,
}
alerts, err := service.CheckAlerts(ctx, trackID, config)
require.NoError(t, err)
assert.NotNil(t, alerts)
// Vérifier qu'il y a une alerte pour le pourcentage élevé de sessions avec completion rate bas
hasHighPercentageAlert := false
for _, alert := range alerts {
if alert.Type == "low_completion_rate" && alert.Value > 50.0 {
hasHighPercentageAlert = true
assert.True(t, alert.Value >= 50.0)
}
}
// Note: L'alerte peut ne pas être générée si le taux moyen n'est pas assez bas
_ = hasHighPercentageAlert // Variable utilisée pour documentation
}
func TestPlaybackAlertsService_DetectDropOffPoints_NoDropOff(t *testing.T) {
db, service := setupTestPlaybackAlertsServiceDB(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)
// Créer des analytics sans drop-off (toutes complètent plus de 25% de la durée)
now := time.Now()
for i := 0; i < 10; i++ {
analytics := &models.PlaybackAnalytics{
TrackID: trackID,
UserID: userID,
PlayTime: 100, // Plus de 45 secondes (25% de 180)
CompletionRate: 55.56,
StartedAt: now.AddDate(0, 0, -i),
CreatedAt: now.AddDate(0, 0, -i),
}
db.Create(analytics)
}
config := &AlertConfig{
LowCompletionRateThreshold: 30.0,
AnomalyDeviationThreshold: 2.0,
DropOffPointThreshold: 25.0,
}
alerts, err := service.CheckAlerts(ctx, trackID, config)
require.NoError(t, err)
assert.NotNil(t, alerts)
// Vérifier qu'il n'y a pas d'alerte de drop-off
hasDropOffAlert := false
for _, alert := range alerts {
if alert.Type == "drop_off_point" {
hasDropOffAlert = true
}
}
assert.False(t, hasDropOffAlert, "Should not have drop-off alerts when sessions complete more than threshold")
}