Aniworld/tests/api/test_queue_features.py
Lukas f4d14cf17e Task 4.3: Verify queue API endpoints use key identifier
- Verified queue API endpoints already use 'serie_id' (key) as primary identifier
- Updated test fixtures to use explicit key values (e.g., 'test-series-key')
- Added test to verify queue items include serie_id (key) and serie_folder (metadata)
- Fixed test_queue_items_have_required_fields to find correct item by ID
- Added test_queue_item_uses_key_as_identifier for explicit key verification
- Updated instructions.md to mark Task 4.3 as complete

All 870 tests pass.
2025-11-27 19:46:49 +01:00

544 lines
17 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.
Note: serie_id is the primary identifier (key) used for all lookups.
serie_folder is metadata only used for filesystem operations.
"""
return {
"serie_id": "test-series-key", # Provider key (primary identifier)
"serie_folder": "Test Series (2024)", # Filesystem folder (metadata)
"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 the added item IDs from response
add_data = add_response.json()
added_ids = add_data.get("added_items", [])
assert len(added_ids) > 0, "No items were added"
# 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
# Find the item we just added by ID
item = next((i for i in pending if i["id"] in added_ids), None)
assert item is not None, f"Could not find added item in queue. Added IDs: {added_ids}"
# Verify required fields for display
assert "id" in item
assert "serie_id" in item # Key - primary identifier
assert "serie_folder" in item # Metadata for filesystem ops
assert "serie_name" in item
assert "episode" in item
assert "priority" in item
assert "added_at" in item
# Verify serie_id (key) matches what we sent
assert item["serie_id"] == sample_download_request["serie_id"]
# Verify episode structure
episode = item["episode"]
assert "season" in episode
assert "episode" in episode
@pytest.mark.asyncio
async def test_queue_item_uses_key_as_identifier(
self, client: AsyncClient, auth_headers: dict
):
"""Test that queue items use serie_id (key) as primary identifier.
Verifies that:
- serie_id is the provider-assigned key (URL-safe identifier)
- serie_folder is metadata only (not used for identification)
- Both fields are present in queue item responses
"""
# Add an item with explicit key and folder
request = {
"serie_id": "my-test-anime-key", # Provider key (primary ID)
"serie_folder": "My Test Anime (2024)", # Display name/folder
"serie_name": "My Test Anime",
"episodes": [{"season": 1, "episode": 1}],
"priority": "normal"
}
add_response = await client.post(
"/api/queue/add",
json=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()
pending = data["status"]["pending_queue"]
# Find our item by key
matching_items = [
item for item in pending
if item["serie_id"] == "my-test-anime-key"
]
assert len(matching_items) >= 1, "Item should be findable by key"
item = matching_items[0]
# Verify key is used as identifier
assert item["serie_id"] == "my-test-anime-key"
# Verify folder is preserved as metadata
assert item["serie_folder"] == "My Test Anime (2024)"
# Verify serie_name is also present
assert item["serie_name"] == "My Test Anime"
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"reorder-test-key-{i}", # Key (primary ID)
"serie_folder": f"Reorder Test {i} (2024)", # Metadata
"serie_name": f"Reorder Test {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-key-{i}", # Key (primary ID)
"serie_folder": f"Bulk Test {i} (2024)", # Metadata
"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