- Updated DownloadRequest and DownloadItem models with comprehensive docstrings explaining serie_id (key as primary identifier) vs serie_folder (filesystem metadata) - Updated add_to_queue() endpoint docstring to document request parameters - Updated all test files to include required serie_folder field: - tests/api/test_download_endpoints.py - tests/api/test_queue_features.py - tests/frontend/test_existing_ui_integration.py - tests/integration/test_download_flow.py - Updated infrastructure.md with Download Queue request/response models - All 869 tests pass This is part of the Series Identifier Standardization effort (Phase 4.2) to ensure key is used as the primary identifier throughout the codebase.
471 lines
14 KiB
Python
471 lines
14 KiB
Python
"""Tests for queue management features.
|
|
|
|
This module tests the queue page functionality including:
|
|
- Display of queued items in organized lists
|
|
- Drag-and-drop reordering
|
|
- Starting and stopping queue processing
|
|
- Filtering completed and failed downloads
|
|
"""
|
|
import pytest
|
|
from httpx import ASGITransport, AsyncClient
|
|
|
|
from src.server.fastapi_app import app
|
|
|
|
|
|
@pytest.fixture
|
|
async def client():
|
|
"""Create an async test client."""
|
|
transport = ASGITransport(app=app)
|
|
async with AsyncClient(
|
|
transport=transport, base_url="http://test"
|
|
) as client:
|
|
yield client
|
|
|
|
|
|
@pytest.fixture
|
|
async def auth_headers(client: AsyncClient):
|
|
"""Get authentication headers with valid JWT token."""
|
|
# Setup auth
|
|
await client.post(
|
|
"/api/auth/setup",
|
|
json={"master_password": "TestPass123!"}
|
|
)
|
|
|
|
# Login
|
|
response = await client.post(
|
|
"/api/auth/login",
|
|
json={"password": "TestPass123!"}
|
|
)
|
|
|
|
data = response.json()
|
|
token = data["access_token"]
|
|
|
|
return {"Authorization": f"Bearer {token}"}
|
|
|
|
|
|
@pytest.fixture
|
|
def sample_download_request():
|
|
"""Sample download request for testing."""
|
|
return {
|
|
"serie_id": "test-series",
|
|
"serie_folder": "Test Series (2024)",
|
|
"serie_name": "Test Series",
|
|
"episodes": [
|
|
{"season": 1, "episode": 1},
|
|
{"season": 1, "episode": 2}
|
|
],
|
|
"priority": "normal"
|
|
}
|
|
|
|
|
|
class TestQueueDisplay:
|
|
"""Test queue display and organization."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_queue_status_includes_all_sections(
|
|
self, client: AsyncClient, auth_headers: dict
|
|
):
|
|
"""Test queue status includes all sections."""
|
|
response = await client.get(
|
|
"/api/queue/status",
|
|
headers=auth_headers
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
|
|
# Verify top-level structure
|
|
assert "status" in data
|
|
assert "statistics" in data
|
|
|
|
# Verify status nested structure
|
|
status = data["status"]
|
|
assert "active_downloads" in status
|
|
assert "pending_queue" in status
|
|
assert "completed_downloads" in status
|
|
assert "failed_downloads" in status
|
|
assert "is_running" in status
|
|
assert "is_paused" in status
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_queue_items_have_required_fields(
|
|
self, client: AsyncClient, auth_headers: dict,
|
|
sample_download_request: dict
|
|
):
|
|
"""Test queue items have required display fields."""
|
|
# Add an item to the queue
|
|
add_response = await client.post(
|
|
"/api/queue/add",
|
|
json=sample_download_request,
|
|
headers=auth_headers
|
|
)
|
|
assert add_response.status_code == 201
|
|
|
|
# Get queue status
|
|
response = await client.get(
|
|
"/api/queue/status",
|
|
headers=auth_headers
|
|
)
|
|
assert response.status_code == 200
|
|
|
|
data = response.json()
|
|
# Updated for nested status structure
|
|
pending = data["status"]["pending_queue"]
|
|
|
|
assert len(pending) > 0
|
|
item = pending[0]
|
|
|
|
# Verify required fields for display
|
|
assert "id" in item
|
|
assert "serie_name" in item
|
|
assert "episode" in item
|
|
assert "priority" in item
|
|
assert "added_at" in item
|
|
|
|
# Verify episode structure
|
|
episode = item["episode"]
|
|
assert "season" in episode
|
|
assert "episode" in episode
|
|
|
|
|
|
class TestQueueReordering:
|
|
"""Test queue reordering functionality."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_reorder_queue_with_item_ids(
|
|
self, client: AsyncClient, auth_headers: dict
|
|
):
|
|
"""Test reordering queue using item_ids array."""
|
|
# Clear existing queue first
|
|
status_response = await client.get(
|
|
"/api/queue/status",
|
|
headers=auth_headers
|
|
)
|
|
existing_items = [
|
|
item["id"]
|
|
for item in status_response.json()["status"]["pending_queue"]
|
|
]
|
|
if existing_items:
|
|
await client.request(
|
|
"DELETE",
|
|
"/api/queue/",
|
|
json={"item_ids": existing_items},
|
|
headers=auth_headers
|
|
)
|
|
|
|
# Add exactly 3 items
|
|
added_ids = []
|
|
for i in range(3):
|
|
response = await client.post(
|
|
"/api/queue/add",
|
|
json={
|
|
"serie_id": f"test-{i}",
|
|
"serie_folder": f"Test Series {i} (2024)",
|
|
"serie_name": f"Test Series {i}",
|
|
"episodes": [{"season": 1, "episode": i+1}],
|
|
"priority": "normal"
|
|
},
|
|
headers=auth_headers
|
|
)
|
|
if response.status_code == 201:
|
|
data = response.json()
|
|
if "added_items" in data and data["added_items"]:
|
|
added_ids.extend(data["added_items"])
|
|
|
|
assert len(added_ids) == 3, f"Expected 3 items, got {len(added_ids)}"
|
|
|
|
# Reverse the order
|
|
new_order = list(reversed(added_ids))
|
|
|
|
# Reorder
|
|
reorder_response = await client.post(
|
|
"/api/queue/reorder",
|
|
json={"item_ids": new_order},
|
|
headers=auth_headers
|
|
)
|
|
|
|
assert reorder_response.status_code == 200
|
|
assert reorder_response.json()["status"] == "success"
|
|
|
|
# Verify new order
|
|
status_response = await client.get(
|
|
"/api/queue/status",
|
|
headers=auth_headers
|
|
)
|
|
current_order = [
|
|
item["id"]
|
|
for item in status_response.json()["status"]["pending_queue"]
|
|
]
|
|
|
|
assert current_order == new_order
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_reorder_with_invalid_ids(
|
|
self, client: AsyncClient, auth_headers: dict
|
|
):
|
|
"""Test reordering with non-existent IDs succeeds (idempotent)."""
|
|
response = await client.post(
|
|
"/api/queue/reorder",
|
|
json={"item_ids": ["invalid-id-1", "invalid-id-2"]},
|
|
headers=auth_headers
|
|
)
|
|
|
|
# Bulk reorder is idempotent and succeeds even with invalid IDs
|
|
# It just ignores items that don't exist
|
|
assert response.status_code == 200
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_reorder_empty_list(
|
|
self, client: AsyncClient, auth_headers: dict
|
|
):
|
|
"""Test reordering with empty list."""
|
|
response = await client.post(
|
|
"/api/queue/reorder",
|
|
json={"item_ids": []},
|
|
headers=auth_headers
|
|
)
|
|
|
|
# Should succeed but do nothing
|
|
assert response.status_code in [200, 404]
|
|
|
|
|
|
class TestQueueControl:
|
|
"""Test queue start/stop functionality."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_start_queue(
|
|
self, client: AsyncClient, auth_headers: dict
|
|
):
|
|
"""Test starting the download queue."""
|
|
response = await client.post(
|
|
"/api/queue/start",
|
|
headers=auth_headers
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["status"] == "success"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_stop_queue(
|
|
self, client: AsyncClient, auth_headers: dict
|
|
):
|
|
"""Test stopping the download queue."""
|
|
# Start first
|
|
await client.post("/api/queue/start", headers=auth_headers)
|
|
|
|
# Then stop
|
|
response = await client.post(
|
|
"/api/queue/stop",
|
|
headers=auth_headers
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["status"] == "success"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_queue_status_reflects_running_state(
|
|
self, client: AsyncClient, auth_headers: dict
|
|
):
|
|
"""Test queue status reflects running state."""
|
|
# Initially not running
|
|
status = await client.get(
|
|
"/api/queue/status",
|
|
headers=auth_headers
|
|
)
|
|
assert status.json()["status"]["is_running"] is False
|
|
|
|
# Start queue
|
|
await client.post("/api/queue/start", headers=auth_headers)
|
|
|
|
# Should be running
|
|
status = await client.get(
|
|
"/api/queue/status",
|
|
headers=auth_headers
|
|
)
|
|
assert status.json()["status"]["is_running"] is True
|
|
|
|
# Stop queue
|
|
await client.post("/api/queue/stop", headers=auth_headers)
|
|
|
|
# Should not be running
|
|
status = await client.get(
|
|
"/api/queue/status",
|
|
headers=auth_headers
|
|
)
|
|
assert status.json()["status"]["is_running"] is False
|
|
|
|
|
|
class TestCompletedDownloads:
|
|
"""Test completed downloads management."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_clear_completed_downloads(
|
|
self, client: AsyncClient, auth_headers: dict
|
|
):
|
|
"""Test clearing completed downloads."""
|
|
response = await client.delete(
|
|
"/api/queue/completed",
|
|
headers=auth_headers
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert "count" in data
|
|
assert data["status"] == "success"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_completed_section_count(
|
|
self, client: AsyncClient, auth_headers: dict
|
|
):
|
|
"""Test that completed count is accurate."""
|
|
status = await client.get(
|
|
"/api/queue/status",
|
|
headers=auth_headers
|
|
)
|
|
data = status.json()
|
|
|
|
completed_count = data["statistics"]["completed_count"]
|
|
completed_list = len(data["status"]["completed_downloads"])
|
|
|
|
# Count should match list length
|
|
assert completed_count == completed_list
|
|
|
|
|
|
class TestFailedDownloads:
|
|
"""Test failed downloads management."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_clear_failed_downloads(
|
|
self, client: AsyncClient, auth_headers: dict
|
|
):
|
|
"""Test clearing failed downloads."""
|
|
response = await client.delete(
|
|
"/api/queue/failed",
|
|
headers=auth_headers
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert "count" in data
|
|
assert data["status"] == "success"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_retry_failed_downloads(
|
|
self, client: AsyncClient, auth_headers: dict
|
|
):
|
|
"""Test retrying failed downloads."""
|
|
response = await client.post(
|
|
"/api/queue/retry",
|
|
json={"item_ids": []},
|
|
headers=auth_headers
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert "retried_count" in data
|
|
assert data["status"] == "success"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_retry_specific_failed_download(
|
|
self, client: AsyncClient, auth_headers: dict
|
|
):
|
|
"""Test retrying a specific failed download."""
|
|
# Test the endpoint accepts the format
|
|
response = await client.post(
|
|
"/api/queue/retry",
|
|
json={"item_ids": ["some-id"]},
|
|
headers=auth_headers
|
|
)
|
|
|
|
# Should succeed even if ID doesn't exist (idempotent)
|
|
assert response.status_code == 200
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_failed_section_count(
|
|
self, client: AsyncClient, auth_headers: dict
|
|
):
|
|
"""Test that failed count is accurate."""
|
|
status = await client.get(
|
|
"/api/queue/status",
|
|
headers=auth_headers
|
|
)
|
|
data = status.json()
|
|
|
|
failed_count = data["statistics"]["failed_count"]
|
|
failed_list = len(data["status"]["failed_downloads"])
|
|
|
|
# Count should match list length
|
|
assert failed_count == failed_list
|
|
|
|
|
|
class TestBulkOperations:
|
|
"""Test bulk queue operations."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_remove_multiple_items(
|
|
self, client: AsyncClient, auth_headers: dict
|
|
):
|
|
"""Test removing multiple items from queue."""
|
|
# Add multiple items
|
|
item_ids = []
|
|
for i in range(3):
|
|
add_response = await client.post(
|
|
"/api/queue/add",
|
|
json={
|
|
"serie_id": f"bulk-test-{i}",
|
|
"serie_name": f"Bulk Test {i}",
|
|
"episodes": [{"season": 1, "episode": i+1}],
|
|
"priority": "normal"
|
|
},
|
|
headers=auth_headers
|
|
)
|
|
if add_response.status_code == 201:
|
|
data = add_response.json()
|
|
if "added_items" in data and len(data["added_items"]) > 0:
|
|
item_ids.append(data["added_items"][0])
|
|
|
|
# Remove all at once
|
|
if item_ids:
|
|
response = await client.request(
|
|
"DELETE",
|
|
"/api/queue/",
|
|
json={"item_ids": item_ids},
|
|
headers=auth_headers
|
|
)
|
|
|
|
assert response.status_code == 204
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_clear_entire_pending_queue(
|
|
self, client: AsyncClient, auth_headers: dict
|
|
):
|
|
"""Test clearing entire pending queue."""
|
|
# Get all pending items
|
|
status = await client.get(
|
|
"/api/queue/status",
|
|
headers=auth_headers
|
|
)
|
|
pending = status.json()["status"]["pending_queue"]
|
|
|
|
if pending:
|
|
item_ids = [item["id"] for item in pending]
|
|
|
|
# Remove all
|
|
response = await client.request(
|
|
"DELETE",
|
|
"/api/queue/",
|
|
json={"item_ids": item_ids},
|
|
headers=auth_headers
|
|
)
|
|
|
|
assert response.status_code == 204
|
|
|
|
# Verify queue is empty
|
|
status = await client.get(
|
|
"/api/queue/status",
|
|
headers=auth_headers
|
|
)
|
|
assert len(status.json()["status"]["pending_queue"]) == 0
|