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)
This commit is contained in:
2026-06-04 21:11:53 +02:00
parent 09d454d4c0
commit 5526ab884a
76 changed files with 1186 additions and 3574 deletions

View File

@@ -1,9 +1,10 @@
"""Integration tests for episode download sync with data file updates.
"""Integration tests for episode download sync with in-memory updates.
Tests verify that when episodes are downloaded successfully:
- In-memory Serie.episodeDict is updated
- Deprecated data file is updated (if it exists)
- 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
@@ -14,12 +15,24 @@ from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from src.core.entities.series import Serie
from src.core.SeriesApp import SeriesApp
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."""
@@ -35,18 +48,17 @@ class TestEpisodeRemovedFromMissingListAfterDownload:
anime_service = MagicMock()
anime_service._directory = str(temp_dir)
# Create mock app withSerie with missing episodes
serie = Serie(
anime = make_anime(
key="test-series",
name="Test Series",
site="https://example.com",
folder="Test Series",
episodeDict={1: [1, 2, 3]},
episode_dict={1: [1, 2, 3]},
)
mock_app = MagicMock()
mock_app.list.keyDict = {"test-series": serie}
mock_app.list.GetMissingEpisode.return_value = [serie]
mock_app.series_list = [serie]
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()
@@ -62,7 +74,7 @@ class TestEpisodeRemovedFromMissingListAfterDownload:
queue_repository=MagicMock(),
max_retries=3,
)
service._directory = tmp
service._directory = str(mock_anime_service._directory)
yield service
@pytest.mark.asyncio
@@ -70,24 +82,24 @@ class TestEpisodeRemovedFromMissingListAfterDownload:
self, mock_download_service, mock_anime_service
):
"""Verify episode no longer appears in missing list after download completes."""
serie = mock_anime_service._app.list.keyDict["test-series"]
anime = mock_anime_service._app.list.keyDict["test-series"]
# Verify episode starts in missing list
assert 2 in serie.episodeDict[1], "Episode should start 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 serie.episodeDict[1], "Episode should be removed from missing list"
assert serie.episodeDict[1] == [1, 3]
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 Serie.episodeDict is updated after download."""
"""Verify in-memory AnimeSeries.episodeDict is updated after download."""
@pytest.fixture
def mock_anime_service(self):
@@ -95,21 +107,20 @@ class TestDownloadUpdatesInMemoryCache:
anime_service = MagicMock()
anime_service._directory = "/tmp/test"
# Create mock app with series having multiple seasons and episodes
serie = Serie(
anime = make_anime(
key="multi-season-series",
name="Multi Season Series",
site="https://example.com",
folder="Multi Season Series",
episodeDict={
episode_dict={
1: [1, 2, 3, 4, 5],
2: [1, 2, 3],
},
)
mock_app = MagicMock()
mock_app.list.keyDict = {"multi-season-series": serie}
mock_app.list.GetMissingEpisode.return_value = [serie]
mock_app.series_list = [serie]
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()
@@ -125,23 +136,22 @@ class TestDownloadUpdatesInMemoryCache:
queue_repository=MagicMock(),
max_retries=3,
)
service._directory = tmp
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 Serie.episodeDict is updated after download."""
# First reset to known state (remove the defaults first call might have set)
serie = mock_anime_service._app.list.keyDict["multi-season-series"]
"""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
serie.episodeDict = {1: [1, 2, 3, 4, 5], 2: [1, 2, 3]}
anime.episodeDict = {1: [1, 2, 3, 4, 5], 2: [1, 2, 3]}
# Verify preconditions
assert 1 in serie.episodeDict[1]
assert 3 in serie.episodeDict[2]
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)
@@ -149,125 +159,39 @@ class TestDownloadUpdatesInMemoryCache:
mock_download_service._remove_episode_from_memory("multi-season-series", 2, 2)
# Verify episodes removed
assert 1 not in serie.episodeDict[1], "Episode 1 of season 1 should be removed"
assert 3 not in serie.episodeDict[1], "Episode 3 of season 1 should be removed"
assert 2 in serie.episodeDict[1], "Episode 2 of season 1 should remain"
assert 3 in serie.episodeDict[2], "Episode 3 of season 2 should remain"
assert 2 not in serie.episodeDict[2], "Episode 2 of season 2 should be 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 serie.episodeDict, "Season 2 should still exist (has episode 1, 3)"
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."""
# Modify the series so season 1 only has episode 2 left
serie = mock_anime_service._app.list.keyDict["multi-season-series"]
anime = mock_anime_service._app.list.keyDict["multi-season-series"]
# Reset and set to proper test state
serie.episodeDict = {1: [2], 2: [1, 2, 3]} # Season 1 only has episode 2
anime.episodeDict = {1: [2], 2: [1, 2, 3]} # Season 1 only has episode 2
# Verify initial state
assert 2 in serie.episodeDict[1]
assert 2 in serie.episodeDict[2]
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 serie.episodeDict, "Season 1 should be removed"
assert 1 not in anime.episodeDict, "Season 1 should be removed"
# Season 2 should still exist
assert 2 in serie.episodeDict, "Season 2 should still exist"
assert 2 in anime.episodeDict, "Season 2 should still exist"
class TestDataFileUpdatedAfterDownload:
"""Verify data file is updated after download (when it exists)."""
@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)
# Create series folder with data file
series_folder = temp_dir / "Test Series"
series_folder.mkdir()
data_path = series_folder / "data"
serie = Serie(
key="test-series-with-data",
name="Test Series",
site="https://example.com",
folder="Test Series",
episodeDict={1: [1, 2, 3]},
)
# Save data file to disk
import warnings
with warnings.catch_warnings():
warnings.simplefilter("ignore", DeprecationWarning)
serie.save_to_file(str(data_path))
# Update episodeDict to simulate in-progress download state
# (episodeDict still has all episodes; will be updated after download)
mock_app = MagicMock()
mock_app.list.keyDict = {"test-series-with-data": serie}
mock_app.list.GetMissingEpisode.return_value = [serie]
mock_app.series_list = [serie]
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_data_file_updated_after_download(
self, mock_download_service, mock_anime_service, temp_dir
):
"""Verify data file is updated after download when data file exists."""
serie = mock_anime_service._app.list.keyDict["test-series-with-data"]
data_path = temp_dir / "Test Series" / "data"
# Verify data file exists before test
assert data_path.exists(), "Data file should exist before test"
# Read original data file
with open(data_path) as f:
original_data = json.load(f)
assert 2 in original_data["episodeDict"]["1"], "Episode should be in original data"
# Simulate download completion
mock_download_service._remove_episode_from_memory("test-series-with-data", 1, 2)
# Read updated data file
with open(data_path) as f:
updated_data = json.load(f)
# Verify episode 2 was removed from data file
assert 2 not in updated_data["episodeDict"]["1"], "Episode should be removed from data file"
assert updated_data["episodeDict"]["1"] == [1, 3]
class TestDataFileNotRequiredForDownload:
"""Verify downloads work even when data file doesn't exist."""
class TestDownloadWithoutDataFile:
"""Verify downloads work without data file (in-memory only)."""
@pytest.fixture
def temp_dir(self):
@@ -281,19 +205,18 @@ class TestDataFileNotRequiredForDownload:
anime_service = MagicMock()
anime_service._directory = str(temp_dir)
# Create series with NO data file on disk (only in memory)
serie = Serie(
anime = make_anime(
key="memory-only-series",
name="Memory Only Series",
site="https://example.com",
folder="Memory Only Series",
episodeDict={1: [1, 2, 3]},
episode_dict={1: [1, 2, 3]},
)
mock_app = MagicMock()
mock_app.list.keyDict = {"memory-only-series": serie}
mock_app.list.GetMissingEpisode.return_value = [serie]
mock_app.series_list = [serie]
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()
@@ -316,7 +239,7 @@ class TestDataFileNotRequiredForDownload:
self, mock_download_service, mock_anime_service
):
"""Verify downloads work even when no data file exists on disk."""
serie = mock_anime_service._app.list.keyDict["memory-only-series"]
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
@@ -327,7 +250,7 @@ class TestDataFileNotRequiredForDownload:
mock_download_service._remove_episode_from_memory("memory-only-series", 1, 2)
# Episode should be removed from in-memory state
assert 2 not in serie.episodeDict[1], "Episode should be removed from memory"
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"
assert not data_path.exists(), "No data file should be created"