Files
Aniworld/tests/unit/test_background_loader_service.py
Lukas f18c31a035 Implement async series data loading with background processing
- Add loading status fields to AnimeSeries model
- Create BackgroundLoaderService for async task processing
- Update POST /api/anime/add to return 202 Accepted immediately
- Add GET /api/anime/{key}/loading-status endpoint
- Integrate background loader with startup/shutdown lifecycle
- Create database migration script for loading status fields
- Add unit tests for BackgroundLoaderService (10 tests, all passing)
- Update AnimeSeriesService.create() to accept loading status fields

Architecture follows clean separation with no code duplication:
- BackgroundLoader orchestrates, doesn't reimplement
- Reuses existing AnimeService, NFOService, WebSocket patterns
- Database-backed status survives restarts
2026-01-19 07:14:55 +01:00

202 lines
6.4 KiB
Python

"""Unit tests for BackgroundLoaderService.
Tests task queuing, status tracking, and worker logic in isolation.
"""
import asyncio
from datetime import datetime
from unittest.mock import AsyncMock, Mock, patch
import pytest
from src.server.services.background_loader_service import (
BackgroundLoaderService,
LoadingStatus,
SeriesLoadingTask,
)
@pytest.fixture
def mock_websocket_service():
"""Mock WebSocket service."""
service = Mock()
service.broadcast = AsyncMock()
return service
@pytest.fixture
def mock_anime_service():
"""Mock anime service."""
service = Mock()
service.rescan = AsyncMock()
return service
@pytest.fixture
def mock_series_app():
"""Mock SeriesApp."""
app = Mock()
app.directory_to_search = "/test/anime"
app.nfo_service = Mock()
app.nfo_service.create_tvshow_nfo = AsyncMock()
return app
@pytest.fixture
async def background_loader(mock_websocket_service, mock_anime_service, mock_series_app):
"""Create BackgroundLoaderService instance."""
service = BackgroundLoaderService(
websocket_service=mock_websocket_service,
anime_service=mock_anime_service,
series_app=mock_series_app
)
yield service
await service.stop()
class TestBackgroundLoaderService:
"""Test suite for BackgroundLoaderService."""
@pytest.mark.asyncio
async def test_service_initialization(self, background_loader):
"""Test service initializes correctly."""
assert background_loader.task_queue is not None
assert isinstance(background_loader.active_tasks, dict)
assert len(background_loader.active_tasks) == 0
@pytest.mark.asyncio
async def test_start_worker(self, background_loader):
"""Test worker starts successfully."""
await background_loader.start()
assert background_loader.worker_task is not None
assert not background_loader.worker_task.done()
@pytest.mark.asyncio
async def test_stop_worker_gracefully(self, background_loader):
"""Test worker stops gracefully."""
await background_loader.start()
await background_loader.stop()
assert background_loader.worker_task.done()
@pytest.mark.asyncio
async def test_add_series_loading_task(self, background_loader):
"""Test adding a series to the loading queue."""
await background_loader.add_series_loading_task(
key="test-series",
folder="Test Series",
name="Test Series",
year=2024
)
# Verify task in active tasks
assert "test-series" in background_loader.active_tasks
task = background_loader.active_tasks["test-series"]
assert task.key == "test-series"
assert task.status == LoadingStatus.PENDING
@pytest.mark.asyncio
async def test_duplicate_task_handling(self, background_loader):
"""Test that duplicate tasks for same series are handled correctly."""
key = "test-series"
await background_loader.add_series_loading_task(
key=key,
folder="Test Series",
name="Test Series"
)
await background_loader.add_series_loading_task(
key=key,
folder="Test Series",
name="Test Series"
)
# Verify only one task exists
assert len([k for k in background_loader.active_tasks if k == key]) == 1
@pytest.mark.asyncio
async def test_check_missing_data_all_missing(
self,
background_loader,
mock_series_app
):
"""Test checking for missing data when all data is missing."""
with patch('src.server.database.service.AnimeSeriesService.get_by_key') as mock_get:
mock_series = Mock()
mock_series.episodes_loaded = False
mock_series.has_nfo = False
mock_series.logo_loaded = False
mock_series.images_loaded = False
mock_get.return_value = mock_series
mock_db = AsyncMock()
missing_data = await background_loader.check_missing_data(
key="test-series",
folder="Test Series",
anime_directory="/test/anime",
db=mock_db
)
assert missing_data["episodes"] is True
assert missing_data["nfo"] is True
assert missing_data["logo"] is True
assert missing_data["images"] is True
@pytest.mark.asyncio
async def test_broadcast_status(self, background_loader, mock_websocket_service):
"""Test status broadcasting via WebSocket."""
task = SeriesLoadingTask(
key="test-series",
folder="Test Series",
name="Test Series",
status=LoadingStatus.LOADING_EPISODES
)
await background_loader._broadcast_status(task)
# Verify broadcast was called
mock_websocket_service.broadcast.assert_called_once()
# Verify message structure
call_args = mock_websocket_service.broadcast.call_args[0][0]
assert call_args["type"] == "series_loading_update"
assert call_args["key"] == "test-series"
assert call_args["loading_status"] == "loading_episodes"
class TestSeriesLoadingTask:
"""Test SeriesLoadingTask model."""
def test_task_initialization(self):
"""Test task initializes with correct defaults."""
task = SeriesLoadingTask(
key="test",
folder="Test",
name="Test"
)
assert task.key == "test"
assert task.status == LoadingStatus.PENDING
assert not any(task.progress.values())
def test_task_progress_tracking(self):
"""Test progress tracking updates correctly."""
task = SeriesLoadingTask(
key="test",
folder="Test",
name="Test"
)
task.progress["episodes"] = True
assert task.progress["episodes"] is True
assert not task.progress["nfo"]
assert not task.progress["logo"]
assert not task.progress["images"]
def test_loading_status_enum(self):
"""Test LoadingStatus enum values."""
assert LoadingStatus.PENDING == "pending"
assert LoadingStatus.LOADING_EPISODES == "loading_episodes"
assert LoadingStatus.LOADING_NFO == "loading_nfo"
assert LoadingStatus.COMPLETED == "completed"
assert LoadingStatus.FAILED == "failed"