Aniworld/tests/integration/test_identifier_consistency.py
Lukas b0f3b643c7 Migrate download queue from JSON to SQLite database
- Created QueueRepository adapter in src/server/services/queue_repository.py
- Refactored DownloadService to use repository pattern instead of JSON
- Updated application startup to initialize download service from database
- Updated all test fixtures to use MockQueueRepository
- All 1104 tests passing
2025-12-02 16:01:25 +01:00

518 lines
17 KiB
Python

"""Integration tests for series identifier consistency.
This module verifies that the 'key' identifier is used consistently
across all layers of the application (API, services, database, WebSocket).
The identifier standardization ensures:
- 'key' is the primary identifier (provider-assigned, URL-safe)
- 'folder' is metadata only (not used for lookups)
- Consistent identifier usage throughout the codebase
"""
import asyncio
from datetime import datetime, timezone
from typing import Any, Dict, List
from unittest.mock import AsyncMock, Mock
import pytest
from httpx import ASGITransport, AsyncClient
from src.server.fastapi_app import app
from src.server.models.download import (
DownloadItem,
DownloadPriority,
DownloadStatus,
EpisodeIdentifier,
)
from src.server.services.anime_service import AnimeService
from src.server.services.auth_service import auth_service
from src.server.services.download_service import DownloadService
from src.server.services.progress_service import ProgressService
@pytest.fixture(autouse=True)
def reset_auth():
"""Reset authentication state before each test."""
original_hash = auth_service._hash
auth_service._hash = None
auth_service._failed.clear()
yield
auth_service._hash = original_hash
auth_service._failed.clear()
@pytest.fixture
async def client():
"""Create an async test client."""
transport = ASGITransport(app=app)
async with AsyncClient(transport=transport, base_url="http://test") as ac:
yield ac
@pytest.fixture
async def authenticated_client(client):
"""Create an authenticated test client with token."""
# Setup master password
await client.post(
"/api/auth/setup",
json={"master_password": "TestPassword123!"}
)
# Login to get token
response = await client.post(
"/api/auth/login",
json={"password": "TestPassword123!"}
)
token = response.json()["access_token"]
# Add token to default headers
client.headers.update({"Authorization": f"Bearer {token}"})
yield client
@pytest.fixture
def mock_series_app():
"""Mock SeriesApp for testing."""
app_mock = Mock()
app_mock.series_list = []
app_mock.search = Mock(return_value=[])
app_mock.ReScan = Mock()
app_mock.download = Mock(return_value=True)
return app_mock
@pytest.fixture
def progress_service():
"""Create a ProgressService instance for testing."""
return ProgressService()
@pytest.fixture
async def download_service(mock_series_app, progress_service, tmp_path):
"""Create a DownloadService with mock repository for testing."""
from tests.unit.test_download_service import MockQueueRepository
mock_repo = MockQueueRepository()
anime_service = AnimeService(
series_app=mock_series_app,
progress_service=progress_service,
)
anime_service.download = AsyncMock(return_value=True)
service = DownloadService(
anime_service=anime_service,
progress_service=progress_service,
queue_repository=mock_repo,
)
yield service
await service.stop()
class TestAPIIdentifierConsistency:
"""Test that API endpoints use 'key' as the primary identifier."""
@pytest.mark.asyncio
async def test_queue_add_returns_key_in_response(
self, authenticated_client
):
"""Test that adding to queue uses key as identifier.
Verifies:
- Request accepts serie_id (key) as primary identifier
- serie_folder is accepted as metadata
- Response reflects correct identifiers
"""
request_data = {
"serie_id": "attack-on-titan", # Key (primary identifier)
"serie_folder": "Attack on Titan (2013)", # Metadata only
"serie_name": "Attack on Titan",
"episodes": [{"season": 1, "episode": 1}],
"priority": "normal"
}
response = await authenticated_client.post(
"/api/queue/add",
json=request_data
)
assert response.status_code == 201
data = response.json()
# Verify response structure
assert data["status"] == "success"
assert len(data.get("added_items", [])) > 0
@pytest.mark.asyncio
async def test_queue_status_contains_key_identifier(
self, authenticated_client
):
"""Test that queue status returns key as identifier.
Verifies:
- Queue items have serie_id (key) as identifier
- Queue items have serie_folder as metadata
- Both fields are present and distinct
"""
import uuid
# Add an item first with unique key
unique_suffix = str(uuid.uuid4())[:8]
unique_key = f"one-piece-{unique_suffix}"
unique_folder = f"One Piece ({unique_suffix})"
await authenticated_client.post(
"/api/queue/add",
json={
"serie_id": unique_key,
"serie_folder": unique_folder,
"serie_name": "One Piece",
"episodes": [{"season": 1, "episode": 1}],
"priority": "normal"
}
)
# Get queue status
response = await authenticated_client.get("/api/queue/status")
assert response.status_code == 200
data = response.json()
# Navigate to pending queue
pending = data["status"]["pending_queue"]
assert len(pending) > 0
# Find the item we just added by key
matching_items = [
item for item in pending if item["serie_id"] == unique_key
]
assert len(matching_items) == 1, (
f"Expected to find item with key {unique_key}"
)
item = matching_items[0]
# Verify identifier structure in queue item
assert "serie_id" in item, "Queue item must have serie_id (key)"
assert "serie_folder" in item, "Queue item must have serie_folder"
# Verify key format (lowercase, hyphenated)
assert item["serie_id"] == unique_key
# Verify folder is preserved as metadata
assert item["serie_folder"] == unique_folder
# Verify both are present but different
assert item["serie_id"] != item["serie_folder"]
@pytest.mark.asyncio
async def test_key_used_for_lookup_not_folder(
self, authenticated_client
):
"""Test that lookups use key, not folder.
Verifies:
- Items can be identified by serie_id (key)
- Multiple items with same folder but different keys are distinct
"""
import uuid
unique_suffix = str(uuid.uuid4())[:8]
# Add two items with different keys but similar folders
key1 = f"naruto-original-{unique_suffix}"
key2 = f"naruto-shippuden-{unique_suffix}"
shared_folder = f"Naruto Series ({unique_suffix})"
await authenticated_client.post(
"/api/queue/add",
json={
"serie_id": key1,
"serie_folder": shared_folder,
"serie_name": "Naruto",
"episodes": [{"season": 1, "episode": 1}],
"priority": "normal"
}
)
await authenticated_client.post(
"/api/queue/add",
json={
"serie_id": key2,
"serie_folder": shared_folder, # Same folder
"serie_name": "Naruto Shippuden",
"episodes": [{"season": 1, "episode": 1}],
"priority": "normal"
}
)
# Get queue status
response = await authenticated_client.get("/api/queue/status")
data = response.json()
pending = data["status"]["pending_queue"]
# Both items should be present (same folder doesn't cause collision)
serie_ids = [item["serie_id"] for item in pending]
assert key1 in serie_ids
assert key2 in serie_ids
class TestServiceIdentifierConsistency:
"""Test that services use 'key' as the primary identifier."""
@pytest.mark.asyncio
async def test_download_service_uses_key(self, download_service):
"""Test that DownloadService uses key as identifier.
Verifies:
- Items are stored with serie_id (key)
- Items can be retrieved by key
- Queue operations use key consistently
"""
# Add item to queue
item_ids = await download_service.add_to_queue(
serie_id="my-hero-academia",
serie_folder="My Hero Academia (2016)",
serie_name="My Hero Academia",
episodes=[EpisodeIdentifier(season=1, episode=1)],
priority=DownloadPriority.NORMAL,
)
assert len(item_ids) == 1
# Verify item is stored correctly
pending = download_service._pending_queue
assert len(pending) == 1
item = pending[0]
assert item.serie_id == "my-hero-academia"
assert item.serie_folder == "My Hero Academia (2016)"
@pytest.mark.asyncio
async def test_download_item_normalizes_key(self, download_service):
"""Test that serie_id is normalized (lowercase, stripped).
Verifies:
- Key is converted to lowercase
- Whitespace is stripped
"""
# Add item with uppercase key
item_ids = await download_service.add_to_queue(
serie_id=" DEMON-SLAYER ",
serie_folder="Demon Slayer (2019)",
serie_name="Demon Slayer",
episodes=[EpisodeIdentifier(season=1, episode=1)],
priority=DownloadPriority.NORMAL,
)
assert len(item_ids) == 1
# Verify key is normalized
item = download_service._pending_queue[0]
assert item.serie_id == "demon-slayer"
@pytest.mark.asyncio
async def test_queue_persistence_uses_key(
self, download_service, tmp_path
):
"""Test that persisted queue data uses key as identifier.
Verifies:
- Persisted data contains serie_id (key)
- Data can be restored with correct identifiers
"""
# Add item to queue
await download_service.add_to_queue(
serie_id="jujutsu-kaisen",
serie_folder="Jujutsu Kaisen (2020)",
serie_name="Jujutsu Kaisen",
episodes=[EpisodeIdentifier(season=1, episode=1)],
priority=DownloadPriority.NORMAL,
)
# Verify item is in pending queue (in-memory cache synced with DB)
pending_items = list(download_service._pending_queue)
assert len(pending_items) == 1
persisted_item = pending_items[0]
assert persisted_item.serie_id == "jujutsu-kaisen"
assert persisted_item.serie_folder == "Jujutsu Kaisen (2020)"
class TestWebSocketIdentifierConsistency:
"""Test that WebSocket events use 'key' in their payloads."""
@pytest.mark.asyncio
async def test_progress_events_include_key(
self, download_service, progress_service
):
"""Test that progress events include key identifier.
Verifies:
- Progress events contain key information
- Events use consistent identifier structure
"""
broadcasts: List[Dict[str, Any]] = []
async def mock_event_handler(event):
broadcasts.append({
"type": event.event_type,
"data": event.progress.to_dict(),
"room": event.room,
})
progress_service.subscribe("progress_updated", mock_event_handler)
# Add item to trigger events
await download_service.add_to_queue(
serie_id="spy-x-family",
serie_folder="Spy x Family (2022)",
serie_name="Spy x Family",
episodes=[EpisodeIdentifier(season=1, episode=1)],
priority=DownloadPriority.NORMAL,
)
# Verify events were emitted
assert len(broadcasts) >= 1
# Check queue progress events for metadata
queue_events = [
b for b in broadcasts if b["type"] == "queue_progress"
]
# Verify metadata structure includes identifier info
for event in queue_events:
metadata = event["data"].get("metadata", {})
# Queue events should track items by their identifiers
if "added_ids" in metadata:
assert len(metadata["added_ids"]) > 0
class TestIdentifierValidation:
"""Test identifier validation and edge cases."""
@pytest.mark.asyncio
async def test_key_format_validation(self, authenticated_client):
"""Test that key format is validated correctly.
Verifies:
- Valid keys are accepted (lowercase, hyphenated)
- Keys are normalized on input
"""
import uuid
unique_suffix = str(uuid.uuid4())[:8]
# Valid key format
response = await authenticated_client.post(
"/api/queue/add",
json={
"serie_id": f"valid-key-format-{unique_suffix}",
"serie_folder": f"Valid Key ({unique_suffix})",
"serie_name": "Valid Key",
"episodes": [{"season": 1, "episode": 1}],
"priority": "normal"
}
)
assert response.status_code == 201
@pytest.mark.asyncio
async def test_folder_not_used_for_identification(
self, download_service
):
"""Test that folder changes don't affect identification.
Verifies:
- Same key with different folder is same series
- Folder is metadata only, not identity
"""
# Add item
await download_service.add_to_queue(
serie_id="chainsaw-man",
serie_folder="Chainsaw Man (2022)",
serie_name="Chainsaw Man",
episodes=[EpisodeIdentifier(season=1, episode=1)],
priority=DownloadPriority.NORMAL,
)
# Add another episode for same key, different folder
await download_service.add_to_queue(
serie_id="chainsaw-man",
serie_folder="Chainsaw Man Updated (2022)", # Different folder
serie_name="Chainsaw Man",
episodes=[EpisodeIdentifier(season=1, episode=2)],
priority=DownloadPriority.NORMAL,
)
# Both should be added (same key, different episodes)
assert len(download_service._pending_queue) == 2
# Verify both use the same key
keys = [item.serie_id for item in download_service._pending_queue]
assert all(k == "chainsaw-man" for k in keys)
class TestEndToEndIdentifierFlow:
"""End-to-end tests for identifier consistency across layers."""
@pytest.mark.asyncio
async def test_complete_flow_with_key(
self, authenticated_client
):
"""Test complete flow uses key consistently.
Verifies:
- API -> Service -> Storage uses key
- All responses contain correct identifiers
"""
import uuid
# Use unique key to avoid conflicts with other tests
unique_suffix = str(uuid.uuid4())[:8]
unique_key = f"bleach-tybw-{unique_suffix}"
unique_folder = f"Bleach: TYBW ({unique_suffix})"
# 1. Add to queue via API
add_response = await authenticated_client.post(
"/api/queue/add",
json={
"serie_id": unique_key,
"serie_folder": unique_folder,
"serie_name": "Bleach: TYBW",
"episodes": [{"season": 1, "episode": 1}],
"priority": "high"
}
)
assert add_response.status_code == 201
# 2. Verify in queue status
status_response = await authenticated_client.get("/api/queue/status")
assert status_response.status_code == 200
status_data = status_response.json()
pending = status_data["status"]["pending_queue"]
# Find our item by key
items = [
i for i in pending
if i["serie_id"] == unique_key
]
assert len(items) == 1, (
f"Expected exactly 1 item with key {unique_key}, "
f"found {len(items)}"
)
item = items[0]
# 3. Verify identifier consistency
assert item["serie_id"] == unique_key
assert item["serie_folder"] == unique_folder
assert item["serie_name"] == "Bleach: TYBW"
# 4. Verify key and folder are different
assert item["serie_id"] != item["serie_folder"]
if __name__ == "__main__":
pytest.main([__file__, "-v"])