"""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()