package queue_test import ( "log/slog" "testing" "time" "gobuz/internal/downloader" "gobuz/internal/models" "gobuz/internal/testutil" "gobuz/internal/web/pages" "gobuz/internal/web/queue" ) func newTestManager(broker *queue.SSEBroker, p *pages.Pages) *queue.Manager { return queue.NewManager(broker, testutil.NewTestDownloader(), p, slog.New(slog.DiscardHandler)) } func TestManager_StageRelease_EnqueuesAndTracksRelease(t *testing.T) { broker := queue.NewSSEBroker() p := testutil.NewTestPages() mgr := newTestManager(broker, p) if mgr.IsQueued(0, "alb-123") { t.Fatal("expected alb-123 to not be queued initially") } mgr.StageRelease(0, models.QueueItem{ Release: models.Release{ ID: "alb-123", Title: "Test Release", ArtistName: "Test Artist", UPC: "0123456789", }, }) if !mgr.IsQueued(0, "alb-123") { t.Fatal("expected alb-123 to be queued after StageRelease") } vm := mgr.GetViewModel(0) if len(vm.Items) != 1 || vm.StagedCount != 1 { t.Fatalf("expected 1 staged item, got %d (staged: %d)", len(vm.Items), vm.StagedCount) } mgr.RemoveRelease(0, "alb-123") if mgr.IsQueued(0, "alb-123") { t.Fatal("expected alb-123 to not be queued after RemoveRelease") } } func TestManager_Update_UpdatesProgressAndStatus(t *testing.T) { broker := queue.NewSSEBroker() p := testutil.NewTestPages() mgr := newTestManager(broker, p) uq := mgr.GetUserQueue(1) uq.SetProcessing(true) // Stage an album first mgr.StageRelease(1, models.QueueItem{ Release: models.Release{ ID: "alb-100", Title: "Test Album", ArtistName: "Test Artist", }, }) // Simulate registration of a track uq.OnAlbum(downloader.MsgAlbum{AlbumID: "alb-100", Title: "Test Album", Artist: "Test Artist"}) uq.OnRegisterTrack(downloader.MsgRegisterTrack{AlbumID: "alb-100", ID: "101", Num: 1, Name: "Track One"}) uq.OnSetTotal(downloader.MsgSetTotal{ID: "101", Total: 1000}) vm := mgr.GetViewModel(1) if len(vm.Items) != 1 { t.Fatalf("expected 1 item in queue, got %d", len(vm.Items)) } if vm.Items[0].Title != "Test Album" { t.Errorf("expected album title Test Album, got %s", vm.Items[0].Title) } if len(vm.Items[0].Tracks) != 1 || vm.Items[0].Tracks[0].Title != "Track One" { t.Errorf("expected 1 track with title Track One in album") } // Mark done uq.OnDone(downloader.MsgDone{ID: "101"}) vm = mgr.GetViewModel(1) if vm.Items[0].Status != models.StatusDone { t.Errorf("expected status done, got %s", vm.Items[0].Status) } } func TestManager_StageRelease_DuringActiveDownload_RemainsStaged(t *testing.T) { broker := queue.NewSSEBroker() p := testutil.NewTestPages() mgr := newTestManager(broker, p) uq := mgr.GetUserQueue(1) uq.SetProcessing(true) // Stage Album A and start downloading mgr.StageRelease(1, models.QueueItem{ Release: models.Release{ ID: "album-a", Title: "Album A", ArtistName: "Mount Kimbie", }, }) uq.OnAlbum(downloader.MsgAlbum{AlbumID: "album-a", Title: "Album A", Artist: "Mount Kimbie"}) // While Album A is downloading, user stages Album B (same artist) mgr.StageRelease(1, models.QueueItem{ Release: models.Release{ ID: "album-b", Title: "Album B", ArtistName: "Mount Kimbie", }, }) uq.OnRegisterTrack(downloader.MsgRegisterTrack{AlbumID: "album-a", ID: "track-a1", Num: 1, Name: "Track A1"}) vm := mgr.GetViewModel(1) if len(vm.Items) != 2 { t.Fatalf("expected 2 items in queue, got %d", len(vm.Items)) } albumA := vm.Items[0] albumB := vm.Items[1] if len(albumA.Tracks) != 1 || albumA.Tracks[0].ID != "track-a1" { t.Errorf("Album A should have track-a1, got tracks: %+v", albumA.Tracks) } if len(albumB.Tracks) != 0 { t.Errorf("Album B should have 0 tracks, got %d tracks", len(albumB.Tracks)) } } func TestManager_UserIsolation_MaintainsSeparateQueuesPerUser(t *testing.T) { broker := queue.NewSSEBroker() p := testutil.NewTestPages() mgr := newTestManager(broker, p) // User 1 stages album mgr.StageRelease(1, models.QueueItem{ Release: models.Release{ ID: "user1-album", Title: "Alice's Album", ArtistName: "Alice Artist", }, }) // User 2 stages different album mgr.StageRelease(2, models.QueueItem{ Release: models.Release{ ID: "user2-album", Title: "Bob's Album", ArtistName: "Bob Artist", }, }) if !mgr.IsQueued(1, "user1-album") || mgr.IsQueued(2, "user1-album") { t.Error("user1-album should be queued for User 1, but not for User 2") } if !mgr.IsQueued(2, "user2-album") || mgr.IsQueued(1, "user2-album") { t.Error("user2-album should be queued for User 2, but not for User 1") } vm1 := mgr.GetViewModel(1) vm2 := mgr.GetViewModel(2) if len(vm1.Items) != 1 || vm1.Items[0].ID != "user1-album" { t.Errorf("User 1 view model invalid: %+v", vm1) } if len(vm2.Items) != 1 || vm2.Items[0].ID != "user2-album" { t.Errorf("User 2 view model invalid: %+v", vm2) } } func TestManager_OnComplete_TriggersCallbackWhenQueueDrains(t *testing.T) { broker := queue.NewSSEBroker() p := testutil.NewTestPages() dl := testutil.NewTestDownloader() mgr := queue.NewManager(broker, dl, p, slog.New(slog.DiscardHandler)) completed := make(chan struct{}) mgr.SetOnComplete(func() { close(completed) }) mgr.StageRelease(1, models.QueueItem{ Release: models.Release{ ID: "alb_finish_1", Title: "Finished Album", ArtistName: "Test Artist", TracksCount: 0, }, }) mgr.StartDownloads(1) select { case <-completed: case <-time.After(2 * time.Second): t.Fatal("timed out waiting for onComplete callback") } completed2 := make(chan struct{}) mgr.SetOnComplete(func() { close(completed2) }) // User 2 queue is created after SetOnComplete mgr.StageRelease(2, models.QueueItem{ Release: models.Release{ ID: "alb_finish_2", Title: "Finished Album 2", ArtistName: "Test Artist", TracksCount: 0, }, }) mgr.StartDownloads(2) select { case <-completed2: case <-time.After(2 * time.Second): t.Fatal("timed out waiting for onComplete callback for newly created queue") } } func TestManager_ProcessingState_TracksActiveState(t *testing.T) { broker := queue.NewSSEBroker() p := testutil.NewTestPages() mgr := newTestManager(broker, p) if mgr.IsAnyProcessing() { t.Errorf("expected IsAnyProcessing false initially") } if mgr.IsProcessing(1) { t.Errorf("expected IsProcessing false for user 1 initially") } mgr.GetUserQueue(1).SetProcessing(true) if !mgr.IsAnyProcessing() { t.Errorf("expected IsAnyProcessing true after setting user 1 processing") } if !mgr.IsProcessing(1) { t.Errorf("expected IsProcessing true for user 1") } if mgr.IsProcessing(2) { t.Errorf("expected IsProcessing false for user 2") } mgr.GetUserQueue(1).SetProcessing(false) if mgr.IsAnyProcessing() { t.Errorf("expected IsAnyProcessing false after clearing user 1") } }