Files
Aniworld/tests/integration/test_episode_download_sync.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

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"