// Package e2e contains end-to-end tests for the music aggregator. // // This file covers Section 4 of FLOWS.md: Download Tracking // - 4.1 Track Active Downloads // - 4.2 Completed Download Handling // - 4.3 Failed Download Handling // - 4.4 Download Queue Management package e2e import ( "context" "testing" "time" "github.com/fujin/music-agregator/testing/e2e/testutil" ) // TestTrackActiveDownloads_Flow covers section 4.1 of FLOWS.md: // 1. Poll torrent client for status of all active downloads // 2. Match against download_queue entries by torrent_hash // 3. Update: progress, size_left, status // 4. Detect state transitions: queued → downloading → seeding → completed func TestTrackActiveDownloads_Flow(t *testing.T) { env := testutil.NewTestEnv(t) defer env.Close() ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) defer cancel() if err := env.CleanupDownloadQueue(ctx); err != nil { t.Fatalf("cleanup failed: %v", err) } t.Cleanup(func() { env.CleanupDownloadQueue(context.Background()) }) t.Run("Step1_ListDownloadQueue", func(t *testing.T) { resp, err := env.GET("/api/queue") if err != nil { t.Fatalf("request failed: %v", err) } resp.AssertStatus(t, 200) var result struct { Items []struct { ID string `json:"id"` Title string `json:"title"` Status string `json:"status"` Progress float64 `json:"progress"` TorrentHash *string `json:"torrent_hash"` } `json:"items"` Total int `json:"total"` } if err := resp.DecodeJSON(&result); err != nil { t.Fatalf("failed to decode response: %v", err) } t.Logf("queue has %d items", result.Total) }) t.Run("Step2_AddToQueue", func(t *testing.T) { resp, err := env.POST("/api/queue", map[string]any{ "title": "Test Album - FLAC", "torrent_hash": "abc123def456", "size": 500000000, "indexer": "test-indexer", }) if err != nil { t.Fatalf("request failed: %v", err) } resp.AssertStatus(t, 200) var result struct { ID string `json:"id"` Title string `json:"title"` Status string `json:"status"` TorrentHash string `json:"torrent_hash"` } if err := resp.DecodeJSON(&result); err != nil { t.Fatalf("failed to decode response: %v", err) } if result.ID == "" { t.Error("expected non-empty ID") } if result.Status != "queued" { t.Errorf("expected status=queued, got %s", result.Status) } }) t.Run("Step3_SyncQueueWithTorrentClient", func(t *testing.T) { resp, err := env.POST("/api/queue/sync", nil) if err != nil { t.Fatalf("request failed: %v", err) } resp.AssertStatus(t, 200) var result struct { Synced int `json:"synced"` Updated int `json:"updated"` } if err := resp.DecodeJSON(&result); err != nil { t.Fatalf("failed to decode response: %v", err) } t.Logf("synced %d items, updated %d", result.Synced, result.Updated) }) t.Run("Step4_GetQueueItem", func(t *testing.T) { listResp, err := env.GET("/api/queue") if err != nil { t.Fatalf("request failed: %v", err) } var listResult struct { Items []struct { ID string `json:"id"` } `json:"items"` } if err := listResp.DecodeJSON(&listResult); err != nil { t.Fatalf("failed to decode response: %v", err) } if len(listResult.Items) == 0 { t.Skip("no items in queue") } itemID := listResult.Items[0].ID resp, err := env.GET("/api/queue/" + itemID) if err != nil { t.Fatalf("request failed: %v", err) } resp.AssertStatus(t, 200) var item struct { ID string `json:"id"` Title string `json:"title"` Status string `json:"status"` Progress float64 `json:"progress"` Size int64 `json:"size"` SizeLeft int64 `json:"size_left"` } if err := resp.DecodeJSON(&item); err != nil { t.Fatalf("failed to decode response: %v", err) } if item.ID != itemID { t.Errorf("expected id=%s, got %s", itemID, item.ID) } }) } // TestCompletedDownloadHandling_Flow covers section 4.2 of FLOWS.md: // 1. Detect download_queue entry where torrent reports completed/seeding // 2. Mark download_queue.status = completed, set completed_at // 3. Remove from wanted_albums func TestCompletedDownloadHandling_Flow(t *testing.T) { env := testutil.NewTestEnv(t) defer env.Close() ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) defer cancel() if err := env.CleanupDownloadQueue(ctx); err != nil { t.Fatalf("cleanup failed: %v", err) } if err := env.CleanupWantedAlbums(ctx); err != nil { t.Fatalf("cleanup wanted_albums failed: %v", err) } t.Cleanup(func() { env.CleanupDownloadQueue(context.Background()) env.CleanupWantedAlbums(context.Background()) }) t.Run("Step1_MarkDownloadCompleted", func(t *testing.T) { addResp, err := env.POST("/api/queue", map[string]any{ "title": "Completed Album - FLAC", "torrent_hash": "completed123", "size": 100000000, }) if err != nil { t.Fatalf("request failed: %v", err) } addResp.AssertStatus(t, 200) var addResult struct { ID string `json:"id"` } if err := addResp.DecodeJSON(&addResult); err != nil { t.Fatalf("failed to decode response: %v", err) } resp, err := env.PUT("/api/queue/"+addResult.ID, map[string]any{ "status": "completed", }) if err != nil { t.Fatalf("request failed: %v", err) } resp.AssertStatus(t, 200) var result struct { Status string `json:"status"` CompletedAt *string `json:"completed_at"` } if err := resp.DecodeJSON(&result); err != nil { t.Fatalf("failed to decode response: %v", err) } if result.Status != "completed" { t.Errorf("expected status=completed, got %s", result.Status) } if result.CompletedAt == nil { t.Error("expected completed_at to be set") } }) } // TestFailedDownloadHandling_Flow covers section 4.3 of FLOWS.md: // 1. Detect download failure (torrent client reports error) // 2. Mark download_queue.status = failed, set error_message // 3. Add release to blocklist (source_title, torrent_hash, indexer, quality) // 4. Re-add album to wanted_albums for retry search func TestFailedDownloadHandling_Flow(t *testing.T) { env := testutil.NewTestEnv(t) defer env.Close() ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) defer cancel() if err := env.CleanupDownloadQueue(ctx); err != nil { t.Fatalf("cleanup failed: %v", err) } if err := env.CleanupBlocklist(ctx); err != nil { t.Fatalf("cleanup blocklist failed: %v", err) } t.Cleanup(func() { env.CleanupDownloadQueue(context.Background()) env.CleanupBlocklist(context.Background()) }) t.Run("Step1_MarkDownloadFailed", func(t *testing.T) { addResp, err := env.POST("/api/queue", map[string]any{ "title": "Failed Album - FLAC", "torrent_hash": "failed456", "size": 100000000, "indexer": "test-indexer", }) if err != nil { t.Fatalf("request failed: %v", err) } addResp.AssertStatus(t, 200) var addResult struct { ID string `json:"id"` } if err := addResp.DecodeJSON(&addResult); err != nil { t.Fatalf("failed to decode response: %v", err) } resp, err := env.PUT("/api/queue/"+addResult.ID, map[string]any{ "status": "failed", "error_message": "Tracker returned error: torrent not found", }) if err != nil { t.Fatalf("request failed: %v", err) } resp.AssertStatus(t, 200) var result struct { Status string `json:"status"` ErrorMessage string `json:"error_message"` } if err := resp.DecodeJSON(&result); err != nil { t.Fatalf("failed to decode response: %v", err) } if result.Status != "failed" { t.Errorf("expected status=failed, got %s", result.Status) } if result.ErrorMessage == "" { t.Error("expected error_message to be set") } }) t.Run("Step2_BlocklistAndRemove", func(t *testing.T) { addResp, err := env.POST("/api/queue", map[string]any{ "title": "To Blocklist Album", "torrent_hash": "blocklist789", "size": 100000000, "indexer": "test-indexer", }) if err != nil { t.Fatalf("request failed: %v", err) } addResp.AssertStatus(t, 200) var addResult struct { ID string `json:"id"` } if err := addResp.DecodeJSON(&addResult); err != nil { t.Fatalf("failed to decode response: %v", err) } resp, err := env.POST("/api/queue/"+addResult.ID+"/blocklist", nil) if err != nil { t.Fatalf("request failed: %v", err) } resp.AssertStatus(t, 200) var result struct { Blocklisted bool `json:"blocklisted"` Removed bool `json:"removed"` } if err := resp.DecodeJSON(&result); err != nil { t.Fatalf("failed to decode response: %v", err) } if !result.Removed { t.Error("expected item to be removed from queue") } getResp, err := env.GET("/api/queue/" + addResult.ID) if err != nil { t.Fatalf("request failed: %v", err) } if getResp.StatusCode != 404 { t.Error("expected item to return 404 after removal") } }) } // TestDownloadQueueManagement_Flow covers section 4.4 of FLOWS.md: // 1. List all download_queue entries with status and progress // 2. Remove entry (cancel download in torrent client) // 3. Blocklist and remove (add to blocklist, cancel, re-search) func TestDownloadQueueManagement_Flow(t *testing.T) { env := testutil.NewTestEnv(t) defer env.Close() ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) defer cancel() if err := env.CleanupDownloadQueue(ctx); err != nil { t.Fatalf("cleanup failed: %v", err) } t.Cleanup(func() { env.CleanupDownloadQueue(context.Background()) }) t.Run("Step1_AddMultipleItems", func(t *testing.T) { for i := 0; i < 3; i++ { resp, err := env.POST("/api/queue", map[string]any{ "title": "Queue Item " + string(rune('A'+i)), "torrent_hash": "hash" + string(rune('a'+i)), "size": 100000000 * (i + 1), }) if err != nil { t.Fatalf("request failed: %v", err) } resp.AssertStatus(t, 200) } }) t.Run("Step2_ListWithFilters", func(t *testing.T) { resp, err := env.GET("/api/queue?status=queued") if err != nil { t.Fatalf("request failed: %v", err) } resp.AssertStatus(t, 200) var result struct { Items []struct { Status string `json:"status"` } `json:"items"` Total int `json:"total"` } if err := resp.DecodeJSON(&result); err != nil { t.Fatalf("failed to decode response: %v", err) } for _, item := range result.Items { if item.Status != "queued" { t.Errorf("expected all items to have status=queued, got %s", item.Status) } } }) t.Run("Step3_RemoveFromQueue", func(t *testing.T) { listResp, err := env.GET("/api/queue") if err != nil { t.Fatalf("request failed: %v", err) } var listResult struct { Items []struct { ID string `json:"id"` } `json:"items"` } if err := listResp.DecodeJSON(&listResult); err != nil { t.Fatalf("failed to decode response: %v", err) } if len(listResult.Items) == 0 { t.Skip("no items in queue") } itemID := listResult.Items[0].ID countBefore, _ := env.CountDownloadQueue(ctx) resp, err := env.DELETE("/api/queue/" + itemID) if err != nil { t.Fatalf("request failed: %v", err) } resp.AssertStatus(t, 200) countAfter, _ := env.CountDownloadQueue(ctx) if countAfter >= countBefore { t.Error("expected queue count to decrease") } }) t.Run("Step4_QueueStats", func(t *testing.T) { resp, err := env.GET("/api/queue/stats") if err != nil { t.Fatalf("request failed: %v", err) } resp.AssertStatus(t, 200) var result struct { Total int `json:"total"` Downloading int `json:"downloading"` Queued int `json:"queued"` Completed int `json:"completed"` Failed int `json:"failed"` } if err := resp.DecodeJSON(&result); err != nil { t.Fatalf("failed to decode response: %v", err) } t.Logf("queue stats: total=%d, downloading=%d, queued=%d, completed=%d, failed=%d", result.Total, result.Downloading, result.Queued, result.Completed, result.Failed) }) } func TestDownloadQueue_NotFound(t *testing.T) { env := testutil.NewTestEnv(t) defer env.Close() resp, err := env.GET("/api/queue/00000000-0000-0000-0000-000000000000") if err != nil { t.Fatalf("request failed: %v", err) } resp.AssertStatus(t, 404) }