veza/veza-backend-api/internal/services/hls_service_test.go
2025-12-16 11:23:49 -05:00

567 lines
16 KiB
Go

package services
import (
"context"
"fmt"
"os"
"path/filepath"
"testing"
"github.com/google/uuid"
"veza-backend-api/internal/models"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/zap/zaptest"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
func setupTestHLSService(t *testing.T) (*HLSService, *gorm.DB, string, func()) {
// Setup in-memory database
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
require.NoError(t, err)
// Enable foreign keys
db.Exec("PRAGMA foreign_keys = ON")
// Auto-migrate
err = db.AutoMigrate(&models.User{}, &models.Track{}, &models.HLSStream{})
require.NoError(t, err)
userID := uuid.New()
// Create test user
user := &models.User{
ID: userID,
Username: "testuser",
Email: "test@example.com",
IsActive: true,
}
err = db.Create(user).Error
require.NoError(t, err)
// Create test track
track := &models.Track{
UserID: userID,
Title: "Test Track",
FilePath: "/test/track.mp3",
FileSize: 5 * 1024 * 1024,
Format: "MP3",
Duration: 180,
IsPublic: true,
Status: models.TrackStatusCompleted,
}
err = db.Create(track).Error
require.NoError(t, err)
// Create test directory structure
testDir := filepath.Join(os.TempDir(), fmt.Sprintf("hls_service_test_%d", os.Getpid()))
require.NoError(t, os.MkdirAll(testDir, 0755))
trackDir := filepath.Join(testDir, fmt.Sprintf("track_%s", track.ID.String()))
require.NoError(t, os.MkdirAll(trackDir, 0755))
// Create master playlist
masterPlaylistPath := filepath.Join(trackDir, "master.m3u8")
masterPlaylistContent := `#EXTM3U
#EXT-X-VERSION:3
#EXT-X-STREAM-INF:BANDWIDTH=128000
128k/playlist.m3u8
`
require.NoError(t, os.WriteFile(masterPlaylistPath, []byte(masterPlaylistContent), 0644))
// Create quality playlist
qualityDir := filepath.Join(trackDir, "128k")
require.NoError(t, os.MkdirAll(qualityDir, 0755))
qualityPlaylistPath := filepath.Join(qualityDir, "playlist.m3u8")
qualityPlaylistContent := `#EXTM3U
#EXT-X-VERSION:3
#EXTINF:10.0,
segment_000.ts
`
require.NoError(t, os.WriteFile(qualityPlaylistPath, []byte(qualityPlaylistContent), 0644))
// Create test segment
segmentPath := filepath.Join(qualityDir, "segment_000.ts")
require.NoError(t, os.WriteFile(segmentPath, []byte("test segment data"), 0644))
// Create HLS stream
hlsStream := &models.HLSStream{
TrackID: track.ID,
PlaylistURL: filepath.Join(fmt.Sprintf("track_%s", track.ID.String()), "master.m3u8"),
SegmentsCount: 1,
Bitrates: models.BitrateList{128},
Status: models.HLSStatusReady,
}
err = db.Create(hlsStream).Error
require.NoError(t, err)
// Create service
logger := zaptest.NewLogger(t)
service := NewHLSService(db, testDir, logger)
cleanup := func() {
os.RemoveAll(testDir)
}
return service, db, testDir, cleanup
}
func TestNewHLSService(t *testing.T) {
logger := zaptest.NewLogger(t)
db, _ := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
service := NewHLSService(db, "/tmp", logger)
assert.NotNil(t, service)
assert.Equal(t, "/tmp", service.outputDir)
assert.NotNil(t, service.logger)
}
func TestNewHLSService_NilLogger(t *testing.T) {
db, _ := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
service := NewHLSService(db, "/tmp", nil)
assert.NotNil(t, service)
assert.NotNil(t, service.logger) // Devrait créer un logger Nop
}
func TestHLSService_GetMasterPlaylist(t *testing.T) {
service, _, _, cleanup := setupTestHLSService(t)
defer cleanup()
ctx := context.Background()
// We need to find the track ID created in setup
// Since we can't easily get it returned from setup without changing signature,
// let's query the DB or rely on the fact that setup creates one track.
// Actually, setupTestHLSService returns (service, db, testDir, cleanup).
// We can query the DB.
var track models.Track
result := service.db.First(&track)
require.NoError(t, result.Error)
playlist, err := service.GetMasterPlaylist(ctx, track.ID)
assert.NoError(t, err)
assert.Contains(t, playlist, "#EXTM3U")
assert.Contains(t, playlist, "128k/playlist.m3u8")
}
func TestHLSService_GetMasterPlaylist_NotFound(t *testing.T) {
service, _, _, cleanup := setupTestHLSService(t)
defer cleanup()
ctx := context.Background()
playlist, err := service.GetMasterPlaylist(ctx, uuid.New())
assert.Error(t, err)
assert.Empty(t, playlist)
assert.Contains(t, err.Error(), "not found")
}
func TestHLSService_GetQualityPlaylist(t *testing.T) {
service, _, _, cleanup := setupTestHLSService(t)
defer cleanup()
ctx := context.Background()
var track models.Track
service.db.First(&track)
playlist, err := service.GetQualityPlaylist(ctx, track.ID, "128k")
assert.NoError(t, err)
assert.Contains(t, playlist, "#EXTM3U")
assert.Contains(t, playlist, "segment_000.ts")
}
func TestHLSService_GetQualityPlaylist_NotFound(t *testing.T) {
service, _, _, cleanup := setupTestHLSService(t)
defer cleanup()
ctx := context.Background()
playlist, err := service.GetQualityPlaylist(ctx, uuid.New(), "128k")
assert.Error(t, err)
assert.Empty(t, playlist)
assert.Contains(t, err.Error(), "not found")
}
func TestHLSService_GetQualityPlaylist_InvalidBitrate(t *testing.T) {
service, _, _, cleanup := setupTestHLSService(t)
defer cleanup()
ctx := context.Background()
var track models.Track
service.db.First(&track)
playlist, err := service.GetQualityPlaylist(ctx, track.ID, "999k")
assert.Error(t, err)
assert.Empty(t, playlist)
assert.Contains(t, err.Error(), "not found")
}
func TestHLSService_GetSegmentPath(t *testing.T) {
service, _, _, cleanup := setupTestHLSService(t)
defer cleanup()
ctx := context.Background()
var track models.Track
service.db.First(&track)
segmentPath, err := service.GetSegmentPath(ctx, track.ID, "128k", "segment_000.ts")
assert.NoError(t, err)
assert.NotEmpty(t, segmentPath)
assert.FileExists(t, segmentPath)
}
func TestHLSService_GetSegmentPath_NotFound(t *testing.T) {
service, _, _, cleanup := setupTestHLSService(t)
defer cleanup()
ctx := context.Background()
segmentPath, err := service.GetSegmentPath(ctx, uuid.New(), "128k", "segment_000.ts")
assert.Error(t, err)
assert.Empty(t, segmentPath)
assert.Contains(t, err.Error(), "not found")
}
func TestHLSService_GetSegmentPath_InvalidSegment(t *testing.T) {
service, _, _, cleanup := setupTestHLSService(t)
defer cleanup()
ctx := context.Background()
var track models.Track
service.db.First(&track)
segmentPath, err := service.GetSegmentPath(ctx, track.ID, "128k", "nonexistent.ts")
assert.Error(t, err)
assert.Empty(t, segmentPath)
assert.Contains(t, err.Error(), "not found")
}
func TestHLSService_GetSegmentPath_DirectoryTraversal(t *testing.T) {
service, _, _, cleanup := setupTestHLSService(t)
defer cleanup()
ctx := context.Background()
var track models.Track
service.db.First(&track)
// Tentative de directory traversal
segmentPath, err := service.GetSegmentPath(ctx, track.ID, "128k", "../../../etc/passwd")
assert.Error(t, err)
assert.Empty(t, segmentPath)
// Le fichier n'existe pas, donc erreur "not found" ou "invalid path"
assert.True(t, err != nil)
}
func TestHLSService_GetStreamStatus(t *testing.T) {
service, _, _, cleanup := setupTestHLSService(t)
defer cleanup()
ctx := context.Background()
var track models.Track
service.db.First(&track)
status, err := service.GetStreamStatus(ctx, track.ID)
assert.NoError(t, err)
assert.NotNil(t, status)
assert.Equal(t, models.HLSStatusReady, status["status"])
assert.Equal(t, models.BitrateList{128}, status["bitrates"])
assert.Equal(t, 1, status["segments_count"])
assert.Contains(t, status["playlist_url"], "master.m3u8")
assert.Equal(t, track.ID, status["track_id"])
}
func TestHLSService_GetStreamStatus_NotFound(t *testing.T) {
service, _, _, cleanup := setupTestHLSService(t)
defer cleanup()
ctx := context.Background()
status, err := service.GetStreamStatus(ctx, uuid.New())
assert.Error(t, err)
assert.Nil(t, status)
assert.Contains(t, err.Error(), "not found")
}
func TestHLSService_GetStreamStatus_Processing(t *testing.T) {
logger := zaptest.NewLogger(t)
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.HLSStream{}, &models.HLSTranscodeQueue{})
require.NoError(t, err)
userID := uuid.New()
// Create test user
user := &models.User{
ID: userID,
Username: "testuser",
Email: "test@example.com",
IsActive: true,
}
err = db.Create(user).Error
require.NoError(t, err)
// Create test track
track := &models.Track{
UserID: userID,
Title: "Test Track",
FilePath: "/test/track.mp3",
FileSize: 5 * 1024 * 1024,
Format: "MP3",
Duration: 180,
IsPublic: true,
Status: models.TrackStatusProcessing,
}
err = db.Create(track).Error
require.NoError(t, err)
// Create HLS stream with processing status
hlsStream := &models.HLSStream{
TrackID: track.ID,
PlaylistURL: "track_1/master.m3u8",
SegmentsCount: 0,
Bitrates: models.BitrateList{},
Status: models.HLSStatusProcessing,
}
err = db.Create(hlsStream).Error
require.NoError(t, err)
// Create queue job
queueJob := &models.HLSTranscodeQueue{
TrackID: track.ID,
Priority: 5,
Status: models.QueueStatusProcessing,
RetryCount: 0,
MaxRetries: 3,
}
err = db.Create(queueJob).Error
require.NoError(t, err)
// Create service
testDir := filepath.Join(os.TempDir(), fmt.Sprintf("hls_service_test_%d", os.Getpid()))
service := NewHLSService(db, testDir, logger)
ctx := context.Background()
status, err := service.GetStreamStatus(ctx, track.ID)
assert.NoError(t, err)
assert.NotNil(t, status)
assert.Equal(t, models.HLSStatusProcessing, status["status"])
assert.Equal(t, queueJob.ID, status["queue_job_id"])
assert.Equal(t, queueJob.RetryCount, status["retry_count"])
}
func TestHLSService_TriggerTranscode(t *testing.T) {
// Setup
logger := zaptest.NewLogger(t)
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.HLSStream{})
require.NoError(t, err)
userID := uuid.New()
// Create test user
user := &models.User{
ID: userID,
Username: "testuser",
Email: "test@example.com",
IsActive: true,
}
err = db.Create(user).Error
require.NoError(t, err)
// Create test directory
testDir := filepath.Join(os.TempDir(), fmt.Sprintf("hls_trigger_test_%d", os.Getpid()))
require.NoError(t, os.MkdirAll(testDir, 0755))
defer os.RemoveAll(testDir)
// Create test track with audio file
testAudioFile := filepath.Join(testDir, "test.mp3")
require.NoError(t, os.WriteFile(testAudioFile, []byte("fake audio content"), 0644))
track := &models.Track{
UserID: userID,
Title: "Test Track",
FilePath: testAudioFile,
FileSize: 1024,
Format: "mp3",
Duration: 180,
Status: models.TrackStatusCompleted,
}
err = db.Create(track).Error
require.NoError(t, err)
// Create transcode service
transcodeService := NewHLSTranscodeService(testDir, logger)
hlsService := NewHLSServiceWithTranscode(db, testDir, transcodeService, logger)
ctx := context.Background()
// Note: Ce test échouera si ffmpeg n'est pas installé
// C'est acceptable car c'est un test d'intégration
err = hlsService.TriggerTranscode(ctx, track)
if err != nil {
// Si ffmpeg n'est pas disponible, vérifier que l'erreur est logique
assert.Error(t, err)
// Vérifier qu'un stream a été créé avec statut "failed"
var stream models.HLSStream
err = db.Where("track_id = ?", track.ID).First(&stream).Error
if err == nil {
assert.Equal(t, models.HLSStatusFailed, stream.Status)
}
} else {
// Si ffmpeg est disponible, vérifier que le stream a été créé avec succès
var stream models.HLSStream
err = db.Where("track_id = ?", track.ID).First(&stream).Error
assert.NoError(t, err)
assert.Equal(t, models.HLSStatusReady, stream.Status)
assert.NotEmpty(t, stream.PlaylistURL)
assert.Greater(t, stream.SegmentsCount, 0)
}
}
func TestHLSService_TriggerTranscode_NilTrack(t *testing.T) {
logger := zaptest.NewLogger(t)
db, _ := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
transcodeService := NewHLSTranscodeService("/tmp", logger)
service := NewHLSServiceWithTranscode(db, "/tmp", transcodeService, logger)
ctx := context.Background()
err := service.TriggerTranscode(ctx, nil)
assert.Error(t, err)
assert.Contains(t, err.Error(), "track cannot be nil")
}
func TestHLSService_TriggerTranscode_NoTranscodeService(t *testing.T) {
logger := zaptest.NewLogger(t)
db, _ := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
service := NewHLSService(db, "/tmp", logger)
track := &models.Track{
ID: uuid.New(),
Title: "Test Track",
FilePath: "/test/track.mp3",
}
ctx := context.Background()
err := service.TriggerTranscode(ctx, track)
assert.Error(t, err)
assert.Contains(t, err.Error(), "transcode service not configured")
}
func TestHLSService_TriggerTranscode_AlreadyExists(t *testing.T) {
logger := zaptest.NewLogger(t)
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.HLSStream{})
require.NoError(t, err)
userID := uuid.New()
// Create test user
user := &models.User{
ID: userID,
Username: "testuser",
Email: "test@example.com",
IsActive: true,
}
err = db.Create(user).Error
require.NoError(t, err)
// Create test track
track := &models.Track{
UserID: userID,
Title: "Test Track",
FilePath: "/test/track.mp3",
FileSize: 1024,
Format: "mp3",
Duration: 180,
Status: models.TrackStatusCompleted,
}
err = db.Create(track).Error
require.NoError(t, err)
// Create existing HLS stream with ready status
hlsStream := &models.HLSStream{
TrackID: track.ID,
PlaylistURL: "/test/master.m3u8",
Status: models.HLSStatusReady,
}
err = db.Create(hlsStream).Error
require.NoError(t, err)
transcodeService := NewHLSTranscodeService("/tmp", logger)
service := NewHLSServiceWithTranscode(db, "/tmp", transcodeService, logger)
ctx := context.Background()
err = service.TriggerTranscode(ctx, track)
assert.Error(t, err)
assert.Contains(t, err.Error(), "already exists and is ready")
}
func TestHLSService_TriggerTranscode_AlreadyProcessing(t *testing.T) {
logger := zaptest.NewLogger(t)
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.HLSStream{})
require.NoError(t, err)
userID := uuid.New()
// Create test user
user := &models.User{
ID: userID,
Username: "testuser",
Email: "test@example.com",
IsActive: true,
}
err = db.Create(user).Error
require.NoError(t, err)
// Create test track
track := &models.Track{
UserID: userID,
Title: "Test Track",
FilePath: "/test/track.mp3",
FileSize: 1024,
Format: "mp3",
Duration: 180,
Status: models.TrackStatusProcessing,
}
err = db.Create(track).Error
require.NoError(t, err)
// Create existing HLS stream with processing status
hlsStream := &models.HLSStream{
TrackID: track.ID,
Status: models.HLSStatusProcessing,
}
err = db.Create(hlsStream).Error
require.NoError(t, err)
transcodeService := NewHLSTranscodeService("/tmp", logger)
service := NewHLSServiceWithTranscode(db, "/tmp", transcodeService, logger)
ctx := context.Background()
err = service.TriggerTranscode(ctx, track)
assert.Error(t, err)
assert.Contains(t, err.Error(), "already being processed")
}