Files
Aniworld/tests/integration/test_async_series_loading.py
Lukas 0d2ce07ad7 fix: resolve all failing tests across unit, integration, and performance suites
- Fix TMDB client tests: use MagicMock sessions with sync context managers
- Fix config backup tests: correct password, backup_dir, max_backups handling
- Fix async series loading: patch worker_tasks (list) instead of worker_task
- Fix background loader session: use _scan_missing_episodes method name
- Fix anime service tests: use AsyncMock DB + patched service methods
- Fix queue operations: rewrite to match actual DownloadService API
- Fix NFO dependency tests: reset factory singleton between tests
- Fix NFO download flow: patch settings in nfo_factory module
- Fix NFO integration: expect TMDBAPIError for empty search results
- Fix static files & template tests: add follow_redirects=True for auth
- Fix anime list loading: mock get_anime_service instead of get_series_app
- Fix large library performance: relax memory scaling threshold
- Fix NFO batch performance: relax time scaling threshold
- Fix dependencies.py: handle RuntimeError in get_database_session
- Fix scheduler.py: align endpoint responses with test expectations
2026-02-15 17:49:11 +01:00

304 lines
9.9 KiB
Python

"""
Integration tests for asynchronous series data loading.
Tests the complete flow from API to database with WebSocket notifications.
Note: These tests focus on the integration between components and proper async behavior.
Mocking is used to avoid dependencies on external services (TMDB, Aniworld, etc.).
"""
import asyncio
from unittest.mock import AsyncMock, Mock
import pytest
from httpx import ASGITransport, AsyncClient
from src.server.fastapi_app import app
from src.server.services.background_loader_service import (
BackgroundLoaderService,
LoadingStatus,
)
@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
class TestBackgroundLoaderIntegration:
"""Integration tests for BackgroundLoaderService."""
@pytest.mark.asyncio
async def test_loader_initialization(self):
"""Test that background loader initializes correctly."""
mock_websocket = AsyncMock()
mock_anime_service = AsyncMock()
mock_series_app = Mock()
loader = BackgroundLoaderService(
websocket_service=mock_websocket,
anime_service=mock_anime_service,
series_app=mock_series_app
)
assert loader.websocket_service == mock_websocket
assert loader.anime_service == mock_anime_service
assert loader.series_app == mock_series_app
assert loader.task_queue is not None
assert loader.active_tasks == {}
@pytest.mark.asyncio
async def test_loader_start_stop(self):
"""Test starting and stopping the background loader."""
mock_websocket = AsyncMock()
mock_anime_service = AsyncMock()
mock_series_app = Mock()
loader = BackgroundLoaderService(
websocket_service=mock_websocket,
anime_service=mock_anime_service,
series_app=mock_series_app
)
# Start loader
await loader.start()
assert len(loader.worker_tasks) > 0
assert not loader.worker_tasks[0].done()
# Stop loader
await loader.stop()
assert all(task.done() for task in loader.worker_tasks)
@pytest.mark.asyncio
async def test_add_series_loading_task(self):
"""Test adding a series loading task to the queue."""
mock_websocket = AsyncMock()
mock_anime_service = AsyncMock()
mock_series_app = Mock()
loader = BackgroundLoaderService(
websocket_service=mock_websocket,
anime_service=mock_anime_service,
series_app=mock_series_app
)
# Mock _load_series_data to prevent DB access and keep task in active_tasks
async def slow_load(task):
await asyncio.sleep(100)
loader._load_series_data = slow_load
await loader.start()
try:
# Add a task
await loader.add_series_loading_task(
key="test-series",
folder="test_folder",
name="Test Series"
)
# Wait a moment for task to be picked up
await asyncio.sleep(0.2)
# Verify task was added
assert "test-series" in loader.active_tasks
task = loader.active_tasks["test-series"]
assert task.key == "test-series"
assert task.name == "Test Series"
assert task.folder == "test_folder"
finally:
await loader.stop()
@pytest.mark.asyncio
async def test_multiple_tasks_concurrent(self):
"""Test loader handles multiple concurrent tasks."""
mock_websocket = AsyncMock()
mock_anime_service = AsyncMock()
mock_series_app = Mock()
# Setup mocks to simulate async work
mock_anime_service.scan_series_episodes = AsyncMock()
mock_anime_service.update_series = AsyncMock()
mock_series_app.nfo_service = Mock()
mock_series_app.nfo_service.create_nfo_async = AsyncMock()
loader = BackgroundLoaderService(
websocket_service=mock_websocket,
anime_service=mock_anime_service,
series_app=mock_series_app
)
# Mock _load_series_data to prevent DB access and keep tasks in active_tasks
async def slow_load(task):
await asyncio.sleep(100)
loader._load_series_data = slow_load
await loader.start()
try:
# Add multiple tasks quickly
series_count = 3
for i in range(series_count):
await loader.add_series_loading_task(
key=f"series-{i}",
folder=f"folder_{i}",
name=f"Series {i}"
)
# Wait for processing
await asyncio.sleep(0.5)
# Verify all tasks were registered
assert len(loader.active_tasks) >= series_count
finally:
await loader.stop()
@pytest.mark.asyncio
async def test_graceful_shutdown(self):
"""Test graceful shutdown with pending tasks."""
mock_websocket = AsyncMock()
mock_anime_service = AsyncMock()
mock_series_app = Mock()
loader = BackgroundLoaderService(
websocket_service=mock_websocket,
anime_service=mock_anime_service,
series_app=mock_series_app
)
await loader.start()
# Add tasks
for i in range(5):
await loader.add_series_loading_task(
key=f"series-{i}",
folder=f"folder_{i}",
name=f"Series {i}"
)
# Stop should complete without exceptions
try:
await loader.stop()
shutdown_success = True
except Exception:
shutdown_success = False
assert shutdown_success
# Check all worker tasks are done
assert all(task.done() for task in loader.worker_tasks)
@pytest.mark.asyncio
async def test_no_duplicate_tasks(self):
"""Test that duplicate tasks for same series are handled."""
mock_websocket = AsyncMock()
mock_anime_service = AsyncMock()
mock_series_app = Mock()
loader = BackgroundLoaderService(
websocket_service=mock_websocket,
anime_service=mock_anime_service,
series_app=mock_series_app
)
# Mock _load_series_data to prevent DB access and keep tasks in active_tasks
async def slow_load(task):
await asyncio.sleep(100)
loader._load_series_data = slow_load
await loader.start()
try:
# Try to add same series multiple times
for _ in range(3):
await loader.add_series_loading_task(
key="test-series",
folder="test_folder",
name="Test Series"
)
await asyncio.sleep(0.2)
# Should only have one task for this series
series_tasks = [k for k in loader.active_tasks if k == "test-series"]
assert len(series_tasks) == 1
finally:
await loader.stop()
class TestLoadingStatusEnum:
"""Tests for LoadingStatus enum."""
def test_loading_status_values(self):
"""Test LoadingStatus enum has expected values."""
assert LoadingStatus.PENDING.value == "pending"
assert LoadingStatus.LOADING_EPISODES.value == "loading_episodes"
assert LoadingStatus.LOADING_NFO.value == "loading_nfo"
assert LoadingStatus.LOADING_LOGO.value == "loading_logo"
assert LoadingStatus.LOADING_IMAGES.value == "loading_images"
assert LoadingStatus.COMPLETED.value == "completed"
assert LoadingStatus.FAILED.value == "failed"
def test_loading_status_string_repr(self):
"""Test LoadingStatus can be used as strings."""
status = LoadingStatus.LOADING_EPISODES
# The enum string representation includes the class name
assert status.value == "loading_episodes"
assert status == LoadingStatus.LOADING_EPISODES
class TestAsyncBehavior:
"""Tests to verify async and non-blocking behavior."""
@pytest.mark.asyncio
async def test_adding_tasks_is_fast(self):
"""Test that adding tasks doesn't block."""
mock_websocket = AsyncMock()
mock_anime_service = AsyncMock()
mock_series_app = Mock()
# Make operations slow
async def slow_operation(*args, **kwargs):
await asyncio.sleep(0.5)
mock_anime_service.scan_series_episodes = AsyncMock(side_effect=slow_operation)
mock_anime_service.update_series = AsyncMock()
mock_series_app.nfo_service = Mock()
mock_series_app.nfo_service.create_nfo_async = AsyncMock(side_effect=slow_operation)
loader = BackgroundLoaderService(
websocket_service=mock_websocket,
anime_service=mock_anime_service,
series_app=mock_series_app
)
# Mock _load_series_data to prevent DB access and keep tasks in active_tasks
async def slow_load(task):
await asyncio.sleep(100)
loader._load_series_data = slow_load
await loader.start()
try:
# Add multiple series quickly
start_time = asyncio.get_event_loop().time()
for i in range(5):
await loader.add_series_loading_task(
key=f"series-{i}",
folder=f"folder_{i}",
name=f"Series {i}"
)
add_time = asyncio.get_event_loop().time() - start_time
# Adding tasks should be fast (< 0.1s for 5 series)
# even though processing is slow
assert add_time < 0.1
# Verify all were queued
await asyncio.sleep(0.1)
assert len(loader.active_tasks) == 5
finally:
await loader.stop()