2025-12-03 19:29:37 +00:00
|
|
|
package services
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
fix: stabilize builds, tests, and lint across all stacks
Complete stabilization pass bringing all 3 stacks to green:
Frontend (apps/web/):
- Fix TypeScript nullability in useSeason.ts, useTimeOfDay.ts hooks
- Disable no-undef in ESLint config (TypeScript handles it; JSX misidentified)
- Rename 306 story imports from @storybook/react to @storybook/react-vite
- Fix conditional hook call in useMediaQuery.ts useIsTablet
- Move useQuery to top of LoginPage.tsx component
- Remove useless try/catch in GearFormModal.tsx
- Fix stale closure in ResetPasswordPage.tsx handleChange
- Make Storybook decorators (withRouter, withQueryClient, withToast, withAudio)
no-ops since global StorybookDecorator already provides these — prevents
nested Router / duplicate provider crashes in vitest-browser
- Fix nested MemoryRouter in 3 page stories (TrackDetail, PlaylistDetail, UserProfile)
- Update i18n initialization in test setup (await init before changeLanguage)
- Update ~30 test assertions from English to French to match i18n translations
- Update test assertions to match SUMI V3 design changes (shadow vs border)
- Fix remaining story type errors (PlayerError, PlaylistBatchActions,
TrackFilters, VirtualizedChatMessages)
Backend (veza-backend-api/):
- Fix response_test.go RespondWithAppError signature (2 args, not 3)
- Fix TestErrorContractAuthEndpoints expected error codes
(ErrCodeUnauthorized vs ErrCodeInvalidCredentials)
- Fix TestTrackHandler_GetTrackLikes_Success missing auth middleware setup
- Fix TestPlaybackAnalyticsService_GetTrackStats k-anonymity threshold
(needs 5 unique users, not 1)
- Replace NOW() PostgreSQL function with time.Now() parameter in marketplace
service for SQLite test compatibility
- Add missing AutoMigrate entries in marketplace_test.go
(ProductImage, ProductPreview, ProductLicense, ProductReview)
Results:
- Frontend TypeCheck: 617 errors -> 0 errors
- Frontend ESLint: 349 errors -> 0 errors
- Frontend Vitest: 196 failing tests -> 1 skipped (3396/3397 passing)
- Backend go vet: 1 error -> 0 errors
- Backend tests: 5 failing -> all 13 packages passing
- Rust: 150/150 tests passing (unchanged)
- Storybook audit: 0 errors across 1244 stories
Triage report: docs/TRIAGE_REPORT.md
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 14:48:07 +00:00
|
|
|
"fmt"
|
2025-12-03 19:29:37 +00:00
|
|
|
"testing"
|
|
|
|
|
"time"
|
|
|
|
|
|
2025-12-16 16:23:49 +00:00
|
|
|
"github.com/google/uuid"
|
|
|
|
|
|
2025-12-03 19:29:37 +00:00
|
|
|
"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
|
2025-12-06 16:21:59 +00:00
|
|
|
userID := uuid.New()
|
2025-12-03 19:29:37 +00:00
|
|
|
user := &models.User{
|
2025-12-06 16:21:59 +00:00
|
|
|
ID: userID,
|
2025-12-03 19:29:37 +00:00
|
|
|
Username: "testuser",
|
|
|
|
|
Email: "test@example.com",
|
|
|
|
|
IsActive: true,
|
|
|
|
|
}
|
|
|
|
|
db.Create(user)
|
|
|
|
|
|
2025-12-06 16:21:59 +00:00
|
|
|
trackID := uuid.New()
|
2025-12-03 19:29:37 +00:00
|
|
|
track := &models.Track{
|
2025-12-06 16:21:59 +00:00
|
|
|
ID: trackID,
|
|
|
|
|
UserID: userID,
|
2025-12-03 19:29:37 +00:00
|
|
|
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{
|
2025-12-06 16:21:59 +00:00
|
|
|
TrackID: trackID,
|
|
|
|
|
UserID: userID,
|
2025-12-03 19:29:37 +00:00
|
|
|
PlayTime: 120,
|
|
|
|
|
PauseCount: 3,
|
|
|
|
|
SeekCount: 5,
|
|
|
|
|
StartedAt: now,
|
|
|
|
|
EndedAt: &now,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
err := service.RecordPlayback(ctx, analytics)
|
|
|
|
|
assert.NoError(t, err)
|
|
|
|
|
assert.NotZero(t, analytics.ID)
|
2025-12-16 16:23:49 +00:00
|
|
|
// Use InDelta for floating point comparison (120/180 * 100 = 66.66666666666666)
|
|
|
|
|
assert.InDelta(t, 66.67, analytics.CompletionRate, 0.01) // 120/180 * 100
|
2025-12-03 19:29:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestPlaybackAnalyticsService_RecordPlayback_InvalidTrackID(t *testing.T) {
|
|
|
|
|
_, service := setupTestPlaybackAnalyticsServiceDB(t)
|
|
|
|
|
ctx := context.Background()
|
|
|
|
|
|
|
|
|
|
analytics := &models.PlaybackAnalytics{
|
|
|
|
|
TrackID: uuid.Nil,
|
2025-12-06 16:21:59 +00:00
|
|
|
UserID: uuid.New(),
|
2025-12-03 19:29:37 +00:00
|
|
|
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{
|
2025-12-06 16:21:59 +00:00
|
|
|
TrackID: uuid.New(),
|
2025-12-03 19:29:37 +00:00
|
|
|
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{
|
2025-12-06 16:21:59 +00:00
|
|
|
TrackID: uuid.New(),
|
|
|
|
|
UserID: uuid.New(),
|
2025-12-03 19:29:37 +00:00
|
|
|
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()
|
|
|
|
|
|
2025-12-06 16:21:59 +00:00
|
|
|
userID := uuid.New()
|
|
|
|
|
user := &models.User{ID: userID, Username: "testuser", Email: "test@example.com", IsActive: true}
|
2025-12-03 19:29:37 +00:00
|
|
|
db.Create(user)
|
|
|
|
|
|
2025-12-06 16:21:59 +00:00
|
|
|
trackID := uuid.New()
|
2025-12-03 19:29:37 +00:00
|
|
|
track := &models.Track{
|
2025-12-06 16:21:59 +00:00
|
|
|
ID: trackID,
|
|
|
|
|
UserID: userID,
|
2025-12-03 19:29:37 +00:00
|
|
|
Title: "Test Track",
|
|
|
|
|
FilePath: "/test.mp3",
|
|
|
|
|
FileSize: 1024,
|
|
|
|
|
Format: "MP3",
|
|
|
|
|
Duration: 180,
|
|
|
|
|
IsPublic: true,
|
|
|
|
|
Status: models.TrackStatusCompleted,
|
|
|
|
|
}
|
|
|
|
|
db.Create(track)
|
|
|
|
|
|
|
|
|
|
analytics := &models.PlaybackAnalytics{
|
2025-12-06 16:21:59 +00:00
|
|
|
TrackID: trackID,
|
|
|
|
|
UserID: userID,
|
2025-12-03 19:29:37 +00:00
|
|
|
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()
|
|
|
|
|
|
2025-12-06 16:21:59 +00:00
|
|
|
userID := uuid.New()
|
|
|
|
|
user := &models.User{ID: userID, Username: "testuser", Email: "test@example.com", IsActive: true}
|
2025-12-03 19:29:37 +00:00
|
|
|
db.Create(user)
|
|
|
|
|
|
2025-12-06 16:21:59 +00:00
|
|
|
trackID := uuid.New()
|
2025-12-03 19:29:37 +00:00
|
|
|
track := &models.Track{
|
2025-12-06 16:21:59 +00:00
|
|
|
ID: trackID,
|
|
|
|
|
UserID: userID,
|
2025-12-03 19:29:37 +00:00
|
|
|
Title: "Test Track",
|
|
|
|
|
FilePath: "/test.mp3",
|
|
|
|
|
FileSize: 1024,
|
|
|
|
|
Format: "MP3",
|
|
|
|
|
Duration: 180,
|
|
|
|
|
IsPublic: true,
|
|
|
|
|
Status: models.TrackStatusCompleted,
|
|
|
|
|
}
|
|
|
|
|
db.Create(track)
|
|
|
|
|
|
|
|
|
|
analytics := &models.PlaybackAnalytics{
|
2025-12-06 16:21:59 +00:00
|
|
|
TrackID: trackID,
|
|
|
|
|
UserID: userID,
|
2025-12-03 19:29:37 +00:00
|
|
|
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()
|
|
|
|
|
|
fix: stabilize builds, tests, and lint across all stacks
Complete stabilization pass bringing all 3 stacks to green:
Frontend (apps/web/):
- Fix TypeScript nullability in useSeason.ts, useTimeOfDay.ts hooks
- Disable no-undef in ESLint config (TypeScript handles it; JSX misidentified)
- Rename 306 story imports from @storybook/react to @storybook/react-vite
- Fix conditional hook call in useMediaQuery.ts useIsTablet
- Move useQuery to top of LoginPage.tsx component
- Remove useless try/catch in GearFormModal.tsx
- Fix stale closure in ResetPasswordPage.tsx handleChange
- Make Storybook decorators (withRouter, withQueryClient, withToast, withAudio)
no-ops since global StorybookDecorator already provides these — prevents
nested Router / duplicate provider crashes in vitest-browser
- Fix nested MemoryRouter in 3 page stories (TrackDetail, PlaylistDetail, UserProfile)
- Update i18n initialization in test setup (await init before changeLanguage)
- Update ~30 test assertions from English to French to match i18n translations
- Update test assertions to match SUMI V3 design changes (shadow vs border)
- Fix remaining story type errors (PlayerError, PlaylistBatchActions,
TrackFilters, VirtualizedChatMessages)
Backend (veza-backend-api/):
- Fix response_test.go RespondWithAppError signature (2 args, not 3)
- Fix TestErrorContractAuthEndpoints expected error codes
(ErrCodeUnauthorized vs ErrCodeInvalidCredentials)
- Fix TestTrackHandler_GetTrackLikes_Success missing auth middleware setup
- Fix TestPlaybackAnalyticsService_GetTrackStats k-anonymity threshold
(needs 5 unique users, not 1)
- Replace NOW() PostgreSQL function with time.Now() parameter in marketplace
service for SQLite test compatibility
- Add missing AutoMigrate entries in marketplace_test.go
(ProductImage, ProductPreview, ProductLicense, ProductReview)
Results:
- Frontend TypeCheck: 617 errors -> 0 errors
- Frontend ESLint: 349 errors -> 0 errors
- Frontend Vitest: 196 failing tests -> 1 skipped (3396/3397 passing)
- Backend go vet: 1 error -> 0 errors
- Backend tests: 5 failing -> all 13 packages passing
- Rust: 150/150 tests passing (unchanged)
- Storybook audit: 0 errors across 1244 stories
Triage report: docs/TRIAGE_REPORT.md
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 14:48:07 +00:00
|
|
|
// 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)
|
|
|
|
|
}
|
2025-12-03 19:29:37 +00:00
|
|
|
|
2025-12-06 16:21:59 +00:00
|
|
|
trackID := uuid.New()
|
2025-12-03 19:29:37 +00:00
|
|
|
track := &models.Track{
|
2025-12-06 16:21:59 +00:00
|
|
|
ID: trackID,
|
fix: stabilize builds, tests, and lint across all stacks
Complete stabilization pass bringing all 3 stacks to green:
Frontend (apps/web/):
- Fix TypeScript nullability in useSeason.ts, useTimeOfDay.ts hooks
- Disable no-undef in ESLint config (TypeScript handles it; JSX misidentified)
- Rename 306 story imports from @storybook/react to @storybook/react-vite
- Fix conditional hook call in useMediaQuery.ts useIsTablet
- Move useQuery to top of LoginPage.tsx component
- Remove useless try/catch in GearFormModal.tsx
- Fix stale closure in ResetPasswordPage.tsx handleChange
- Make Storybook decorators (withRouter, withQueryClient, withToast, withAudio)
no-ops since global StorybookDecorator already provides these — prevents
nested Router / duplicate provider crashes in vitest-browser
- Fix nested MemoryRouter in 3 page stories (TrackDetail, PlaylistDetail, UserProfile)
- Update i18n initialization in test setup (await init before changeLanguage)
- Update ~30 test assertions from English to French to match i18n translations
- Update test assertions to match SUMI V3 design changes (shadow vs border)
- Fix remaining story type errors (PlayerError, PlaylistBatchActions,
TrackFilters, VirtualizedChatMessages)
Backend (veza-backend-api/):
- Fix response_test.go RespondWithAppError signature (2 args, not 3)
- Fix TestErrorContractAuthEndpoints expected error codes
(ErrCodeUnauthorized vs ErrCodeInvalidCredentials)
- Fix TestTrackHandler_GetTrackLikes_Success missing auth middleware setup
- Fix TestPlaybackAnalyticsService_GetTrackStats k-anonymity threshold
(needs 5 unique users, not 1)
- Replace NOW() PostgreSQL function with time.Now() parameter in marketplace
service for SQLite test compatibility
- Add missing AutoMigrate entries in marketplace_test.go
(ProductImage, ProductPreview, ProductLicense, ProductReview)
Results:
- Frontend TypeCheck: 617 errors -> 0 errors
- Frontend ESLint: 349 errors -> 0 errors
- Frontend Vitest: 196 failing tests -> 1 skipped (3396/3397 passing)
- Backend go vet: 1 error -> 0 errors
- Backend tests: 5 failing -> all 13 packages passing
- Rust: 150/150 tests passing (unchanged)
- Storybook audit: 0 errors across 1244 stories
Triage report: docs/TRIAGE_REPORT.md
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 14:48:07 +00:00
|
|
|
UserID: userIDs[0],
|
2025-12-03 19:29:37 +00:00
|
|
|
Title: "Test Track",
|
|
|
|
|
FilePath: "/test.mp3",
|
|
|
|
|
FileSize: 1024,
|
|
|
|
|
Format: "MP3",
|
|
|
|
|
Duration: 180,
|
|
|
|
|
IsPublic: true,
|
|
|
|
|
Status: models.TrackStatusCompleted,
|
|
|
|
|
}
|
|
|
|
|
db.Create(track)
|
|
|
|
|
|
fix: stabilize builds, tests, and lint across all stacks
Complete stabilization pass bringing all 3 stacks to green:
Frontend (apps/web/):
- Fix TypeScript nullability in useSeason.ts, useTimeOfDay.ts hooks
- Disable no-undef in ESLint config (TypeScript handles it; JSX misidentified)
- Rename 306 story imports from @storybook/react to @storybook/react-vite
- Fix conditional hook call in useMediaQuery.ts useIsTablet
- Move useQuery to top of LoginPage.tsx component
- Remove useless try/catch in GearFormModal.tsx
- Fix stale closure in ResetPasswordPage.tsx handleChange
- Make Storybook decorators (withRouter, withQueryClient, withToast, withAudio)
no-ops since global StorybookDecorator already provides these — prevents
nested Router / duplicate provider crashes in vitest-browser
- Fix nested MemoryRouter in 3 page stories (TrackDetail, PlaylistDetail, UserProfile)
- Update i18n initialization in test setup (await init before changeLanguage)
- Update ~30 test assertions from English to French to match i18n translations
- Update test assertions to match SUMI V3 design changes (shadow vs border)
- Fix remaining story type errors (PlayerError, PlaylistBatchActions,
TrackFilters, VirtualizedChatMessages)
Backend (veza-backend-api/):
- Fix response_test.go RespondWithAppError signature (2 args, not 3)
- Fix TestErrorContractAuthEndpoints expected error codes
(ErrCodeUnauthorized vs ErrCodeInvalidCredentials)
- Fix TestTrackHandler_GetTrackLikes_Success missing auth middleware setup
- Fix TestPlaybackAnalyticsService_GetTrackStats k-anonymity threshold
(needs 5 unique users, not 1)
- Replace NOW() PostgreSQL function with time.Now() parameter in marketplace
service for SQLite test compatibility
- Add missing AutoMigrate entries in marketplace_test.go
(ProductImage, ProductPreview, ProductLicense, ProductReview)
Results:
- Frontend TypeCheck: 617 errors -> 0 errors
- Frontend ESLint: 349 errors -> 0 errors
- Frontend Vitest: 196 failing tests -> 1 skipped (3396/3397 passing)
- Backend go vet: 1 error -> 0 errors
- Backend tests: 5 failing -> all 13 packages passing
- Rust: 150/150 tests passing (unchanged)
- Storybook audit: 0 errors across 1244 stories
Triage report: docs/TRIAGE_REPORT.md
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 14:48:07 +00:00
|
|
|
// 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%
|
2025-12-03 19:29:37 +00:00
|
|
|
now := time.Now()
|
|
|
|
|
sessions := []*models.PlaybackAnalytics{
|
fix: stabilize builds, tests, and lint across all stacks
Complete stabilization pass bringing all 3 stacks to green:
Frontend (apps/web/):
- Fix TypeScript nullability in useSeason.ts, useTimeOfDay.ts hooks
- Disable no-undef in ESLint config (TypeScript handles it; JSX misidentified)
- Rename 306 story imports from @storybook/react to @storybook/react-vite
- Fix conditional hook call in useMediaQuery.ts useIsTablet
- Move useQuery to top of LoginPage.tsx component
- Remove useless try/catch in GearFormModal.tsx
- Fix stale closure in ResetPasswordPage.tsx handleChange
- Make Storybook decorators (withRouter, withQueryClient, withToast, withAudio)
no-ops since global StorybookDecorator already provides these — prevents
nested Router / duplicate provider crashes in vitest-browser
- Fix nested MemoryRouter in 3 page stories (TrackDetail, PlaylistDetail, UserProfile)
- Update i18n initialization in test setup (await init before changeLanguage)
- Update ~30 test assertions from English to French to match i18n translations
- Update test assertions to match SUMI V3 design changes (shadow vs border)
- Fix remaining story type errors (PlayerError, PlaylistBatchActions,
TrackFilters, VirtualizedChatMessages)
Backend (veza-backend-api/):
- Fix response_test.go RespondWithAppError signature (2 args, not 3)
- Fix TestErrorContractAuthEndpoints expected error codes
(ErrCodeUnauthorized vs ErrCodeInvalidCredentials)
- Fix TestTrackHandler_GetTrackLikes_Success missing auth middleware setup
- Fix TestPlaybackAnalyticsService_GetTrackStats k-anonymity threshold
(needs 5 unique users, not 1)
- Replace NOW() PostgreSQL function with time.Now() parameter in marketplace
service for SQLite test compatibility
- Add missing AutoMigrate entries in marketplace_test.go
(ProductImage, ProductPreview, ProductLicense, ProductReview)
Results:
- Frontend TypeCheck: 617 errors -> 0 errors
- Frontend ESLint: 349 errors -> 0 errors
- Frontend Vitest: 196 failing tests -> 1 skipped (3396/3397 passing)
- Backend go vet: 1 error -> 0 errors
- Backend tests: 5 failing -> all 13 packages passing
- Rust: 150/150 tests passing (unchanged)
- Storybook audit: 0 errors across 1244 stories
Triage report: docs/TRIAGE_REPORT.md
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 14:48:07 +00:00
|
|
|
{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},
|
2025-12-03 19:29:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, session := range sessions {
|
|
|
|
|
db.Create(session)
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-06 16:21:59 +00:00
|
|
|
stats, err := service.GetTrackStats(ctx, trackID)
|
2025-12-03 19:29:37 +00:00
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
fix: stabilize builds, tests, and lint across all stacks
Complete stabilization pass bringing all 3 stacks to green:
Frontend (apps/web/):
- Fix TypeScript nullability in useSeason.ts, useTimeOfDay.ts hooks
- Disable no-undef in ESLint config (TypeScript handles it; JSX misidentified)
- Rename 306 story imports from @storybook/react to @storybook/react-vite
- Fix conditional hook call in useMediaQuery.ts useIsTablet
- Move useQuery to top of LoginPage.tsx component
- Remove useless try/catch in GearFormModal.tsx
- Fix stale closure in ResetPasswordPage.tsx handleChange
- Make Storybook decorators (withRouter, withQueryClient, withToast, withAudio)
no-ops since global StorybookDecorator already provides these — prevents
nested Router / duplicate provider crashes in vitest-browser
- Fix nested MemoryRouter in 3 page stories (TrackDetail, PlaylistDetail, UserProfile)
- Update i18n initialization in test setup (await init before changeLanguage)
- Update ~30 test assertions from English to French to match i18n translations
- Update test assertions to match SUMI V3 design changes (shadow vs border)
- Fix remaining story type errors (PlayerError, PlaylistBatchActions,
TrackFilters, VirtualizedChatMessages)
Backend (veza-backend-api/):
- Fix response_test.go RespondWithAppError signature (2 args, not 3)
- Fix TestErrorContractAuthEndpoints expected error codes
(ErrCodeUnauthorized vs ErrCodeInvalidCredentials)
- Fix TestTrackHandler_GetTrackLikes_Success missing auth middleware setup
- Fix TestPlaybackAnalyticsService_GetTrackStats k-anonymity threshold
(needs 5 unique users, not 1)
- Replace NOW() PostgreSQL function with time.Now() parameter in marketplace
service for SQLite test compatibility
- Add missing AutoMigrate entries in marketplace_test.go
(ProductImage, ProductPreview, ProductLicense, ProductReview)
Results:
- Frontend TypeCheck: 617 errors -> 0 errors
- Frontend ESLint: 349 errors -> 0 errors
- Frontend Vitest: 196 failing tests -> 1 skipped (3396/3397 passing)
- Backend go vet: 1 error -> 0 errors
- Backend tests: 5 failing -> all 13 packages passing
- Rust: 150/150 tests passing (unchanged)
- Storybook audit: 0 errors across 1244 stories
Triage report: docs/TRIAGE_REPORT.md
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 14:48:07 +00:00
|
|
|
assert.Equal(t, int64(5), stats.TotalSessions)
|
2026-04-14 10:22:14 +00:00
|
|
|
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
|
2025-12-03 19:29:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestPlaybackAnalyticsService_GetTrackStats_NoSessions(t *testing.T) {
|
|
|
|
|
db, service := setupTestPlaybackAnalyticsServiceDB(t)
|
|
|
|
|
ctx := context.Background()
|
|
|
|
|
|
2025-12-06 16:21:59 +00:00
|
|
|
userID := uuid.New()
|
|
|
|
|
user := &models.User{ID: userID, Username: "testuser", Email: "test@example.com", IsActive: true}
|
2025-12-03 19:29:37 +00:00
|
|
|
db.Create(user)
|
|
|
|
|
|
2025-12-06 16:21:59 +00:00
|
|
|
trackID := uuid.New()
|
2025-12-03 19:29:37 +00:00
|
|
|
track := &models.Track{
|
2025-12-06 16:21:59 +00:00
|
|
|
ID: trackID,
|
|
|
|
|
UserID: userID,
|
2025-12-03 19:29:37 +00:00
|
|
|
Title: "Test Track",
|
|
|
|
|
FilePath: "/test.mp3",
|
|
|
|
|
FileSize: 1024,
|
|
|
|
|
Format: "MP3",
|
|
|
|
|
Duration: 180,
|
|
|
|
|
IsPublic: true,
|
|
|
|
|
Status: models.TrackStatusCompleted,
|
|
|
|
|
}
|
|
|
|
|
db.Create(track)
|
|
|
|
|
|
2025-12-06 16:21:59 +00:00
|
|
|
stats, err := service.GetTrackStats(ctx, trackID)
|
2025-12-03 19:29:37 +00:00
|
|
|
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()
|
|
|
|
|
|
2025-12-06 16:21:59 +00:00
|
|
|
_, err := service.GetTrackStats(ctx, uuid.New())
|
2025-12-03 19:29:37 +00:00
|
|
|
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()
|
|
|
|
|
|
2025-12-06 16:21:59 +00:00
|
|
|
userID := uuid.New()
|
|
|
|
|
user := &models.User{ID: userID, Username: "testuser", Email: "test@example.com", IsActive: true}
|
2025-12-03 19:29:37 +00:00
|
|
|
db.Create(user)
|
|
|
|
|
|
2025-12-06 16:21:59 +00:00
|
|
|
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}
|
2025-12-03 19:29:37 +00:00
|
|
|
db.Create(track1)
|
|
|
|
|
db.Create(track2)
|
|
|
|
|
|
|
|
|
|
now := time.Now()
|
|
|
|
|
sessions := []*models.PlaybackAnalytics{
|
2025-12-06 16:21:59 +00:00
|
|
|
{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},
|
2025-12-03 19:29:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, session := range sessions {
|
|
|
|
|
db.Create(session)
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-06 16:21:59 +00:00
|
|
|
stats, err := service.GetUserStats(ctx, userID)
|
2025-12-03 19:29:37 +00:00
|
|
|
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()
|
|
|
|
|
|
2025-12-06 16:21:59 +00:00
|
|
|
_, err := service.GetUserStats(ctx, uuid.New())
|
2025-12-03 19:29:37 +00:00
|
|
|
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()
|
|
|
|
|
|
2025-12-06 16:21:59 +00:00
|
|
|
userID := uuid.New()
|
|
|
|
|
user := &models.User{ID: userID, Username: "testuser", Email: "test@example.com", IsActive: true}
|
2025-12-03 19:29:37 +00:00
|
|
|
db.Create(user)
|
|
|
|
|
|
2025-12-06 16:21:59 +00:00
|
|
|
trackID := uuid.New()
|
2025-12-03 19:29:37 +00:00
|
|
|
track := &models.Track{
|
2025-12-06 16:21:59 +00:00
|
|
|
ID: trackID,
|
|
|
|
|
UserID: userID,
|
2025-12-03 19:29:37 +00:00
|
|
|
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{
|
2025-12-16 16:23:49 +00:00
|
|
|
{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
|
2025-12-03 19:29:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, session := range sessions {
|
|
|
|
|
db.Create(session)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Récupérer les sessions des 3 derniers jours
|
|
|
|
|
startDate := baseTime.AddDate(0, 0, -2)
|
|
|
|
|
endDate := baseTime
|
|
|
|
|
|
2025-12-06 16:21:59 +00:00
|
|
|
result, err := service.GetSessionsByDateRange(ctx, trackID, startDate, endDate)
|
2025-12-03 19:29:37 +00:00
|
|
|
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()
|
|
|
|
|
|
2025-12-06 16:21:59 +00:00
|
|
|
_, err := service.GetSessionsByDateRange(ctx, uuid.Nil, startDate, endDate)
|
2025-12-03 19:29:37 +00:00
|
|
|
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
|
2025-12-06 16:21:59 +00:00
|
|
|
// Créer user et track
|
|
|
|
|
userID := uuid.New()
|
|
|
|
|
user := &models.User{ID: userID, Username: "testuser", Email: "test@example.com", IsActive: true}
|
2025-12-03 19:29:37 +00:00
|
|
|
db.Create(user)
|
2025-12-06 16:21:59 +00:00
|
|
|
trackID := uuid.New()
|
2025-12-03 19:29:37 +00:00
|
|
|
track := &models.Track{
|
2025-12-06 16:21:59 +00:00
|
|
|
ID: trackID,
|
|
|
|
|
UserID: userID,
|
2025-12-03 19:29:37 +00:00
|
|
|
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{
|
2025-12-06 16:21:59 +00:00
|
|
|
TrackID: trackID,
|
|
|
|
|
UserID: userID,
|
2025-12-03 19:29:37 +00:00
|
|
|
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()
|
|
|
|
|
|
2025-12-06 16:21:59 +00:00
|
|
|
userID := uuid.New()
|
|
|
|
|
user := &models.User{ID: userID, Username: "testuser", Email: "test@example.com", IsActive: true}
|
2025-12-03 19:29:37 +00:00
|
|
|
db.Create(user)
|
2025-12-06 16:21:59 +00:00
|
|
|
trackID := uuid.New()
|
2025-12-03 19:29:37 +00:00
|
|
|
track := &models.Track{
|
2025-12-06 16:21:59 +00:00
|
|
|
ID: trackID,
|
|
|
|
|
UserID: userID,
|
2025-12-03 19:29:37 +00:00
|
|
|
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{
|
2025-12-06 16:21:59 +00:00
|
|
|
TrackID: trackID,
|
|
|
|
|
UserID: userID,
|
2025-12-03 19:29:37 +00:00
|
|
|
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()
|
|
|
|
|
|
2025-12-06 16:21:59 +00:00
|
|
|
userID := uuid.New()
|
|
|
|
|
user := &models.User{ID: userID, Username: "testuser", Email: "test@example.com", IsActive: true}
|
2025-12-03 19:29:37 +00:00
|
|
|
db.Create(user)
|
2025-12-06 16:21:59 +00:00
|
|
|
trackID := uuid.New()
|
2025-12-03 19:29:37 +00:00
|
|
|
track := &models.Track{
|
2025-12-06 16:21:59 +00:00
|
|
|
ID: trackID,
|
|
|
|
|
UserID: userID,
|
2025-12-03 19:29:37 +00:00
|
|
|
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{
|
2025-12-06 16:21:59 +00:00
|
|
|
TrackID: trackID,
|
|
|
|
|
UserID: userID,
|
2025-12-03 19:29:37 +00:00
|
|
|
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()
|
|
|
|
|
|
2025-12-06 16:21:59 +00:00
|
|
|
userID := uuid.New()
|
|
|
|
|
user := &models.User{ID: userID, Username: "testuser", Email: "test@example.com", IsActive: true}
|
2025-12-03 19:29:37 +00:00
|
|
|
db.Create(user)
|
2025-12-06 16:21:59 +00:00
|
|
|
trackID := uuid.New()
|
2025-12-03 19:29:37 +00:00
|
|
|
track := &models.Track{
|
2025-12-06 16:21:59 +00:00
|
|
|
ID: trackID,
|
|
|
|
|
UserID: userID,
|
2025-12-03 19:29:37 +00:00
|
|
|
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{
|
2025-12-06 16:21:59 +00:00
|
|
|
TrackID: trackID,
|
|
|
|
|
UserID: userID,
|
2025-12-03 19:29:37 +00:00
|
|
|
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{
|
2025-12-06 16:21:59 +00:00
|
|
|
ID: uuid.Nil, // Non sauvegardé
|
|
|
|
|
TrackID: uuid.New(),
|
|
|
|
|
UserID: uuid.New(),
|
2025-12-03 19:29:37 +00:00
|
|
|
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()
|
|
|
|
|
|
2025-12-06 16:21:59 +00:00
|
|
|
userID := uuid.New()
|
|
|
|
|
user := &models.User{ID: userID, Username: "testuser", Email: "test@example.com", IsActive: true}
|
2025-12-03 19:29:37 +00:00
|
|
|
db.Create(user)
|
2025-12-06 16:21:59 +00:00
|
|
|
trackID := uuid.New()
|
2025-12-03 19:29:37 +00:00
|
|
|
track := &models.Track{
|
2025-12-06 16:21:59 +00:00
|
|
|
ID: trackID,
|
|
|
|
|
UserID: userID,
|
2025-12-03 19:29:37 +00:00
|
|
|
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{
|
2025-12-06 16:21:59 +00:00
|
|
|
TrackID: trackID,
|
|
|
|
|
UserID: userID,
|
2025-12-03 19:29:37 +00:00
|
|
|
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()
|
|
|
|
|
|
2025-12-06 16:21:59 +00:00
|
|
|
userID := uuid.New()
|
|
|
|
|
user := &models.User{ID: userID, Username: "testuser", Email: "test@example.com", IsActive: true}
|
2025-12-03 19:29:37 +00:00
|
|
|
db.Create(user)
|
2025-12-06 16:21:59 +00:00
|
|
|
trackID := uuid.New()
|
2025-12-03 19:29:37 +00:00
|
|
|
track := &models.Track{
|
2025-12-06 16:21:59 +00:00
|
|
|
ID: trackID,
|
|
|
|
|
UserID: userID,
|
2025-12-03 19:29:37 +00:00
|
|
|
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{
|
2025-12-06 16:21:59 +00:00
|
|
|
TrackID: trackID,
|
|
|
|
|
UserID: userID,
|
2025-12-03 19:29:37 +00:00
|
|
|
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()
|
|
|
|
|
|
2025-12-06 16:21:59 +00:00
|
|
|
err := service.UpdatePlaybackProgress(ctx, uuid.New(), 90, 180)
|
2025-12-03 19:29:37 +00:00
|
|
|
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
|
2025-12-06 16:21:59 +00:00
|
|
|
err := service.UpdatePlaybackProgress(ctx, uuid.Nil, 90, 180)
|
2025-12-03 19:29:37 +00:00
|
|
|
assert.Error(t, err)
|
|
|
|
|
assert.Contains(t, err.Error(), "invalid analytics ID")
|
|
|
|
|
|
|
|
|
|
// Test avec play time négatif
|
2025-12-06 16:21:59 +00:00
|
|
|
err = service.UpdatePlaybackProgress(ctx, uuid.New(), -10, 180)
|
2025-12-03 19:29:37 +00:00
|
|
|
assert.Error(t, err)
|
|
|
|
|
assert.Contains(t, err.Error(), "invalid play time")
|
|
|
|
|
|
|
|
|
|
// Test avec duration invalide
|
2025-12-06 16:21:59 +00:00
|
|
|
err = service.UpdatePlaybackProgress(ctx, uuid.New(), 90, 0)
|
2025-12-03 19:29:37 +00:00
|
|
|
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
|
2025-12-06 16:21:59 +00:00
|
|
|
userID := uuid.New()
|
|
|
|
|
user := &models.User{ID: userID, Username: "testuser", Slug: "testuser", Email: "test@example.com", IsActive: true}
|
2025-12-03 19:29:37 +00:00
|
|
|
db.Create(user)
|
2025-12-06 16:21:59 +00:00
|
|
|
trackID := uuid.New()
|
2025-12-03 19:29:37 +00:00
|
|
|
track := &models.Track{
|
2025-12-06 16:21:59 +00:00
|
|
|
ID: trackID,
|
|
|
|
|
UserID: userID,
|
2025-12-03 19:29:37 +00:00
|
|
|
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{
|
2025-12-06 16:21:59 +00:00
|
|
|
{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},
|
2025-12-03 19:29:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
err := service.RecordPlaybackBatch(ctx, analyticsList)
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
|
|
// Vérifier que tous les analytics ont été enregistrés
|
|
|
|
|
var count int64
|
2025-12-06 16:21:59 +00:00
|
|
|
db.Model(&models.PlaybackAnalytics{}).Where("track_id = ?", trackID).Count(&count)
|
2025-12-03 19:29:37 +00:00
|
|
|
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{
|
2025-12-06 16:21:59 +00:00
|
|
|
{TrackID: uuid.Nil, UserID: uuid.New(), PlayTime: 120, StartedAt: now}, // TrackID invalide
|
2025-12-03 19:29:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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
|
2025-12-06 16:21:59 +00:00
|
|
|
userID := uuid.New()
|
|
|
|
|
user := &models.User{ID: userID, Username: "testuser", Slug: "testuser", Email: "test@example.com", IsActive: true}
|
2025-12-03 19:29:37 +00:00
|
|
|
db.Create(user)
|
2025-12-06 16:21:59 +00:00
|
|
|
trackID := uuid.New()
|
2025-12-03 19:29:37 +00:00
|
|
|
track := &models.Track{
|
2025-12-06 16:21:59 +00:00
|
|
|
ID: trackID,
|
|
|
|
|
UserID: userID,
|
2025-12-03 19:29:37 +00:00
|
|
|
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{
|
2025-12-06 16:21:59 +00:00
|
|
|
TrackID: trackID,
|
|
|
|
|
UserID: userID,
|
2025-12-03 19:29:37 +00:00
|
|
|
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
|
2025-12-06 16:21:59 +00:00
|
|
|
result, err := service.GetSessionsByDateRangePaginated(ctx, trackID, startDate, endDate, 1, 5)
|
2025-12-03 19:29:37 +00:00
|
|
|
require.NoError(t, err)
|
|
|
|
|
assert.Equal(t, 5, len(result))
|
|
|
|
|
|
|
|
|
|
// Page 2, 5 éléments par page
|
2025-12-06 16:21:59 +00:00
|
|
|
result2, err := service.GetSessionsByDateRangePaginated(ctx, trackID, startDate, endDate, 2, 5)
|
2025-12-03 19:29:37 +00:00
|
|
|
require.NoError(t, err)
|
|
|
|
|
assert.Equal(t, 5, len(result2))
|
|
|
|
|
|
|
|
|
|
// Vérifier qu'il n'y a pas de doublons
|
2025-12-06 16:21:59 +00:00
|
|
|
ids1 := make(map[uuid.UUID]bool)
|
2025-12-03 19:29:37 +00:00
|
|
|
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
|
2025-12-06 16:21:59 +00:00
|
|
|
userID := uuid.New()
|
|
|
|
|
user := &models.User{ID: userID, Username: "testuser", Slug: "testuser", Email: "test@example.com", IsActive: true}
|
2025-12-03 19:29:37 +00:00
|
|
|
db.Create(user)
|
2025-12-06 16:21:59 +00:00
|
|
|
trackID := uuid.New()
|
2025-12-03 19:29:37 +00:00
|
|
|
track := &models.Track{
|
2025-12-06 16:21:59 +00:00
|
|
|
ID: trackID,
|
|
|
|
|
UserID: userID,
|
2025-12-03 19:29:37 +00:00
|
|
|
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{
|
2025-12-06 16:21:59 +00:00
|
|
|
TrackID: trackID,
|
|
|
|
|
UserID: userID,
|
2025-12-03 19:29:37 +00:00
|
|
|
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
|
2025-12-06 16:21:59 +00:00
|
|
|
result, err := service.GetSessionsByDateRangePaginatedResult(ctx, trackID, startDate, endDate, 1, 10)
|
2025-12-03 19:29:37 +00:00
|
|
|
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
|
2025-12-06 16:21:59 +00:00
|
|
|
userID := uuid.New()
|
|
|
|
|
user := &models.User{ID: userID, Username: "testuser", Slug: "testuser", Email: "test@example.com", IsActive: true}
|
2025-12-03 19:29:37 +00:00
|
|
|
db.Create(user)
|
2025-12-06 16:21:59 +00:00
|
|
|
trackID := uuid.New()
|
2025-12-03 19:29:37 +00:00
|
|
|
track := &models.Track{
|
2025-12-06 16:21:59 +00:00
|
|
|
ID: trackID,
|
|
|
|
|
UserID: userID,
|
2025-12-03 19:29:37 +00:00
|
|
|
Title: "Test Track",
|
|
|
|
|
FilePath: "/test.mp3",
|
|
|
|
|
FileSize: 1024,
|
|
|
|
|
Format: "MP3",
|
|
|
|
|
Duration: 180,
|
|
|
|
|
IsPublic: true,
|
|
|
|
|
Status: models.TrackStatusCompleted,
|
|
|
|
|
}
|
|
|
|
|
db.Create(track)
|
|
|
|
|
|
|
|
|
|
now := time.Now()
|
2025-12-06 16:21:59 +00:00
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-03 19:29:37 +00:00
|
|
|
startDate := now.Add(-1 * time.Hour)
|
2025-12-06 16:21:59 +00:00
|
|
|
endDate := now.Add(26 * time.Hour)
|
2025-12-03 19:29:37 +00:00
|
|
|
|
|
|
|
|
// Tester avec page = 0 (devrait devenir 1)
|
2025-12-06 16:21:59 +00:00
|
|
|
result, err := service.GetSessionsByDateRangePaginatedResult(ctx, trackID, startDate, endDate, 0, 0)
|
2025-12-03 19:29:37 +00:00
|
|
|
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)
|
2025-12-06 16:21:59 +00:00
|
|
|
result2, err := service.GetSessionsByDateRangePaginatedResult(ctx, trackID, startDate, endDate, 1, 2000)
|
2025-12-03 19:29:37 +00:00
|
|
|
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
|
2025-12-06 16:21:59 +00:00
|
|
|
userID := uuid.New()
|
|
|
|
|
user := &models.User{ID: userID, Username: "testuser", Slug: "testuser", Email: "test@example.com", IsActive: true}
|
2025-12-03 19:29:37 +00:00
|
|
|
db.Create(user)
|
2025-12-06 16:21:59 +00:00
|
|
|
trackID := uuid.New()
|
2025-12-03 19:29:37 +00:00
|
|
|
track := &models.Track{
|
2025-12-06 16:21:59 +00:00
|
|
|
ID: trackID,
|
|
|
|
|
UserID: userID,
|
2025-12-03 19:29:37 +00:00
|
|
|
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{
|
2025-12-06 16:21:59 +00:00
|
|
|
TrackID: trackID,
|
|
|
|
|
UserID: userID,
|
2025-12-03 19:29:37 +00:00
|
|
|
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)
|
2025-12-06 16:21:59 +00:00
|
|
|
result, err := service.GetSessionsByDateRangePaginated(ctx, trackID, startDate, endDate, 0, 100)
|
2025-12-03 19:29:37 +00:00
|
|
|
require.NoError(t, err)
|
|
|
|
|
assert.Equal(t, 5, len(result)) // Devrait retourner toutes les sessions
|
|
|
|
|
}
|