- 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)
256 lines
9.8 KiB
Python
256 lines
9.8 KiB
Python
"""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" |