Files
Aniworld/tests/unit/test_anime_list_loading.py
Lukas 5526ab884a refactor: restructure core→server, split large entity files into database module
- Move src/core/ → src/server/
- Split SerieList.py (531 lines) and series.py (414 lines) into src/server/database/
- Add database/models.py for SQLAlchemy models
- Update all test imports to reflect new structure
- Remove deprecated test files (test_serie_class.py, test_serie_folder_with_year.py)
2026-06-04 21:11:53 +02:00

363 lines
13 KiB
Python

"""Unit tests for anime list loading from database.
Tests the fix for the issue where /api/anime returned empty array
because series weren't loaded from database into SeriesApp memory.
"""
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from src.server.SeriesApp import SeriesApp
from src.server.database.models import AnimeSeries, Episode
from src.server.services.anime_service import AnimeService
def make_anime(key, name, site, folder, episodeDict=None, year=None):
"""Create a mock AnimeSeries with episodeDict cache set."""
if episodeDict is None:
episodeDict = {}
mock = MagicMock(spec=AnimeSeries)
mock.key = key
mock.name = name
mock.site = site
mock.folder = folder
mock.year = year
mock.episodeDict = episodeDict
mock._episode_dict_cache = episodeDict
return mock
class TestAnimeListLoading:
"""Test suite for anime list loading functionality."""
@pytest.fixture
def mock_series_app(self):
"""Create a mock SeriesApp instance."""
app = MagicMock(spec=SeriesApp)
app.directory_to_search = "/test/anime"
app.list = MagicMock()
app.list.keyDict = {}
app.list.GetList = MagicMock(return_value=[])
app.load_series_from_list = MagicMock()
return app
@pytest.fixture
def sample_db_series(self):
"""Create sample database series for testing."""
series1 = AnimeSeries(
id=1,
key="test-anime-1",
name="Test Anime 1",
site="aniworld.to",
folder="Test Anime 1 (2023)",
year=2023,
episodes=[]
)
series2 = AnimeSeries(
id=2,
key="test-anime-2",
name="Test Anime 2",
site="aniworld.to",
folder="Test Anime 2 (2024)",
year=2024,
episodes=[
Episode(
id=1,
series_id=2,
season=1,
episode_number=1
),
Episode(
id=2,
series_id=2,
season=1,
episode_number=2
)
]
)
return [series1, series2]
@pytest.mark.asyncio
async def test_load_series_from_db_populates_series_app(
self, mock_series_app, sample_db_series
):
"""Test that _load_series_from_db loads series into SeriesApp memory.
This is the core fix for the empty anime list issue:
- Database has series data
- _load_series_from_db() should load them into SeriesApp.list
- series_app.list.GetList() should then return the series
"""
# Create AnimeService with mock SeriesApp
anime_service = AnimeService(mock_series_app)
# Mock database session and service
with patch('src.server.database.connection.get_db_session') as mock_get_session, \
patch('src.server.database.service.AnimeSeriesService') as mock_db_service:
# Setup mock database to return sample series
mock_db = AsyncMock()
mock_get_session.return_value.__aenter__.return_value = mock_db
mock_db_service.get_all = AsyncMock(return_value=sample_db_series)
# Call the method that loads series from DB
await anime_service._load_series_from_db()
# Verify load_series_from_list was called
mock_series_app.load_series_from_list.assert_called_once()
# Verify it was called with Serie objects
called_series = mock_series_app.load_series_from_list.call_args[0][0]
assert len(called_series) == 2
# Verify AnimeSeries objects have correct attributes
assert all(isinstance(s, AnimeSeries) for s in called_series)
assert called_series[0].key == "test-anime-1"
assert called_series[0].name == "Test Anime 1"
assert called_series[0].folder == "Test Anime 1 (2023)"
assert called_series[0].episodeDict == {}
assert called_series[1].key == "test-anime-2"
assert called_series[1].name == "Test Anime 2"
assert called_series[1].episodeDict == {1: [1, 2]}
@pytest.mark.asyncio
async def test_series_app_list_empty_before_loading(self, tmpdir):
"""Test that SeriesApp.list is empty when initialized with skip_load=True.
This demonstrates the original issue:
- SeriesApp is initialized with skip_load=True
- GetList() returns empty list until series are loaded from DB
"""
# Create a real SeriesApp with skip_load=True
anime_dir = str(tmpdir.mkdir("anime"))
series_app = SeriesApp(anime_dir)
# Verify list is empty (this was the bug)
assert len(series_app.list.GetList()) == 0
assert len(series_app.list.keyDict) == 0
def test_load_series_from_list_populates_keydict(self, tmpdir):
"""Test that load_series_from_list correctly populates the keyDict.
This is the method that fixes the empty list issue by loading
series from database into SeriesApp memory.
"""
# Create a real SeriesApp
anime_dir = str(tmpdir.mkdir("anime"))
series_app = SeriesApp(anime_dir)
# Verify list starts empty
assert len(series_app.list.GetList()) == 0
# Create test series
test_series = [
make_anime(
key="test-1",
name="Test Series 1",
site="aniworld.to",
folder="Test Series 1 (2023)",
episodeDict={1: [1, 2, 3]}
),
make_anime(
key="test-2",
name="Test Series 2",
site="aniworld.to",
folder="Test Series 2 (2024)",
episodeDict={}
)
]
# Load series into SeriesApp (this is what the fix does)
series_app.load_series_from_list(test_series)
# Verify list is now populated
loaded_series = series_app.list.GetList()
assert len(loaded_series) == 2
assert loaded_series[0].key == "test-1"
assert loaded_series[1].key == "test-2"
# Verify keyDict is populated correctly
assert "test-1" in series_app.list.keyDict
assert "test-2" in series_app.list.keyDict
assert series_app.list.keyDict["test-1"].name == "Test Series 1"
@pytest.mark.asyncio
async def test_anime_endpoint_returns_series_after_loading(
self, tmpdir
):
"""Integration test: Verify /api/anime endpoint returns series after loading.
This tests the complete flow:
1. Series exist in database
2. _load_series_from_db() loads them into memory
3. /api/anime endpoint returns them
"""
from unittest.mock import AsyncMock, MagicMock
from httpx import ASGITransport, AsyncClient
from src.server.fastapi_app import app as fastapi_app
from src.server.utils.dependencies import (
get_anime_service,
get_series_app,
require_auth,
)
# Create a mock AnimeService that returns the test data
mock_anime_svc = MagicMock()
mock_anime_svc.list_series_with_filters = AsyncMock(return_value=[
{
"key": "attack-on-titan",
"name": "Attack on Titan",
"site": "aniworld.to",
"folder": "Attack on Titan (2013)",
"episodeDict": {1: [1, 2]},
"has_nfo": False,
},
{
"key": "one-piece",
"name": "One Piece",
"site": "aniworld.to",
"folder": "One Piece (1999)",
"episodeDict": {},
"has_nfo": False,
},
])
# Override dependencies
fastapi_app.dependency_overrides[get_anime_service] = lambda: mock_anime_svc
fastapi_app.dependency_overrides[require_auth] = lambda: {"user": "test"}
try:
transport = ASGITransport(app=fastapi_app)
async with AsyncClient(
transport=transport,
base_url="http://test"
) as client:
# Call the endpoint (no auth needed due to override)
response = await client.get("/api/anime")
# Verify response
assert response.status_code == 200
data = response.json()
# This was the bug: empty array
# After fix: returns series
assert len(data) == 2
assert data[0]["key"] == "attack-on-titan"
assert data[0]["name"] == "Attack on Titan"
assert data[0]["has_missing"] is True
assert data[1]["key"] == "one-piece"
assert data[1]["has_missing"] is False
finally:
# Clean up override
fastapi_app.dependency_overrides.clear()
@pytest.mark.asyncio
async def test_empty_database_returns_empty_list(self, tmpdir):
"""Test that empty database correctly returns empty list.
Edge case: No series in database should return empty array,
not cause an error.
"""
from httpx import ASGITransport, AsyncClient
from src.server.fastapi_app import app as fastapi_app
from src.server.utils.dependencies import get_series_app, require_auth
# Create SeriesApp with no series
anime_dir = str(tmpdir.mkdir("anime"))
series_app = SeriesApp(anime_dir)
series_app.load_series_from_list([]) # Empty list
# Override dependencies
fastapi_app.dependency_overrides[get_series_app] = lambda: series_app
fastapi_app.dependency_overrides[require_auth] = lambda: {"user": "test"}
try:
transport = ASGITransport(app=fastapi_app)
async with AsyncClient(
transport=transport,
base_url="http://test"
) as client:
# Call the endpoint (no auth needed due to override)
response = await client.get("/api/anime")
# Should return 200 with empty array
assert response.status_code == 200
data = response.json()
assert data == []
finally:
# Clean up override
fastapi_app.dependency_overrides.clear()
def test_series_app_skip_load_behavior(self, tmpdir):
"""Test that skip_load=True prevents automatic filesystem loading.
This documents why the bug occurred:
- SeriesApp initializes with skip_load=True
- This prevents automatic loading from filesystem
- Series must be explicitly loaded via load_series_from_list()
"""
# Test with skip_load=True (default in production)
anime_dir = str(tmpdir.mkdir("anime"))
series_app = SeriesApp(anime_dir)
assert len(series_app.list.GetList()) == 0, \
"With skip_load=True, list should be empty initially"
# Test that manual loading works
test_serie = make_anime(
key="test",
name="Test",
site="aniworld.to",
folder="Test (2023)",
episodeDict={}
)
series_app.load_series_from_list([test_serie])
assert len(series_app.list.GetList()) == 1, \
"After load_series_from_list, series should be available"
@pytest.mark.asyncio
async def test_load_series_with_missing_episodes(self, sample_db_series):
"""Test that missing episodes are correctly converted from DB format.
Verifies that Episode records in database are correctly converted
to episodeDict format in Serie objects.
"""
from src.server.services.anime_service import AnimeService
# Create mock SeriesApp
series_app = MagicMock(spec=SeriesApp)
series_app.directory_to_search = "/test/anime"
series_app.load_series_from_list = MagicMock()
anime_service = AnimeService(series_app)
# Mock database
with patch('src.server.database.connection.get_db_session') as mock_get_session, \
patch('src.server.database.service.AnimeSeriesService') as mock_db_service:
mock_db = AsyncMock()
mock_get_session.return_value.__aenter__.return_value = mock_db
mock_db_service.get_all = AsyncMock(return_value=sample_db_series)
# Load from DB
await anime_service._load_series_from_db()
# Get the loaded series
called_series = series_app.load_series_from_list.call_args[0][0]
# Verify episode dict conversion
series_with_episodes = [s for s in called_series if s.episodeDict]
assert len(series_with_episodes) == 1
assert series_with_episodes[0].key == "test-anime-2"
assert series_with_episodes[0].episodeDict == {1: [1, 2]}
if __name__ == "__main__":
pytest.main([__file__, "-v"])