459 lines
12 KiB
Go
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)
|
|
}
|