refactor: remove database access from core layer
- Remove db_session parameter from SeriesApp, SerieList, SerieScanner - Move all database operations to AnimeService (service layer) - Add add_series_to_db, contains_in_db methods to AnimeService - Update sync_series_from_data_files to use inline DB operations - Remove obsolete test classes for removed DB methods - Fix pylint issues: add broad-except comments, fix line lengths - Core layer (src/core/) now has zero database imports 722 unit tests pass
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
import os
|
||||
import tempfile
|
||||
import warnings
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -30,41 +30,6 @@ def sample_serie():
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_db_session():
|
||||
"""Create a mock async database session."""
|
||||
session = AsyncMock()
|
||||
return session
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_anime_series():
|
||||
"""Create a mock AnimeSeries database model."""
|
||||
anime_series = MagicMock()
|
||||
anime_series.key = "test-series"
|
||||
anime_series.name = "Test Series"
|
||||
anime_series.site = "https://aniworld.to/anime/stream/test-series"
|
||||
anime_series.folder = "Test Series (2020)"
|
||||
# Mock episodes relationship
|
||||
mock_ep1 = MagicMock()
|
||||
mock_ep1.season = 1
|
||||
mock_ep1.episode_number = 1
|
||||
mock_ep2 = MagicMock()
|
||||
mock_ep2.season = 1
|
||||
mock_ep2.episode_number = 2
|
||||
mock_ep3 = MagicMock()
|
||||
mock_ep3.season = 1
|
||||
mock_ep3.episode_number = 3
|
||||
mock_ep4 = MagicMock()
|
||||
mock_ep4.season = 2
|
||||
mock_ep4.episode_number = 1
|
||||
mock_ep5 = MagicMock()
|
||||
mock_ep5.season = 2
|
||||
mock_ep5.episode_number = 2
|
||||
anime_series.episodes = [mock_ep1, mock_ep2, mock_ep3, mock_ep4, mock_ep5]
|
||||
return anime_series
|
||||
|
||||
|
||||
class TestSerieListKeyBasedStorage:
|
||||
"""Test SerieList uses key for internal storage."""
|
||||
|
||||
@@ -261,238 +226,18 @@ class TestSerieListPublicAPI:
|
||||
assert serie_list.get_by_folder(sample_serie.folder) is not None
|
||||
|
||||
|
||||
class TestSerieListDatabaseMode:
|
||||
"""Test SerieList database-backed storage functionality."""
|
||||
|
||||
def test_init_with_db_session_skips_file_load(
|
||||
self, temp_directory, mock_db_session
|
||||
):
|
||||
"""Test initialization with db_session skips file-based loading."""
|
||||
# Create a data file that should NOT be loaded
|
||||
folder_path = os.path.join(temp_directory, "Test Folder")
|
||||
os.makedirs(folder_path, exist_ok=True)
|
||||
data_path = os.path.join(folder_path, "data")
|
||||
|
||||
serie = Serie(
|
||||
key="test-key",
|
||||
name="Test",
|
||||
site="https://test.com",
|
||||
folder="Test Folder",
|
||||
episodeDict={}
|
||||
)
|
||||
serie.save_to_file(data_path)
|
||||
|
||||
# Initialize with db_session - should skip file loading
|
||||
serie_list = SerieList(
|
||||
temp_directory,
|
||||
db_session=mock_db_session
|
||||
)
|
||||
|
||||
# Should have empty keyDict (file loading skipped)
|
||||
assert len(serie_list.keyDict) == 0
|
||||
class TestSerieListSkipLoad:
|
||||
"""Test SerieList initialization options."""
|
||||
|
||||
def test_init_with_skip_load(self, temp_directory):
|
||||
"""Test initialization with skip_load=True skips loading."""
|
||||
serie_list = SerieList(temp_directory, skip_load=True)
|
||||
assert len(serie_list.keyDict) == 0
|
||||
|
||||
def test_convert_from_db_basic(self, mock_anime_series):
|
||||
"""Test _convert_from_db converts AnimeSeries to Serie correctly."""
|
||||
serie = SerieList._convert_from_db(mock_anime_series)
|
||||
|
||||
assert serie.key == mock_anime_series.key
|
||||
assert serie.name == mock_anime_series.name
|
||||
assert serie.site == mock_anime_series.site
|
||||
assert serie.folder == mock_anime_series.folder
|
||||
# Season keys should be built from episodes relationship
|
||||
assert 1 in serie.episodeDict
|
||||
assert 2 in serie.episodeDict
|
||||
assert serie.episodeDict[1] == [1, 2, 3]
|
||||
assert serie.episodeDict[2] == [1, 2]
|
||||
|
||||
def test_convert_from_db_empty_episodes(self, mock_anime_series):
|
||||
"""Test _convert_from_db handles empty episodes."""
|
||||
mock_anime_series.episodes = []
|
||||
|
||||
serie = SerieList._convert_from_db(mock_anime_series)
|
||||
|
||||
assert serie.episodeDict == {}
|
||||
|
||||
def test_convert_from_db_none_episodes(self, mock_anime_series):
|
||||
"""Test _convert_from_db handles None episodes."""
|
||||
mock_anime_series.episodes = None
|
||||
|
||||
serie = SerieList._convert_from_db(mock_anime_series)
|
||||
|
||||
assert serie.episodeDict == {}
|
||||
|
||||
def test_convert_to_db_dict(self, sample_serie):
|
||||
"""Test _convert_to_db_dict creates correct dictionary."""
|
||||
result = SerieList._convert_to_db_dict(sample_serie)
|
||||
|
||||
assert result["key"] == sample_serie.key
|
||||
assert result["name"] == sample_serie.name
|
||||
assert result["site"] == sample_serie.site
|
||||
assert result["folder"] == sample_serie.folder
|
||||
# episode_dict should not be in result anymore
|
||||
assert "episode_dict" not in result
|
||||
|
||||
def test_convert_to_db_dict_empty_episode_dict(self):
|
||||
"""Test _convert_to_db_dict handles empty episode_dict."""
|
||||
serie = Serie(
|
||||
key="test",
|
||||
name="Test",
|
||||
site="https://test.com",
|
||||
folder="Test",
|
||||
episodeDict={}
|
||||
)
|
||||
|
||||
result = SerieList._convert_to_db_dict(serie)
|
||||
|
||||
# episode_dict should not be in result anymore
|
||||
assert "episode_dict" not in result
|
||||
|
||||
|
||||
class TestSerieListDatabaseAsync:
|
||||
"""Test async database methods of SerieList."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_load_series_from_db(
|
||||
self, temp_directory, mock_db_session, mock_anime_series
|
||||
):
|
||||
"""Test load_series_from_db loads from database."""
|
||||
# Setup mock to return list of anime series
|
||||
with patch(
|
||||
'src.server.database.service.AnimeSeriesService'
|
||||
) as mock_service:
|
||||
mock_service.get_all = AsyncMock(return_value=[mock_anime_series])
|
||||
|
||||
serie_list = SerieList(temp_directory, skip_load=True)
|
||||
count = await serie_list.load_series_from_db(mock_db_session)
|
||||
|
||||
assert count == 1
|
||||
assert mock_anime_series.key in serie_list.keyDict
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_load_series_from_db_clears_existing(
|
||||
self, temp_directory, mock_db_session, mock_anime_series
|
||||
):
|
||||
"""Test load_series_from_db clears existing data."""
|
||||
serie_list = SerieList(temp_directory, skip_load=True)
|
||||
# Add an existing entry
|
||||
serie_list.keyDict["old-key"] = MagicMock()
|
||||
|
||||
with patch(
|
||||
'src.server.database.service.AnimeSeriesService'
|
||||
) as mock_service:
|
||||
mock_service.get_all = AsyncMock(return_value=[mock_anime_series])
|
||||
|
||||
await serie_list.load_series_from_db(mock_db_session)
|
||||
|
||||
# Old entry should be cleared
|
||||
assert "old-key" not in serie_list.keyDict
|
||||
assert mock_anime_series.key in serie_list.keyDict
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_to_db_creates_new_series(
|
||||
self, temp_directory, mock_db_session, sample_serie
|
||||
):
|
||||
"""Test add_to_db creates new series in database."""
|
||||
with patch(
|
||||
'src.server.database.service.AnimeSeriesService'
|
||||
) as mock_service:
|
||||
mock_service.get_by_key = AsyncMock(return_value=None)
|
||||
mock_created = MagicMock()
|
||||
mock_created.id = 1
|
||||
mock_service.create = AsyncMock(return_value=mock_created)
|
||||
|
||||
serie_list = SerieList(temp_directory, skip_load=True)
|
||||
result = await serie_list.add_to_db(sample_serie, mock_db_session)
|
||||
|
||||
assert result is mock_created
|
||||
mock_service.create.assert_called_once()
|
||||
# Should also add to in-memory collection
|
||||
assert sample_serie.key in serie_list.keyDict
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_to_db_skips_existing(
|
||||
self, temp_directory, mock_db_session, sample_serie
|
||||
):
|
||||
"""Test add_to_db skips if series already exists."""
|
||||
with patch(
|
||||
'src.server.database.service.AnimeSeriesService'
|
||||
) as mock_service:
|
||||
existing = MagicMock()
|
||||
mock_service.get_by_key = AsyncMock(return_value=existing)
|
||||
|
||||
serie_list = SerieList(temp_directory, skip_load=True)
|
||||
result = await serie_list.add_to_db(sample_serie, mock_db_session)
|
||||
|
||||
assert result is None
|
||||
mock_service.create.assert_not_called()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_contains_in_db_returns_true_when_exists(
|
||||
self, temp_directory, mock_db_session
|
||||
):
|
||||
"""Test contains_in_db returns True when series exists."""
|
||||
with patch(
|
||||
'src.server.database.service.AnimeSeriesService'
|
||||
) as mock_service:
|
||||
mock_service.get_by_key = AsyncMock(return_value=MagicMock())
|
||||
|
||||
serie_list = SerieList(temp_directory, skip_load=True)
|
||||
result = await serie_list.contains_in_db(
|
||||
"test-key", mock_db_session
|
||||
)
|
||||
|
||||
assert result is True
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_contains_in_db_returns_false_when_not_exists(
|
||||
self, temp_directory, mock_db_session
|
||||
):
|
||||
"""Test contains_in_db returns False when series doesn't exist."""
|
||||
with patch(
|
||||
'src.server.database.service.AnimeSeriesService'
|
||||
) as mock_service:
|
||||
mock_service.get_by_key = AsyncMock(return_value=None)
|
||||
|
||||
serie_list = SerieList(temp_directory, skip_load=True)
|
||||
result = await serie_list.contains_in_db(
|
||||
"nonexistent", mock_db_session
|
||||
)
|
||||
|
||||
assert result is False
|
||||
|
||||
|
||||
class TestSerieListDeprecationWarnings:
|
||||
"""Test deprecation warnings are raised for file-based methods."""
|
||||
|
||||
def test_add_raises_deprecation_warning(
|
||||
self, temp_directory, sample_serie
|
||||
):
|
||||
"""Test add() raises deprecation warning."""
|
||||
serie_list = SerieList(temp_directory, skip_load=True)
|
||||
|
||||
with warnings.catch_warnings(record=True) as w:
|
||||
warnings.simplefilter("always")
|
||||
serie_list.add(sample_serie)
|
||||
|
||||
# Check at least one deprecation warning was raised for add()
|
||||
# (Note: save_to_file also raises a warning, so we may get 2)
|
||||
deprecation_warnings = [
|
||||
warning for warning in w
|
||||
if issubclass(warning.category, DeprecationWarning)
|
||||
]
|
||||
assert len(deprecation_warnings) >= 1
|
||||
# Check that one of them is from add()
|
||||
add_warnings = [
|
||||
warning for warning in deprecation_warnings
|
||||
if "add_to_db()" in str(warning.message)
|
||||
]
|
||||
assert len(add_warnings) == 1
|
||||
|
||||
def test_get_by_folder_raises_deprecation_warning(
|
||||
self, temp_directory, sample_serie
|
||||
):
|
||||
|
||||
Reference in New Issue
Block a user