Files
Aniworld/tests/unit/test_serie_scanner_db_writes.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

226 lines
8.3 KiB
Python

"""Tests for SerieScanner DB persistence functionality."""
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from src.server.database.models import AnimeSeries
from src.server.SerieScanner import SerieScanner
@pytest.fixture
def mock_session_factory():
"""Create a mock async session factory."""
mock_session = AsyncMock()
mock_session_factory = MagicMock(return_value=mock_session)
return mock_session_factory, mock_session
@pytest.fixture
def sample_serie():
"""Create a sample AnimeSeries mock for testing."""
anime = MagicMock(spec=AnimeSeries)
anime.key = "attack-on-titan"
anime.name = "Attack on Titan"
anime.site = "aniworld.to"
anime.folder = "Attack on Titan (2013)"
anime.year = 2013
anime.episodeDict = {1: [1, 2, 3], 2: [1, 2]}
return anime
class TestPersistSerieToDb:
"""Test _persist_serie_to_db method."""
@pytest.mark.asyncio
async def test_creates_new_series_when_not_exists(
self, mock_session_factory, sample_serie
):
"""Verify new series is created in DB."""
mock_factory, mock_session = mock_session_factory
with patch(
"src.server.database.connection.get_async_session_factory",
return_value=mock_factory
):
with patch(
"src.server.database.service.AnimeSeriesService.get_by_key",
return_value=None
):
mock_anime_series = MagicMock()
mock_anime_series.id = 1
with patch(
"src.server.database.service.AnimeSeriesService.create",
return_value=mock_anime_series
):
scanner = SerieScanner("/tmp", MagicMock())
await scanner._persist_serie_to_db(sample_serie)
from src.server.database.service import AnimeSeriesService
AnimeSeriesService.create.assert_called_once()
call_kwargs = AnimeSeriesService.create.call_args[1]
assert call_kwargs["key"] == "attack-on-titan"
assert call_kwargs["name"] == "Attack on Titan"
@pytest.mark.asyncio
async def test_updates_existing_series(self, mock_session_factory, sample_serie):
"""Verify existing series is updated."""
mock_factory, mock_session = mock_session_factory
mock_existing = MagicMock()
mock_existing.id = 42
mock_existing.key = "attack-on-titan"
scanner = SerieScanner("/tmp", MagicMock())
with patch(
"src.server.database.connection.get_async_session_factory",
return_value=mock_factory
):
with patch(
"src.server.database.service.AnimeSeriesService.get_by_key",
return_value=mock_existing
):
with patch(
"src.server.database.service.AnimeSeriesService.update",
new_callable=AsyncMock
) as mock_update:
with patch.object(
scanner,
"_sync_episodes_to_db",
new_callable=AsyncMock
):
await scanner._persist_serie_to_db(sample_serie)
mock_update.assert_called_once()
call_args = mock_update.call_args[0]
assert call_args[1] == 42 # series_id
class TestSyncEpisodesToDb:
"""Test _sync_episodes_to_db method."""
@pytest.mark.asyncio
async def test_preserves_downloaded_episodes(self):
"""Verify downloaded episodes are not removed even when no longer missing."""
mock_session = AsyncMock()
# S01E1 was downloaded (file exists), S01E2 was missing but file now exists
# Both are no longer in episode_dict
existing_eps = [
MagicMock(id=1, season=1, episode_number=1, is_downloaded=True),
MagicMock(id=2, season=1, episode_number=2, is_downloaded=True),
]
with patch(
"src.server.database.service.EpisodeService.get_by_series",
return_value=existing_eps
):
with patch(
"src.server.database.service.EpisodeService.delete_by_series",
new_callable=AsyncMock
) as mock_delete:
scanner = SerieScanner("/tmp", MagicMock())
# Neither S01E1 nor S01E2 are missing now
await scanner._sync_episodes_to_db(
mock_session, 1, {} # No episodes missing
)
# Neither should be deleted since both are downloaded
mock_delete.assert_not_called()
@pytest.mark.asyncio
async def test_removes_missing_episodes_when_no_longer_missing(self):
"""Verify episodes removed from DB if file now exists."""
mock_session = AsyncMock()
existing_eps = [
MagicMock(id=1, season=1, episode_number=1, is_downloaded=False),
MagicMock(id=2, season=1, episode_number=2, is_downloaded=False),
]
with patch(
"src.server.database.service.EpisodeService.get_by_series",
return_value=existing_eps
):
with patch(
"src.server.database.service.EpisodeService.delete_by_series",
new_callable=AsyncMock
) as mock_delete:
with patch(
"src.server.database.service.EpisodeService.create",
new_callable=AsyncMock
):
scanner = SerieScanner("/tmp", MagicMock())
await scanner._sync_episodes_to_db(
mock_session, 1, {1: [1]} # Only S01E01 now missing
)
# S01E02 should be deleted since no longer missing
mock_delete.assert_called_once()
@pytest.mark.asyncio
async def test_adds_new_missing_episodes(self):
"""Verify new missing episodes are added."""
mock_session = AsyncMock()
existing_eps = [
MagicMock(id=1, season=1, episode_number=1, is_downloaded=False),
]
with patch(
"src.server.database.service.EpisodeService.get_by_series",
return_value=existing_eps
):
with patch(
"src.server.database.service.EpisodeService.create",
new_callable=AsyncMock
) as mock_create:
scanner = SerieScanner("/tmp", MagicMock())
await scanner._sync_episodes_to_db(
mock_session, 1, {1: [1, 2, 3]} # S01E01, S01E02, S01E03
)
# S01E02 and S01E03 should be created
assert mock_create.call_count == 2
class TestPersistSerieToDbErrorHandling:
"""Test error handling in _persist_serie_to_db."""
@pytest.mark.asyncio
async def test_logs_error_when_db_unavailable(self, sample_serie):
"""Verify DB unavailability is logged but doesn't crash."""
with patch(
"src.server.database.connection.get_async_session_factory",
side_effect=RuntimeError("DB not initialized")
):
scanner = SerieScanner("/tmp", MagicMock())
# Should not raise
await scanner._persist_serie_to_db(sample_serie)
@pytest.mark.asyncio
async def test_rollback_on_failure(self, mock_session_factory, sample_serie):
"""Verify rollback on DB failure."""
mock_factory, mock_session = mock_session_factory
mock_existing = MagicMock()
mock_existing.id = 1
with patch(
"src.server.database.connection.get_async_session_factory",
return_value=mock_session
):
with patch(
"src.server.database.service.AnimeSeriesService.get_by_key",
return_value=mock_existing
):
with patch(
"src.server.database.service.AnimeSeriesService.update",
side_effect=Exception("DB error")
):
scanner = SerieScanner("/tmp", MagicMock())
# Should not raise but should rollback
await scanner._persist_serie_to_db(sample_serie)
mock_session.rollback.assert_called_once()