375 lines
11 KiB
Go
375 lines
11 KiB
Go
package services
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
|
|
"veza-backend-api/internal/models"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"go.uber.org/zap"
|
|
"gorm.io/driver/sqlite"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
func setupTestAnalyticsService(t *testing.T) (*AnalyticsService, *gorm.DB, func()) {
|
|
// Setup in-memory SQLite database
|
|
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
|
|
require.NoError(t, err)
|
|
|
|
// Enable foreign keys for SQLite
|
|
db.Exec("PRAGMA foreign_keys = ON")
|
|
|
|
// Auto-migrate
|
|
err = db.AutoMigrate(&models.User{}, &models.Track{}, &models.TrackPlay{})
|
|
require.NoError(t, err)
|
|
|
|
// Create test user
|
|
user := &models.User{
|
|
Username: "testuser",
|
|
Email: "test@example.com",
|
|
PasswordHash: "hash",
|
|
Slug: "testuser",
|
|
IsActive: true,
|
|
}
|
|
err = db.Create(user).Error
|
|
require.NoError(t, err)
|
|
|
|
// Create test track
|
|
track := &models.Track{
|
|
UserID: user.ID,
|
|
Title: "Test Track",
|
|
FilePath: "/test/track.mp3",
|
|
FileSize: 5 * 1024 * 1024,
|
|
Format: "MP3",
|
|
Duration: 180, // 3 minutes
|
|
IsPublic: true,
|
|
Status: models.TrackStatusCompleted,
|
|
}
|
|
err = db.Create(track).Error
|
|
require.NoError(t, err)
|
|
|
|
// Setup logger
|
|
logger := zap.NewNop()
|
|
|
|
// Setup test service
|
|
service := NewAnalyticsService(db, logger)
|
|
|
|
// Cleanup function
|
|
cleanup := func() {
|
|
// Database will be closed automatically
|
|
}
|
|
|
|
return service, db, cleanup
|
|
}
|
|
|
|
func TestAnalyticsService_RecordPlay(t *testing.T) {
|
|
service, db, cleanup := setupTestAnalyticsService(t)
|
|
defer cleanup()
|
|
|
|
ctx := context.Background()
|
|
|
|
// Get track ID
|
|
var track models.Track
|
|
err := db.First(&track).Error
|
|
require.NoError(t, err)
|
|
|
|
// Get user ID
|
|
var user models.User
|
|
err = db.First(&user).Error
|
|
require.NoError(t, err)
|
|
|
|
t.Run("Record play with user", func(t *testing.T) {
|
|
userID := user.ID
|
|
err := service.RecordPlay(ctx, track.ID, &userID, 120, "Chrome", "192.168.1.1")
|
|
assert.NoError(t, err)
|
|
|
|
// Verify play was recorded
|
|
var count int64
|
|
db.Model(&models.TrackPlay{}).Where("track_id = ? AND user_id = ?", track.ID, userID).Count(&count)
|
|
assert.Equal(t, int64(1), count)
|
|
})
|
|
|
|
t.Run("Record play without user (anonymous)", func(t *testing.T) {
|
|
err := service.RecordPlay(ctx, track.ID, nil, 60, "Firefox", "10.0.0.1")
|
|
assert.NoError(t, err)
|
|
|
|
// Verify play was recorded
|
|
var count int64
|
|
db.Model(&models.TrackPlay{}).Where("track_id = ? AND user_id IS NULL", track.ID).Count(&count)
|
|
assert.Equal(t, int64(1), count)
|
|
})
|
|
|
|
t.Run("Record play with invalid track ID", func(t *testing.T) {
|
|
userID := user.ID
|
|
err := service.RecordPlay(ctx, uuid.New(), &userID, 120, "Chrome", "192.168.1.1")
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "track not found")
|
|
})
|
|
}
|
|
|
|
func TestAnalyticsService_GetTrackStats(t *testing.T) {
|
|
service, db, cleanup := setupTestAnalyticsService(t)
|
|
defer cleanup()
|
|
|
|
ctx := context.Background()
|
|
|
|
// Get track ID
|
|
var track models.Track
|
|
err := db.First(&track).Error
|
|
require.NoError(t, err)
|
|
|
|
// Get user ID
|
|
var user models.User
|
|
err = db.First(&user).Error
|
|
require.NoError(t, err)
|
|
|
|
// Create multiple plays
|
|
userID := user.ID
|
|
plays := []models.TrackPlay{
|
|
{TrackID: track.ID, UserID: &userID, Duration: 120, PlayedAt: time.Now()},
|
|
{TrackID: track.ID, UserID: &userID, Duration: 150, PlayedAt: time.Now()},
|
|
{TrackID: track.ID, UserID: nil, Duration: 100, PlayedAt: time.Now()},
|
|
{TrackID: track.ID, UserID: nil, Duration: 180, PlayedAt: time.Now()}, // Completed
|
|
}
|
|
|
|
for _, play := range plays {
|
|
err = db.Create(&play).Error
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
t.Run("Get track stats", func(t *testing.T) {
|
|
stats, err := service.GetTrackStats(ctx, track.ID)
|
|
assert.NoError(t, err)
|
|
assert.NotNil(t, stats)
|
|
assert.Equal(t, int64(4), stats.TotalPlays)
|
|
assert.Equal(t, int64(1), stats.UniqueListeners) // Only one user (anonymous plays don't count)
|
|
assert.Greater(t, stats.AverageDuration, 0.0)
|
|
assert.Greater(t, stats.CompletionRate, 0.0) // At least one play completed 90%+
|
|
})
|
|
|
|
t.Run("Get track stats with invalid track ID", func(t *testing.T) {
|
|
stats, err := service.GetTrackStats(ctx, uuid.New())
|
|
assert.Error(t, err)
|
|
assert.Nil(t, stats)
|
|
assert.Contains(t, err.Error(), "track not found")
|
|
})
|
|
|
|
t.Run("Get track stats with no plays", func(t *testing.T) {
|
|
// Create a new track without plays
|
|
newTrack := &models.Track{
|
|
UserID: user.ID,
|
|
Title: "New Track",
|
|
FilePath: "/test/new.mp3",
|
|
FileSize: 5 * 1024 * 1024,
|
|
Format: "MP3",
|
|
Duration: 180,
|
|
IsPublic: true,
|
|
Status: models.TrackStatusCompleted,
|
|
}
|
|
err = db.Create(newTrack).Error
|
|
require.NoError(t, err)
|
|
|
|
stats, err := service.GetTrackStats(ctx, newTrack.ID)
|
|
assert.NoError(t, err)
|
|
assert.NotNil(t, stats)
|
|
assert.Equal(t, int64(0), stats.TotalPlays)
|
|
assert.Equal(t, int64(0), stats.UniqueListeners)
|
|
assert.Equal(t, 0.0, stats.AverageDuration)
|
|
assert.Equal(t, 0.0, stats.CompletionRate)
|
|
})
|
|
}
|
|
|
|
func TestAnalyticsService_GetPlaysOverTime(t *testing.T) {
|
|
service, db, cleanup := setupTestAnalyticsService(t)
|
|
defer cleanup()
|
|
|
|
ctx := context.Background()
|
|
|
|
// Get track ID
|
|
var track models.Track
|
|
err := db.First(&track).Error
|
|
require.NoError(t, err)
|
|
|
|
// Create plays at different times
|
|
now := time.Now()
|
|
plays := []models.TrackPlay{
|
|
{TrackID: track.ID, Duration: 120, PlayedAt: now.Add(-24 * time.Hour)},
|
|
{TrackID: track.ID, Duration: 150, PlayedAt: now.Add(-12 * time.Hour)},
|
|
{TrackID: track.ID, Duration: 100, PlayedAt: now},
|
|
}
|
|
|
|
for _, play := range plays {
|
|
err = db.Create(&play).Error
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
t.Run("Get plays over time", func(t *testing.T) {
|
|
startDate := now.Add(-48 * time.Hour)
|
|
endDate := now.Add(1 * time.Hour)
|
|
points, err := service.GetPlaysOverTime(ctx, track.ID, startDate, endDate, "day")
|
|
assert.NoError(t, err)
|
|
assert.NotNil(t, points)
|
|
assert.Greater(t, len(points), 0)
|
|
})
|
|
|
|
t.Run("Get plays over time with invalid track ID", func(t *testing.T) {
|
|
startDate := time.Now().Add(-48 * time.Hour)
|
|
endDate := time.Now()
|
|
points, err := service.GetPlaysOverTime(ctx, uuid.New(), startDate, endDate, "day")
|
|
assert.Error(t, err)
|
|
assert.Nil(t, points)
|
|
assert.Contains(t, err.Error(), "track not found")
|
|
})
|
|
}
|
|
|
|
func TestAnalyticsService_GetTopTracks(t *testing.T) {
|
|
service, db, cleanup := setupTestAnalyticsService(t)
|
|
defer cleanup()
|
|
|
|
ctx := context.Background()
|
|
|
|
// Get user ID
|
|
var user models.User
|
|
err := db.First(&user).Error
|
|
require.NoError(t, err)
|
|
|
|
// Create multiple tracks
|
|
tracks := []models.Track{
|
|
{UserID: user.ID, Title: "Track 1", FilePath: "/test/1.mp3", FileSize: 5 * 1024 * 1024, Format: "MP3", Duration: 180, IsPublic: true, Status: models.TrackStatusCompleted},
|
|
{UserID: user.ID, Title: "Track 2", FilePath: "/test/2.mp3", FileSize: 5 * 1024 * 1024, Format: "MP3", Duration: 180, IsPublic: true, Status: models.TrackStatusCompleted},
|
|
{UserID: user.ID, Title: "Track 3", FilePath: "/test/3.mp3", FileSize: 5 * 1024 * 1024, Format: "MP3", Duration: 180, IsPublic: true, Status: models.TrackStatusCompleted},
|
|
}
|
|
|
|
for i := range tracks {
|
|
err = db.Create(&tracks[i]).Error
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
// Create plays for tracks (Track 1: 5 plays, Track 2: 3 plays, Track 3: 1 play)
|
|
for i := 0; i < 5; i++ {
|
|
play := models.TrackPlay{TrackID: tracks[0].ID, Duration: 120, PlayedAt: time.Now()}
|
|
db.Create(&play)
|
|
}
|
|
for i := 0; i < 3; i++ {
|
|
play := models.TrackPlay{TrackID: tracks[1].ID, Duration: 150, PlayedAt: time.Now()}
|
|
db.Create(&play)
|
|
}
|
|
play := models.TrackPlay{TrackID: tracks[2].ID, Duration: 100, PlayedAt: time.Now()}
|
|
db.Create(&play)
|
|
|
|
t.Run("Get top tracks", func(t *testing.T) {
|
|
topTracks, err := service.GetTopTracks(ctx, 10, nil, nil)
|
|
assert.NoError(t, err)
|
|
assert.NotNil(t, topTracks)
|
|
assert.GreaterOrEqual(t, len(topTracks), 3)
|
|
|
|
// Verify ordering (most plays first)
|
|
if len(topTracks) >= 3 {
|
|
assert.Equal(t, int64(5), topTracks[0].TotalPlays) // Track 1
|
|
assert.Equal(t, int64(3), topTracks[1].TotalPlays) // Track 2
|
|
assert.Equal(t, int64(1), topTracks[2].TotalPlays) // Track 3
|
|
}
|
|
})
|
|
|
|
t.Run("Get top tracks with limit", func(t *testing.T) {
|
|
topTracks, err := service.GetTopTracks(ctx, 2, nil, nil)
|
|
assert.NoError(t, err)
|
|
assert.NotNil(t, topTracks)
|
|
assert.LessOrEqual(t, len(topTracks), 2)
|
|
})
|
|
|
|
t.Run("Get top tracks with date filter", func(t *testing.T) {
|
|
startDate := time.Now().Add(-24 * time.Hour)
|
|
endDate := time.Now().Add(1 * time.Hour)
|
|
topTracks, err := service.GetTopTracks(ctx, 10, &startDate, &endDate)
|
|
assert.NoError(t, err)
|
|
assert.NotNil(t, topTracks)
|
|
})
|
|
}
|
|
|
|
func TestAnalyticsService_GetUserStats(t *testing.T) {
|
|
service, db, cleanup := setupTestAnalyticsService(t)
|
|
defer cleanup()
|
|
|
|
ctx := context.Background()
|
|
|
|
// Get user ID
|
|
var user models.User
|
|
err := db.First(&user).Error
|
|
require.NoError(t, err)
|
|
|
|
// Get track ID
|
|
var track models.Track
|
|
err = db.First(&track).Error
|
|
require.NoError(t, err)
|
|
|
|
// Create another track
|
|
anotherTrack := &models.Track{
|
|
UserID: user.ID,
|
|
Title: "Another Track",
|
|
FilePath: "/test/another.mp3",
|
|
FileSize: 5 * 1024 * 1024,
|
|
Format: "MP3",
|
|
Duration: 180,
|
|
IsPublic: true,
|
|
Status: models.TrackStatusCompleted,
|
|
}
|
|
err = db.Create(anotherTrack).Error
|
|
require.NoError(t, err)
|
|
|
|
// Create plays for the user
|
|
userID := user.ID
|
|
plays := []models.TrackPlay{
|
|
{TrackID: track.ID, UserID: &userID, Duration: 120, PlayedAt: time.Now()},
|
|
{TrackID: track.ID, UserID: &userID, Duration: 150, PlayedAt: time.Now()},
|
|
{TrackID: anotherTrack.ID, UserID: &userID, Duration: 100, PlayedAt: time.Now()},
|
|
}
|
|
|
|
for _, play := range plays {
|
|
err = db.Create(&play).Error
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
t.Run("Get user stats", func(t *testing.T) {
|
|
stats, err := service.GetUserStats(ctx, user.ID)
|
|
assert.NoError(t, err)
|
|
assert.NotNil(t, stats)
|
|
assert.Equal(t, int64(3), stats.TotalPlays)
|
|
assert.Equal(t, int64(2), stats.UniqueTracks)
|
|
assert.Greater(t, stats.TotalDuration, int64(0))
|
|
assert.Greater(t, stats.AverageDuration, 0.0)
|
|
})
|
|
|
|
t.Run("Get user stats with invalid user ID", func(t *testing.T) {
|
|
stats, err := service.GetUserStats(ctx, uuid.New())
|
|
assert.Error(t, err)
|
|
assert.Nil(t, stats)
|
|
assert.Contains(t, err.Error(), "user not found")
|
|
})
|
|
|
|
t.Run("Get user stats with no plays", func(t *testing.T) {
|
|
// Create a new user without plays
|
|
newUser := &models.User{
|
|
Username: "newuser",
|
|
Email: "new@example.com",
|
|
PasswordHash: "hash",
|
|
Slug: "newuser",
|
|
IsActive: true,
|
|
}
|
|
err = db.Create(newUser).Error
|
|
require.NoError(t, err)
|
|
|
|
stats, err := service.GetUserStats(ctx, newUser.ID)
|
|
assert.NoError(t, err)
|
|
assert.NotNil(t, stats)
|
|
assert.Equal(t, int64(0), stats.TotalPlays)
|
|
assert.Equal(t, int64(0), stats.UniqueTracks)
|
|
assert.Equal(t, int64(0), stats.TotalDuration)
|
|
assert.Equal(t, 0.0, stats.AverageDuration)
|
|
})
|
|
}
|