"""Integration tests for episode download sync with in-memory updates. Tests verify that when episodes are downloaded successfully: - In-memory AnimeSeries.episodeDict is updated - Missing episode list reflects the change immediately Note: Data file sync removed since AnimeSeries doesn't have save_to_file/load_from_file. """ import asyncio import json import os import tempfile from pathlib import Path from unittest.mock import AsyncMock, MagicMock, patch import pytest from src.server.database.models import AnimeSeries from src.server.SeriesApp import SeriesApp from src.server.models.download import DownloadItem, DownloadPriority, DownloadStatus from src.server.services.download_service import DownloadService def make_anime(key, name, folder=None, episode_dict=None, year=None, site="https://example.com"): """Create a mock AnimeSeries with needed properties.""" anime = MagicMock(spec=AnimeSeries) anime.key = key anime.name = name anime.folder = folder or name anime.site = site anime.year = year anime.episodeDict = episode_dict or {} return anime class TestEpisodeRemovedFromMissingListAfterDownload: """Verify episode no longer appears in missing list after download completes.""" @pytest.fixture def temp_dir(self): """Create temp directory for test data files.""" with tempfile.TemporaryDirectory() as tmp: yield Path(tmp) @pytest.fixture def mock_anime_service(self, temp_dir): """Create mock anime service with app.""" anime_service = MagicMock() anime_service._directory = str(temp_dir) anime = make_anime( key="test-series", name="Test Series", site="https://example.com", folder="Test Series", episode_dict={1: [1, 2, 3]}, ) mock_app = MagicMock() mock_app.list.keyDict = {"test-series": anime} mock_app.list.GetMissingEpisode.return_value = [anime] mock_app.series_list = [anime] anime_service._app = mock_app anime_service._cached_list_missing = MagicMock() anime_service._broadcast_series_updated = AsyncMock() return anime_service @pytest.fixture def mock_download_service(self, mock_anime_service): """Create download service with mocked dependencies.""" with tempfile.TemporaryDirectory() as tmp: service = DownloadService( anime_service=mock_anime_service, queue_repository=MagicMock(), max_retries=3, ) service._directory = str(mock_anime_service._directory) yield service @pytest.mark.asyncio async def test_episode_removed_from_missing_list_after_download( self, mock_download_service, mock_anime_service ): """Verify episode no longer appears in missing list after download completes.""" anime = mock_anime_service._app.list.keyDict["test-series"] # Verify episode starts in missing list assert 2 in anime.episodeDict[1], "Episode should start in missing list" # Simulate download completion by calling _remove_episode_from_memory mock_download_service._remove_episode_from_memory("test-series", 1, 2) # Episode should be removed from episodeDict assert 2 not in anime.episodeDict[1], "Episode should be removed from missing list" assert anime.episodeDict[1] == [1, 3] # series_list should be refreshed mock_anime_service._app.list.GetMissingEpisode.assert_called() class TestDownloadUpdatesInMemoryCache: """Verify in-memory AnimeSeries.episodeDict is updated after download.""" @pytest.fixture def mock_anime_service(self): """Create mock anime service with app.""" anime_service = MagicMock() anime_service._directory = "/tmp/test" anime = make_anime( key="multi-season-series", name="Multi Season Series", site="https://example.com", folder="Multi Season Series", episode_dict={ 1: [1, 2, 3, 4, 5], 2: [1, 2, 3], }, ) mock_app = MagicMock() mock_app.list.keyDict = {"multi-season-series": anime} mock_app.list.GetMissingEpisode.return_value = [anime] mock_app.series_list = [anime] anime_service._app = mock_app anime_service._cached_list_missing = MagicMock() anime_service._broadcast_series_updated = AsyncMock() return anime_service @pytest.fixture def mock_download_service(self, mock_anime_service): """Create download service with mocked dependencies.""" with tempfile.TemporaryDirectory() as tmp: service = DownloadService( anime_service=mock_anime_service, queue_repository=MagicMock(), max_retries=3, ) service._directory = str(mock_anime_service._directory) yield service @pytest.mark.asyncio async def test_download_updates_in_memory_cache( self, mock_download_service, mock_anime_service ): """Verify in-memory AnimeSeries.episodeDict is updated after download.""" anime = mock_anime_service._app.list.keyDict["multi-season-series"] # Put back episodes after the fixture setup anime.episodeDict = {1: [1, 2, 3, 4, 5], 2: [1, 2, 3]} # Verify preconditions assert 1 in anime.episodeDict[1] assert 3 in anime.episodeDict[2] # Simulate downloading multiple episodes mock_download_service._remove_episode_from_memory("multi-season-series", 1, 1) mock_download_service._remove_episode_from_memory("multi-season-series", 1, 3) mock_download_service._remove_episode_from_memory("multi-season-series", 2, 2) # Verify episodes removed assert 1 not in anime.episodeDict[1], "Episode 1 of season 1 should be removed" assert 3 not in anime.episodeDict[1], "Episode 3 of season 1 should be removed" assert 2 in anime.episodeDict[1], "Episode 2 of season 1 should remain" assert 3 in anime.episodeDict[2], "Episode 3 of season 2 should remain" assert 2 not in anime.episodeDict[2], "Episode 2 of season 2 should be removed" # Verify seasons with no episodes are cleaned up assert 2 in anime.episodeDict, "Season 2 should still exist (has episode 1, 3)" @pytest.mark.asyncio async def test_last_episode_removes_season( self, mock_download_service, mock_anime_service ): """Verify that removing last episode in a season removes the season key.""" anime = mock_anime_service._app.list.keyDict["multi-season-series"] # Reset and set to proper test state anime.episodeDict = {1: [2], 2: [1, 2, 3]} # Season 1 only has episode 2 # Verify initial state assert 2 in anime.episodeDict[1] assert 2 in anime.episodeDict[2] # Remove last episode of season 1 (episode 2) mock_download_service._remove_episode_from_memory("multi-season-series", 1, 2) # Season 1 should be completely removed assert 1 not in anime.episodeDict, "Season 1 should be removed" # Season 2 should still exist assert 2 in anime.episodeDict, "Season 2 should still exist" class TestDownloadWithoutDataFile: """Verify downloads work without data file (in-memory only).""" @pytest.fixture def temp_dir(self): """Create temp directory without data files.""" with tempfile.TemporaryDirectory() as tmp: yield Path(tmp) @pytest.fixture def mock_anime_service(self, temp_dir): """Create mock anime service with app but no data file.""" anime_service = MagicMock() anime_service._directory = str(temp_dir) anime = make_anime( key="memory-only-series", name="Memory Only Series", site="https://example.com", folder="Memory Only Series", episode_dict={1: [1, 2, 3]}, ) mock_app = MagicMock() mock_app.list.keyDict = {"memory-only-series": anime} mock_app.list.GetMissingEpisode.return_value = [anime] mock_app.series_list = [anime] anime_service._app = mock_app anime_service._cached_list_missing = MagicMock() anime_service._broadcast_series_updated = AsyncMock() return anime_service @pytest.fixture def mock_download_service(self, mock_anime_service): """Create download service with mocked dependencies.""" service = DownloadService( anime_service=mock_anime_service, queue_repository=MagicMock(), max_retries=3, ) service._directory = str(mock_anime_service._directory) yield service @pytest.mark.asyncio async def test_download_works_without_data_file( self, mock_download_service, mock_anime_service ): """Verify downloads work even when no data file exists on disk.""" anime = mock_anime_service._app.list.keyDict["memory-only-series"] data_path = Path(mock_anime_service._directory) / "Memory Only Series" / "data" # Verify no data file exists assert not data_path.exists(), "No data file should exist" # Simulate download completion # This should NOT raise even without data file mock_download_service._remove_episode_from_memory("memory-only-series", 1, 2) # Episode should be removed from in-memory state assert 2 not in anime.episodeDict[1], "Episode should be removed from memory" # Data file should still not exist (no file created) assert not data_path.exists(), "No data file should be created"