test: Add unit tests for anime list loading fix

- Test that SeriesApp.list starts empty with skip_load=True
- Test that load_series_from_list populates the keyDict correctly
- Test that _load_series_from_db loads series from database into memory
- Test that /api/anime endpoint returns series after loading
- Test empty database edge case
- Test episode dict conversion from DB format
- All 7 tests passing
This commit is contained in:
2026-01-21 19:02:39 +01:00
parent b2379e05cf
commit 35c82e68b7

View File

@@ -0,0 +1,339 @@
"""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.
"""
import pytest
from unittest.mock import AsyncMock, MagicMock, patch
from src.core.entities.series import Serie
from src.core.SeriesApp import SeriesApp
from src.server.database.models import AnimeSeries, Episode
from src.server.services.anime_service import AnimeService
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 Serie objects have correct attributes
assert all(isinstance(s, Serie) 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 = [
Serie(
key="test-1",
name="Test Series 1",
site="aniworld.to",
folder="Test Series 1 (2023)",
episodeDict={1: [1, 2, 3]}
),
Serie(
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 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 real SeriesApp and load test data
anime_dir = str(tmpdir.mkdir("anime"))
series_app = SeriesApp(anime_dir)
test_series = [
Serie(
key="attack-on-titan",
name="Attack on Titan",
site="aniworld.to",
folder="Attack on Titan (2013)",
episodeDict={1: [1, 2]}
),
Serie(
key="one-piece",
name="One Piece",
site="aniworld.to",
folder="One Piece (1999)",
episodeDict={}
)
]
series_app.load_series_from_list(test_series)
# Override dependencies to use our test SeriesApp and skip auth
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")
# 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 = Serie(
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"])