467 lines
14 KiB
Python
467 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_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 structure
|
|
assert "is_running" in data
|
|
assert "statistics" in data
|
|
|
|
status = data["status"]
|
|
assert "active" in status
|
|
assert "pending" in status
|
|
assert "completed" in status
|
|
assert "failed" 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()
|
|
pending = data["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()["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_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()["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()["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()["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()["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["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["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"]
|
|
|
|
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"]) == 0
|