Files
music-agregator/testing/e2e/download_test.go
T

459 lines
12 KiB
Go

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