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") }