feat: add download tracking endpoints (sections 4.1, 4.2, 4.3, 4.4)
This commit is contained in:
@@ -0,0 +1,458 @@
|
||||
// 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)
|
||||
}
|
||||
Reference in New Issue
Block a user