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

@@ -10,19 +10,19 @@ Tests the functionality of SeriesApp including:
- Error handling
"""
from unittest.mock import AsyncMock, Mock, patch
from unittest.mock import AsyncMock, MagicMock, Mock, patch
import pytest
from src.core.SeriesApp import SeriesApp
from src.server.SeriesApp import SeriesApp
class TestSeriesAppInitialization:
"""Test SeriesApp initialization."""
@patch('src.core.SeriesApp.Loaders')
@patch('src.core.SeriesApp.SerieScanner')
@patch('src.core.SeriesApp.SerieList')
@patch('src.server.SeriesApp.Loaders')
@patch('src.server.SeriesApp.SerieScanner')
@patch('src.server.SeriesApp.SerieList')
def test_init_success(
self, mock_serie_list, mock_scanner, mock_loaders
):
@@ -37,7 +37,7 @@ class TestSeriesAppInitialization:
mock_loaders.assert_called_once()
mock_scanner.assert_called_once()
@patch('src.core.SeriesApp.Loaders')
@patch('src.server.SeriesApp.Loaders')
def test_init_failure_raises_error(self, mock_loaders):
"""Test that initialization failure raises error."""
test_dir = "/test/anime"
@@ -49,10 +49,10 @@ class TestSeriesAppInitialization:
with pytest.raises(RuntimeError):
SeriesApp(test_dir)
@patch('src.core.SeriesApp.Loaders')
@patch('src.core.SeriesApp.SerieScanner')
@patch('src.core.SeriesApp.SerieList')
@patch('src.core.SeriesApp.settings')
@patch('src.server.SeriesApp.Loaders')
@patch('src.server.SeriesApp.SerieScanner')
@patch('src.server.SeriesApp.SerieList')
@patch('src.server.SeriesApp.settings')
def test_init_uses_config_fallback_for_nfo_service(
self,
mock_settings,
@@ -71,9 +71,9 @@ class TestSeriesAppSearch:
"""Test search functionality."""
@pytest.mark.asyncio
@patch('src.core.SeriesApp.Loaders')
@patch('src.core.SeriesApp.SerieScanner')
@patch('src.core.SeriesApp.SerieList')
@patch('src.server.SeriesApp.Loaders')
@patch('src.server.SeriesApp.SerieScanner')
@patch('src.server.SeriesApp.SerieList')
async def test_search_success(
self, mock_serie_list, mock_scanner, mock_loaders
):
@@ -96,9 +96,9 @@ class TestSeriesAppSearch:
app.loader.search.assert_called_once_with("test anime")
@pytest.mark.asyncio
@patch('src.core.SeriesApp.Loaders')
@patch('src.core.SeriesApp.SerieScanner')
@patch('src.core.SeriesApp.SerieList')
@patch('src.server.SeriesApp.Loaders')
@patch('src.server.SeriesApp.SerieScanner')
@patch('src.server.SeriesApp.SerieList')
async def test_search_failure_raises_error(
self, mock_serie_list, mock_scanner, mock_loaders
):
@@ -120,9 +120,9 @@ class TestSeriesAppDownload:
"""Test download functionality."""
@pytest.mark.asyncio
@patch('src.core.SeriesApp.Loaders')
@patch('src.core.SeriesApp.SerieScanner')
@patch('src.core.SeriesApp.SerieList')
@patch('src.server.SeriesApp.Loaders')
@patch('src.server.SeriesApp.SerieScanner')
@patch('src.server.SeriesApp.SerieList')
async def test_download_success(
self, mock_serie_list, mock_scanner, mock_loaders, tmp_path
):
@@ -157,9 +157,9 @@ class TestSeriesAppDownload:
assert os.path.exists(folder_path)
@pytest.mark.asyncio
@patch('src.core.SeriesApp.Loaders')
@patch('src.core.SeriesApp.SerieScanner')
@patch('src.core.SeriesApp.SerieList')
@patch('src.server.SeriesApp.Loaders')
@patch('src.server.SeriesApp.SerieScanner')
@patch('src.server.SeriesApp.SerieList')
async def test_download_with_progress_callback(
self, mock_serie_list, mock_scanner, mock_loaders, tmp_path
):
@@ -197,9 +197,9 @@ class TestSeriesAppDownload:
app.loader.download.assert_called_once()
@pytest.mark.asyncio
@patch('src.core.SeriesApp.Loaders')
@patch('src.core.SeriesApp.SerieScanner')
@patch('src.core.SeriesApp.SerieList')
@patch('src.server.SeriesApp.Loaders')
@patch('src.server.SeriesApp.SerieScanner')
@patch('src.server.SeriesApp.SerieList')
async def test_download_cancellation(
self, mock_serie_list, mock_scanner, mock_loaders, tmp_path
):
@@ -234,9 +234,9 @@ class TestSeriesAppDownload:
assert app._events.download_status.called
@pytest.mark.asyncio
@patch('src.core.SeriesApp.Loaders')
@patch('src.core.SeriesApp.SerieScanner')
@patch('src.core.SeriesApp.SerieList')
@patch('src.server.SeriesApp.Loaders')
@patch('src.server.SeriesApp.SerieScanner')
@patch('src.server.SeriesApp.SerieList')
async def test_download_failure(
self, mock_serie_list, mock_scanner, mock_loaders
):
@@ -268,9 +268,9 @@ class TestSeriesAppReScan:
"""Test directory scanning functionality."""
@pytest.mark.asyncio
@patch('src.core.SeriesApp.Loaders')
@patch('src.core.SeriesApp.SerieScanner')
@patch('src.core.SeriesApp.SerieList')
@patch('src.server.SeriesApp.Loaders')
@patch('src.server.SeriesApp.SerieScanner')
@patch('src.server.SeriesApp.SerieList')
async def test_rescan_success(
self, mock_serie_list, mock_scanner, mock_loaders
):
@@ -295,9 +295,9 @@ class TestSeriesAppReScan:
app.serie_scanner.scan.assert_called_once()
@pytest.mark.asyncio
@patch('src.core.SeriesApp.Loaders')
@patch('src.core.SeriesApp.SerieScanner')
@patch('src.core.SeriesApp.SerieList')
@patch('src.server.SeriesApp.Loaders')
@patch('src.server.SeriesApp.SerieScanner')
@patch('src.server.SeriesApp.SerieList')
async def test_rescan_with_events(
self, mock_serie_list, mock_scanner, mock_loaders
):
@@ -327,9 +327,9 @@ class TestSeriesAppReScan:
app.serie_scanner.unsubscribe_on_progress.assert_called_once()
@pytest.mark.asyncio
@patch('src.core.SeriesApp.Loaders')
@patch('src.core.SeriesApp.SerieScanner')
@patch('src.core.SeriesApp.SerieList')
@patch('src.server.SeriesApp.Loaders')
@patch('src.server.SeriesApp.SerieScanner')
@patch('src.server.SeriesApp.SerieList')
async def test_rescan_cancellation(
self, mock_serie_list, mock_scanner, mock_loaders
):
@@ -359,9 +359,9 @@ class TestSeriesAppReScan:
class TestSeriesAppCancellation:
"""Test operation cancellation."""
@patch('src.core.SeriesApp.Loaders')
@patch('src.core.SeriesApp.SerieScanner')
@patch('src.core.SeriesApp.SerieList')
@patch('src.server.SeriesApp.Loaders')
@patch('src.server.SeriesApp.SerieScanner')
@patch('src.server.SeriesApp.SerieList')
def test_cancel_operation_when_running(
self, mock_serie_list, mock_scanner, mock_loaders
):
@@ -373,9 +373,9 @@ class TestSeriesAppCancellation:
# as the cancel mechanism may have changed
pass
@patch('src.core.SeriesApp.Loaders')
@patch('src.core.SeriesApp.SerieScanner')
@patch('src.core.SeriesApp.SerieList')
@patch('src.server.SeriesApp.Loaders')
@patch('src.server.SeriesApp.SerieScanner')
@patch('src.server.SeriesApp.SerieList')
def test_cancel_operation_when_idle(
self, mock_serie_list, mock_scanner, mock_loaders
):
@@ -387,9 +387,9 @@ class TestSeriesAppCancellation:
class TestSeriesAppGetters:
"""Test getter methods."""
@patch('src.core.SeriesApp.Loaders')
@patch('src.core.SeriesApp.SerieScanner')
@patch('src.core.SeriesApp.SerieList')
@patch('src.server.SeriesApp.Loaders')
@patch('src.server.SeriesApp.SerieScanner')
@patch('src.server.SeriesApp.SerieList')
def test_get_series_list(
self, mock_serie_list, mock_scanner, mock_loaders
):
@@ -400,9 +400,9 @@ class TestSeriesAppGetters:
# Verify app was created
assert app is not None
@patch('src.core.SeriesApp.Loaders')
@patch('src.core.SeriesApp.SerieScanner')
@patch('src.core.SeriesApp.SerieList')
@patch('src.server.SeriesApp.Loaders')
@patch('src.server.SeriesApp.SerieScanner')
@patch('src.server.SeriesApp.SerieList')
def test_get_operation_status(
self, mock_serie_list, mock_scanner, mock_loaders
):
@@ -410,9 +410,9 @@ class TestSeriesAppGetters:
# Skip - operation status API may have changed
pass
@patch('src.core.SeriesApp.Loaders')
@patch('src.core.SeriesApp.SerieScanner')
@patch('src.core.SeriesApp.SerieList')
@patch('src.server.SeriesApp.Loaders')
@patch('src.server.SeriesApp.SerieScanner')
@patch('src.server.SeriesApp.SerieList')
def test_get_current_operation(
self, mock_serie_list, mock_scanner, mock_loaders
):
@@ -424,9 +424,9 @@ class TestSeriesAppGetters:
class TestSeriesAppDatabaseInit:
"""Test SeriesApp initialization (no database support in core)."""
@patch('src.core.SeriesApp.Loaders')
@patch('src.core.SeriesApp.SerieScanner')
@patch('src.core.SeriesApp.SerieList')
@patch('src.server.SeriesApp.Loaders')
@patch('src.server.SeriesApp.SerieScanner')
@patch('src.server.SeriesApp.SerieList')
def test_init_creates_components(
self, mock_serie_list, mock_scanner, mock_loaders
):
@@ -446,45 +446,39 @@ class TestSeriesAppDatabaseInit:
class TestSeriesAppLoadSeriesFromList:
"""Test SeriesApp load_series_from_list method."""
@patch('src.core.SeriesApp.Loaders')
@patch('src.core.SeriesApp.SerieScanner')
@patch('src.core.SeriesApp.SerieList')
@patch('src.server.SeriesApp.Loaders')
@patch('src.server.SeriesApp.SerieScanner')
@patch('src.server.SeriesApp.SerieList')
def test_load_series_from_list_populates_keydict(
self, mock_serie_list, mock_scanner, mock_loaders
):
"""Test load_series_from_list populates the list correctly."""
from src.core.entities.series import Serie
from src.server.database.models import AnimeSeries
test_dir = "/test/anime"
mock_list = Mock()
mock_list.GetMissingEpisode.return_value = []
mock_list.keyDict = {}
mock_serie_list.return_value = mock_list
# Create app
app = SeriesApp(test_dir)
# Create test series
test_series = [
Serie(
key="anime1",
name="Anime 1",
site="aniworld.to",
folder="Anime 1",
episodeDict={1: [1, 2]}
),
Serie(
key="anime2",
name="Anime 2",
site="aniworld.to",
folder="Anime 2",
episodeDict={1: [1]}
),
]
# Create test series (AnimeSeries mocks)
def make_anime(key, name, folder):
anime = MagicMock(spec=AnimeSeries)
anime.key = key
anime.name = name
anime.site = "aniworld.to"
anime.folder = folder
anime.episodeDict = {1: [1, 2]} if key == "anime1" else {1: [1]}
return anime
test_series = [make_anime("anime1", "Anime 1", "Anime 1"), make_anime("anime2", "Anime 2", "Anime 2")]
# Load series
app.load_series_from_list(test_series)
# Verify series were loaded
assert "anime1" in mock_list.keyDict
assert "anime2" in mock_list.keyDict
@@ -493,33 +487,30 @@ class TestSeriesAppLoadSeriesFromList:
class TestSeriesAppGetAllSeriesFromDataFiles:
"""Test get_all_series_from_data_files() functionality."""
@patch('src.core.SeriesApp.Loaders')
@patch('src.core.SeriesApp.SerieScanner')
@patch('src.core.SeriesApp.SerieList')
@patch('src.server.SeriesApp.Loaders')
@patch('src.server.SeriesApp.SerieScanner')
@patch('src.server.SeriesApp.SerieList')
def test_returns_list_of_series(
self, mock_serie_list_class, mock_scanner, mock_loaders
):
"""Test that get_all_series_from_data_files returns a list of Serie."""
from src.core.entities.series import Serie
"""Test that get_all_series_from_data_files returns a list of AnimeSeries."""
from src.server.database.models import AnimeSeries
test_dir = "/test/anime"
def make_anime(key, name, folder):
anime = MagicMock(spec=AnimeSeries)
anime.key = key
anime.name = name
anime.site = "https://aniworld.to"
anime.folder = folder
anime.episodeDict = {1: [1, 2, 3]} if key == "anime1" else {1: [1, 2]}
return anime
# Mock series to return
mock_series = [
Serie(
key="anime1",
name="Anime 1",
site="https://aniworld.to",
folder="Anime 1 (2020)",
episodeDict={1: [1, 2, 3]}
),
Serie(
key="anime2",
name="Anime 2",
site="https://aniworld.to",
folder="Anime 2 (2021)",
episodeDict={1: [1]}
),
make_anime("anime1", "Anime 1", "Anime 1 (2020)"),
make_anime("anime2", "Anime 2", "Anime 2 (2021)"),
]
# Setup mock for the main SerieList instance (constructor call)
@@ -539,16 +530,16 @@ class TestSeriesAppGetAllSeriesFromDataFiles:
# Call the method
result = app.get_all_series_from_data_files()
# Verify result is a list of Serie
# Verify result is a list of AnimeSeries
assert isinstance(result, list)
assert len(result) == 2
assert all(isinstance(s, Serie) for s in result)
assert all(isinstance(s, MagicMock) for s in result)
assert result[0].key == "anime1"
assert result[1].key == "anime2"
@patch('src.core.SeriesApp.Loaders')
@patch('src.core.SeriesApp.SerieScanner')
@patch('src.core.SeriesApp.SerieList')
@patch('src.server.SeriesApp.Loaders')
@patch('src.server.SeriesApp.SerieScanner')
@patch('src.server.SeriesApp.SerieList')
def test_returns_empty_list_when_no_data_files(
self, mock_serie_list_class, mock_scanner, mock_loaders
):
@@ -575,9 +566,9 @@ class TestSeriesAppGetAllSeriesFromDataFiles:
assert isinstance(result, list)
assert len(result) == 0
@patch('src.core.SeriesApp.Loaders')
@patch('src.core.SeriesApp.SerieScanner')
@patch('src.core.SeriesApp.SerieList')
@patch('src.server.SeriesApp.Loaders')
@patch('src.server.SeriesApp.SerieScanner')
@patch('src.server.SeriesApp.SerieList')
def test_handles_exception_gracefully(
self, mock_serie_list_class, mock_scanner, mock_loaders
):
@@ -604,13 +595,13 @@ class TestSeriesAppGetAllSeriesFromDataFiles:
assert isinstance(result, list)
assert len(result) == 0
@patch('src.core.SeriesApp.Loaders')
@patch('src.core.SeriesApp.SerieScanner')
@patch('src.core.SeriesApp.SerieList')
@patch('src.server.SeriesApp.Loaders')
@patch('src.server.SeriesApp.SerieScanner')
@patch('src.server.SeriesApp.SerieList')
def test_uses_file_based_loading(
self, mock_serie_list_class, mock_scanner, mock_loaders
):
"""Test that method uses file-based loading (no db_session)."""
"""Test that method uses SerieList for file-based loading."""
test_dir = "/test/anime"
# Setup mock for the main SerieList instance
@@ -629,24 +620,23 @@ class TestSeriesAppGetAllSeriesFromDataFiles:
# Call the method
app.get_all_series_from_data_files()
# Verify the second SerieList was created with correct params
# (file-based loading: db_session=None, skip_load=False)
# Verify SerieList was called twice (main + temp)
calls = mock_serie_list_class.call_args_list
assert len(calls) == 2
# Check the second call (for get_all_series_from_data_files)
# Check the second call is for temp SerieList with directory
second_call = calls[1]
assert second_call.kwargs.get('db_session') is None
assert second_call.kwargs.get('skip_load') is False
# base_path is passed as positional argument
assert second_call.args[0] == test_dir
@patch('src.core.SeriesApp.Loaders')
@patch('src.core.SeriesApp.SerieScanner')
@patch('src.core.SeriesApp.SerieList')
@patch('src.server.SeriesApp.Loaders')
@patch('src.server.SeriesApp.SerieScanner')
@patch('src.server.SeriesApp.SerieList')
def test_does_not_modify_main_list(
self, mock_serie_list_class, mock_scanner, mock_loaders
):
"""Test that method does not modify the main SerieList instance."""
from src.core.entities.series import Serie
from src.server.database.models import AnimeSeries
test_dir = "/test/anime"
@@ -657,15 +647,13 @@ class TestSeriesAppGetAllSeriesFromDataFiles:
# Setup mock for the temporary SerieList
mock_temp_list = Mock()
mock_temp_list.get_all.return_value = [
Serie(
key="anime1",
name="Anime 1",
site="https://aniworld.to",
folder="Anime 1",
episodeDict={}
)
]
anime = MagicMock(spec=AnimeSeries)
anime.key = "anime1"
anime.name = "Anime 1"
anime.site = "https://aniworld.to"
anime.folder = "Anime 1"
anime.episodeDict = {}
mock_temp_list.get_all.return_value = [anime]
mock_serie_list_class.side_effect = [mock_main_list, mock_temp_list]