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:
2025-12-15 15:19:03 +01:00
parent 27108aacda
commit 596476f9ac
12 changed files with 877 additions and 1651 deletions

View File

@@ -19,7 +19,6 @@ from unittest.mock import AsyncMock, Mock, patch
import pytest
from src.core.entities.SerieList import SerieList
from src.core.entities.series import Serie
from src.core.SeriesApp import SeriesApp
@@ -111,81 +110,6 @@ class TestGetAllSeriesFromDataFiles:
assert len(result) == 0
class TestSerieListAddToDb:
"""Test SerieList.add_to_db() method for database insertion."""
@pytest.mark.asyncio
async def test_add_to_db_creates_record(self):
"""Test that add_to_db creates a database record."""
with tempfile.TemporaryDirectory() as tmp_dir:
serie = Serie(
key="new-anime",
name="New Anime",
site="https://aniworld.to",
folder="New Anime (2024)",
episodeDict={1: [1, 2, 3], 2: [1, 2]}
)
# Mock database session and services
mock_db = AsyncMock()
mock_anime_series = Mock()
mock_anime_series.id = 1
mock_anime_series.key = "new-anime"
mock_anime_series.name = "New Anime"
with patch(
'src.server.database.service.AnimeSeriesService'
) as mock_service, patch(
'src.server.database.service.EpisodeService'
) as mock_episode_service:
# Setup mocks
mock_service.get_by_key = AsyncMock(return_value=None)
mock_service.create = AsyncMock(return_value=mock_anime_series)
mock_episode_service.create = AsyncMock()
serie_list = SerieList(tmp_dir, skip_load=True)
result = await serie_list.add_to_db(serie, mock_db)
# Verify series was created
assert result is not None
mock_service.create.assert_called_once()
# Verify episodes were created (5 total: 3 + 2)
assert mock_episode_service.create.call_count == 5
@pytest.mark.asyncio
async def test_add_to_db_skips_existing_series(self):
"""Test that add_to_db skips existing series."""
with tempfile.TemporaryDirectory() as tmp_dir:
serie = Serie(
key="existing-anime",
name="Existing Anime",
site="https://aniworld.to",
folder="Existing Anime (2023)",
episodeDict={1: [1]}
)
mock_db = AsyncMock()
mock_existing = Mock()
mock_existing.id = 99
mock_existing.key = "existing-anime"
with patch(
'src.server.database.service.AnimeSeriesService'
) as mock_service:
# Return existing series
mock_service.get_by_key = AsyncMock(return_value=mock_existing)
mock_service.create = AsyncMock()
serie_list = SerieList(tmp_dir, skip_load=True)
result = await serie_list.add_to_db(serie, mock_db)
# Verify None returned (already exists)
assert result is None
# Verify create was NOT called
mock_service.create.assert_not_called()
class TestSyncSeriesToDatabase:
"""Test sync_series_from_data_files function from anime_service."""

View File

@@ -6,7 +6,7 @@ error handling, and progress reporting integration.
from __future__ import annotations
import asyncio
from unittest.mock import AsyncMock, MagicMock
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
@@ -183,7 +183,17 @@ class TestRescan:
self, anime_service, mock_series_app, mock_progress_service
):
"""Test successful rescan operation."""
await anime_service.rescan()
# Mock rescan to return empty list (no DB save needed)
mock_series_app.rescan.return_value = []
# Mock the database operations
with patch.object(
anime_service, '_save_scan_results_to_db', new_callable=AsyncMock
):
with patch.object(
anime_service, '_load_series_from_db', new_callable=AsyncMock
):
await anime_service.rescan()
# Verify SeriesApp.rescan was called (lowercase, not ReScan)
mock_series_app.rescan.assert_called_once()
@@ -193,7 +203,15 @@ class TestRescan:
"""Test rescan operation (callback parameter removed)."""
# Rescan no longer accepts callback parameter
# Progress is tracked via event handlers automatically
await anime_service.rescan()
mock_series_app.rescan.return_value = []
with patch.object(
anime_service, '_save_scan_results_to_db', new_callable=AsyncMock
):
with patch.object(
anime_service, '_load_series_from_db', new_callable=AsyncMock
):
await anime_service.rescan()
# Verify rescan was called
mock_series_app.rescan.assert_called_once()
@@ -207,9 +225,17 @@ class TestRescan:
# Update series list
mock_series_app.series_list = [{"name": "Test"}, {"name": "New"}]
mock_series_app.rescan.return_value = []
# Rescan should clear cache
await anime_service.rescan()
# Mock the database operations
with patch.object(
anime_service, '_save_scan_results_to_db', new_callable=AsyncMock
):
with patch.object(
anime_service, '_load_series_from_db', new_callable=AsyncMock
):
# Rescan should clear cache
await anime_service.rescan()
# Next list_missing should return updated data
result = await anime_service.list_missing()

View File

@@ -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
):

View File

@@ -1,9 +1,8 @@
"""Tests for SerieScanner class - database and file-based operations."""
"""Tests for SerieScanner class - file-based operations."""
import os
import tempfile
import warnings
from unittest.mock import AsyncMock, MagicMock, patch
from unittest.mock import MagicMock, patch
import pytest
@@ -38,13 +37,6 @@ def mock_loader():
return loader
@pytest.fixture
def mock_db_session():
"""Create a mock async database session."""
session = AsyncMock()
return session
@pytest.fixture
def sample_serie():
"""Create a sample Serie for testing."""
@@ -68,18 +60,6 @@ class TestSerieScannerInitialization:
assert scanner.loader == mock_loader
assert scanner.keyDict == {}
def test_init_with_db_session(
self, temp_directory, mock_loader, mock_db_session
):
"""Test initialization with database session."""
scanner = SerieScanner(
temp_directory,
mock_loader,
db_session=mock_db_session
)
assert scanner._db_session == mock_db_session
def test_init_empty_path_raises_error(self, mock_loader):
"""Test initialization with empty path raises ValueError."""
with pytest.raises(ValueError, match="empty"):
@@ -91,352 +71,40 @@ class TestSerieScannerInitialization:
SerieScanner("/nonexistent/path", mock_loader)
class TestSerieScannerScanDeprecation:
"""Test scan() deprecation warning."""
class TestSerieScannerScan:
"""Test file-based scan operations."""
def test_scan_raises_deprecation_warning(
self, temp_directory, mock_loader
):
"""Test that scan() raises a deprecation warning."""
scanner = SerieScanner(temp_directory, mock_loader)
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always")
# Mock the internal methods to avoid actual scanning
with patch.object(scanner, 'get_total_to_scan', return_value=0):
with patch.object(
scanner, '_SerieScanner__find_mp4_files',
return_value=iter([])
):
scanner.scan()
# Check deprecation warning was raised
assert len(w) >= 1
deprecation_warnings = [
warning for warning in w
if issubclass(warning.category, DeprecationWarning)
]
assert len(deprecation_warnings) >= 1
assert "scan_async()" in str(deprecation_warnings[0].message)
class TestSerieScannerAsyncScan:
"""Test async database scanning methods."""
@pytest.mark.asyncio
async def test_scan_async_saves_to_database(
self, temp_directory, mock_loader, mock_db_session, sample_serie
):
"""Test scan_async saves results to database."""
scanner = SerieScanner(temp_directory, mock_loader)
# Mock the internal methods
with patch.object(scanner, 'get_total_to_scan', return_value=1):
with patch.object(
scanner,
'_SerieScanner__find_mp4_files',
return_value=iter([
("Attack on Titan (2013)", ["S01E001.mp4"])
])
):
with patch.object(
scanner,
'_SerieScanner__read_data_from_file',
return_value=sample_serie
):
with patch.object(
scanner,
'_SerieScanner__get_missing_episodes_and_season',
return_value=({1: [2, 3]}, "aniworld.to")
):
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
)
await scanner.scan_async(mock_db_session)
# Verify database create was called
mock_service.create.assert_called_once()
@pytest.mark.asyncio
async def test_scan_async_updates_existing_series(
self, temp_directory, mock_loader, mock_db_session, sample_serie
):
"""Test scan_async updates existing series in database."""
scanner = SerieScanner(temp_directory, mock_loader)
# Mock existing series in database with different episodes
existing = MagicMock()
existing.id = 1
existing.folder = sample_serie.folder
# Mock episodes (different from sample_serie)
mock_existing_episodes = [
MagicMock(season=1, episode_number=5),
MagicMock(season=1, episode_number=6),
]
with patch.object(scanner, 'get_total_to_scan', return_value=1):
with patch.object(
scanner,
'_SerieScanner__find_mp4_files',
return_value=iter([
("Attack on Titan (2013)", ["S01E001.mp4"])
])
):
with patch.object(
scanner,
'_SerieScanner__read_data_from_file',
return_value=sample_serie
):
with patch.object(
scanner,
'_SerieScanner__get_missing_episodes_and_season',
return_value=({1: [2, 3]}, "aniworld.to")
):
with patch(
'src.server.database.service.AnimeSeriesService'
) as mock_service:
with patch(
'src.server.database.service.EpisodeService'
) as mock_ep_service:
mock_service.get_by_key = AsyncMock(
return_value=existing
)
mock_service.update = AsyncMock(
return_value=existing
)
mock_ep_service.get_by_series = AsyncMock(
return_value=mock_existing_episodes
)
mock_ep_service.create = AsyncMock()
await scanner.scan_async(mock_db_session)
# Verify episodes were created
assert mock_ep_service.create.called
@pytest.mark.asyncio
async def test_scan_async_handles_errors_gracefully(
self, temp_directory, mock_loader, mock_db_session
):
"""Test scan_async handles folder processing errors gracefully."""
scanner = SerieScanner(temp_directory, mock_loader)
with patch.object(scanner, 'get_total_to_scan', return_value=1):
with patch.object(
scanner,
'_SerieScanner__find_mp4_files',
return_value=iter([
("Error Folder", ["S01E001.mp4"])
])
):
with patch.object(
scanner,
'_SerieScanner__read_data_from_file',
side_effect=Exception("Test error")
):
# Should not raise, should continue
await scanner.scan_async(mock_db_session)
class TestSerieScannerDatabaseHelpers:
"""Test database helper methods."""
@pytest.mark.asyncio
async def test_save_serie_to_db_creates_new(
self, temp_directory, mock_loader, mock_db_session, sample_serie
):
"""Test _save_serie_to_db creates new series."""
scanner = SerieScanner(temp_directory, mock_loader)
with patch(
'src.server.database.service.AnimeSeriesService'
) as mock_service:
with patch(
'src.server.database.service.EpisodeService'
) as mock_ep_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)
mock_ep_service.create = AsyncMock()
result = await scanner._save_serie_to_db(
sample_serie, mock_db_session
)
assert result is mock_created
mock_service.create.assert_called_once()
@pytest.mark.asyncio
async def test_save_serie_to_db_updates_existing(
self, temp_directory, mock_loader, mock_db_session, sample_serie
):
"""Test _save_serie_to_db updates existing series."""
scanner = SerieScanner(temp_directory, mock_loader)
existing = MagicMock()
existing.id = 1
existing.folder = sample_serie.folder
# Mock existing episodes (different from sample_serie)
mock_existing_episodes = [
MagicMock(season=1, episode_number=5),
MagicMock(season=1, episode_number=6),
]
with patch(
'src.server.database.service.AnimeSeriesService'
) as mock_service:
with patch(
'src.server.database.service.EpisodeService'
) as mock_ep_service:
mock_service.get_by_key = AsyncMock(return_value=existing)
mock_service.update = AsyncMock(return_value=existing)
mock_ep_service.get_by_series = AsyncMock(
return_value=mock_existing_episodes
)
mock_ep_service.create = AsyncMock()
result = await scanner._save_serie_to_db(
sample_serie, mock_db_session
)
assert result is existing
# Should have created new episodes
assert mock_ep_service.create.called
@pytest.mark.asyncio
async def test_save_serie_to_db_skips_unchanged(
self, temp_directory, mock_loader, mock_db_session, sample_serie
):
"""Test _save_serie_to_db skips update if unchanged."""
scanner = SerieScanner(temp_directory, mock_loader)
existing = MagicMock()
existing.id = 1
existing.folder = sample_serie.folder
# Mock episodes matching sample_serie.episodeDict
mock_existing_episodes = []
for season, ep_nums in sample_serie.episodeDict.items():
for ep_num in ep_nums:
mock_existing_episodes.append(
MagicMock(season=season, episode_number=ep_num)
)
with patch(
'src.server.database.service.AnimeSeriesService'
) as mock_service:
with patch(
'src.server.database.service.EpisodeService'
) as mock_ep_service:
mock_service.get_by_key = AsyncMock(return_value=existing)
mock_ep_service.get_by_series = AsyncMock(
return_value=mock_existing_episodes
)
result = await scanner._save_serie_to_db(
sample_serie, mock_db_session
)
assert result is None
mock_service.update.assert_not_called()
@pytest.mark.asyncio
async def test_update_serie_in_db_updates_existing(
self, temp_directory, mock_loader, mock_db_session, sample_serie
):
"""Test _update_serie_in_db updates existing series."""
scanner = SerieScanner(temp_directory, mock_loader)
existing = MagicMock()
existing.id = 1
with patch(
'src.server.database.service.AnimeSeriesService'
) as mock_service:
with patch(
'src.server.database.service.EpisodeService'
) as mock_ep_service:
mock_service.get_by_key = AsyncMock(return_value=existing)
mock_service.update = AsyncMock(return_value=existing)
mock_ep_service.get_by_series = AsyncMock(return_value=[])
mock_ep_service.create = AsyncMock()
result = await scanner._update_serie_in_db(
sample_serie, mock_db_session
)
assert result is existing
mock_service.update.assert_called_once()
@pytest.mark.asyncio
async def test_update_serie_in_db_returns_none_if_not_found(
self, temp_directory, mock_loader, mock_db_session, sample_serie
):
"""Test _update_serie_in_db returns None if series not found."""
scanner = SerieScanner(temp_directory, mock_loader)
with patch(
'src.server.database.service.AnimeSeriesService'
) as mock_service:
mock_service.get_by_key = AsyncMock(return_value=None)
result = await scanner._update_serie_in_db(
sample_serie, mock_db_session
)
assert result is None
class TestSerieScannerBackwardCompatibility:
"""Test backward compatibility of file-based operations."""
def test_file_based_scan_still_works(
def test_file_based_scan_works(
self, temp_directory, mock_loader, sample_serie
):
"""Test file-based scan still works with deprecation warning."""
"""Test file-based scan works properly."""
scanner = SerieScanner(temp_directory, mock_loader)
with warnings.catch_warnings():
warnings.simplefilter("ignore", DeprecationWarning)
with patch.object(scanner, 'get_total_to_scan', return_value=1):
with patch.object(scanner, 'get_total_to_scan', return_value=1):
with patch.object(
scanner,
'_SerieScanner__find_mp4_files',
return_value=iter([
("Attack on Titan (2013)", ["S01E001.mp4"])
])
):
with patch.object(
scanner,
'_SerieScanner__find_mp4_files',
return_value=iter([
("Attack on Titan (2013)", ["S01E001.mp4"])
])
'_SerieScanner__read_data_from_file',
return_value=sample_serie
):
with patch.object(
scanner,
'_SerieScanner__read_data_from_file',
return_value=sample_serie
'_SerieScanner__get_missing_episodes_and_season',
return_value=({1: [2, 3]}, "aniworld.to")
):
with patch.object(
scanner,
'_SerieScanner__get_missing_episodes_and_season',
return_value=({1: [2, 3]}, "aniworld.to")
):
with patch.object(
sample_serie, 'save_to_file'
) as mock_save:
scanner.scan()
# Verify file was saved
mock_save.assert_called_once()
sample_serie, 'save_to_file'
) as mock_save:
scanner.scan()
# Verify file was saved
mock_save.assert_called_once()
def test_keydict_populated_after_scan(
self, temp_directory, mock_loader, sample_serie
@@ -444,28 +112,25 @@ class TestSerieScannerBackwardCompatibility:
"""Test keyDict is populated after scan."""
scanner = SerieScanner(temp_directory, mock_loader)
with warnings.catch_warnings():
warnings.simplefilter("ignore", DeprecationWarning)
with patch.object(scanner, 'get_total_to_scan', return_value=1):
with patch.object(scanner, 'get_total_to_scan', return_value=1):
with patch.object(
scanner,
'_SerieScanner__find_mp4_files',
return_value=iter([
("Attack on Titan (2013)", ["S01E001.mp4"])
])
):
with patch.object(
scanner,
'_SerieScanner__find_mp4_files',
return_value=iter([
("Attack on Titan (2013)", ["S01E001.mp4"])
])
'_SerieScanner__read_data_from_file',
return_value=sample_serie
):
with patch.object(
scanner,
'_SerieScanner__read_data_from_file',
return_value=sample_serie
'_SerieScanner__get_missing_episodes_and_season',
return_value=({1: [2, 3]}, "aniworld.to")
):
with patch.object(
scanner,
'_SerieScanner__get_missing_episodes_and_season',
return_value=({1: [2, 3]}, "aniworld.to")
):
with patch.object(sample_serie, 'save_to_file'):
scanner.scan()
assert sample_serie.key in scanner.keyDict
with patch.object(sample_serie, 'save_to_file'):
scanner.scan()
assert sample_serie.key in scanner.keyDict

View File

@@ -251,9 +251,10 @@ class TestSeriesAppReScan:
app.serie_scanner.get_total_to_scan = Mock(return_value=5)
app.serie_scanner.reinit = Mock()
app.serie_scanner.scan = Mock()
app.serie_scanner.keyDict = {}
# Perform rescan with file-based mode (use_database=False)
await app.rescan(use_database=False)
# Perform rescan
await app.rescan()
# Verify rescan completed
app.serie_scanner.reinit.assert_called_once()
@@ -266,7 +267,7 @@ class TestSeriesAppReScan:
async def test_rescan_with_callback(
self, mock_serie_list, mock_scanner, mock_loaders
):
"""Test rescan with progress callbacks (file-based mode)."""
"""Test rescan with progress callbacks."""
test_dir = "/test/anime"
app = SeriesApp(test_dir)
@@ -276,6 +277,7 @@ class TestSeriesAppReScan:
# Mock scanner
app.serie_scanner.get_total_to_scan = Mock(return_value=3)
app.serie_scanner.reinit = Mock()
app.serie_scanner.keyDict = {}
def mock_scan(callback):
callback("folder1", 1)
@@ -284,8 +286,8 @@ class TestSeriesAppReScan:
app.serie_scanner.scan = Mock(side_effect=mock_scan)
# Perform rescan with file-based mode (use_database=False)
await app.rescan(use_database=False)
# Perform rescan
await app.rescan()
# Verify rescan completed
app.serie_scanner.scan.assert_called_once()
@@ -297,7 +299,7 @@ class TestSeriesAppReScan:
async def test_rescan_cancellation(
self, mock_serie_list, mock_scanner, mock_loaders
):
"""Test rescan cancellation (file-based mode)."""
"""Test rescan cancellation."""
test_dir = "/test/anime"
app = SeriesApp(test_dir)
@@ -313,9 +315,9 @@ class TestSeriesAppReScan:
app.serie_scanner.scan = Mock(side_effect=mock_scan)
# Perform rescan - should handle cancellation (file-based mode)
# Perform rescan - should handle cancellation
try:
await app.rescan(use_database=False)
await app.rescan()
except Exception:
pass # Cancellation is expected
@@ -386,178 +388,72 @@ class TestSeriesAppGetters:
class TestSeriesAppDatabaseInit:
"""Test SeriesApp database initialization."""
"""Test SeriesApp initialization (no database support in core)."""
@patch('src.core.SeriesApp.Loaders')
@patch('src.core.SeriesApp.SerieScanner')
@patch('src.core.SeriesApp.SerieList')
def test_init_without_db_session(
def test_init_creates_components(
self, mock_serie_list, mock_scanner, mock_loaders
):
"""Test SeriesApp initializes without database session."""
"""Test SeriesApp initializes all components."""
test_dir = "/test/anime"
# Create app without db_session
# Create app
app = SeriesApp(test_dir)
# Verify db_session is None
assert app._db_session is None
assert app.db_session is None
# Verify SerieList was called with db_session=None
# Verify SerieList was called
mock_serie_list.assert_called_once()
call_kwargs = mock_serie_list.call_args[1]
assert call_kwargs.get("db_session") is None
# Verify SerieScanner was called with db_session=None
call_kwargs = mock_scanner.call_args[1]
assert call_kwargs.get("db_session") is None
# Verify SerieScanner was called
mock_scanner.assert_called_once()
class TestSeriesAppLoadSeriesFromList:
"""Test SeriesApp load_series_from_list method."""
@patch('src.core.SeriesApp.Loaders')
@patch('src.core.SeriesApp.SerieScanner')
@patch('src.core.SeriesApp.SerieList')
def test_init_with_db_session(
def test_load_series_from_list_populates_keydict(
self, mock_serie_list, mock_scanner, mock_loaders
):
"""Test SeriesApp initializes with database session."""
test_dir = "/test/anime"
mock_db = Mock()
# Create app with db_session
app = SeriesApp(test_dir, db_session=mock_db)
# Verify db_session is set
assert app._db_session is mock_db
assert app.db_session is mock_db
# Verify SerieList was called with db_session
call_kwargs = mock_serie_list.call_args[1]
assert call_kwargs.get("db_session") is mock_db
# Verify SerieScanner was called with db_session
call_kwargs = mock_scanner.call_args[1]
assert call_kwargs.get("db_session") is mock_db
class TestSeriesAppDatabaseSession:
"""Test SeriesApp database session management."""
@patch('src.core.SeriesApp.Loaders')
@patch('src.core.SeriesApp.SerieScanner')
@patch('src.core.SeriesApp.SerieList')
def test_set_db_session_updates_all_components(
self, mock_serie_list, mock_scanner, mock_loaders
):
"""Test set_db_session updates app, list, and scanner."""
test_dir = "/test/anime"
mock_list = Mock()
mock_list.GetMissingEpisode.return_value = []
mock_scan = Mock()
mock_serie_list.return_value = mock_list
mock_scanner.return_value = mock_scan
# Create app without db_session
app = SeriesApp(test_dir)
assert app.db_session is None
# Create mock database session
mock_db = Mock()
# Set database session
app.set_db_session(mock_db)
# Verify all components are updated
assert app._db_session is mock_db
assert app.db_session is mock_db
assert mock_list._db_session is mock_db
assert mock_scan._db_session is mock_db
@patch('src.core.SeriesApp.Loaders')
@patch('src.core.SeriesApp.SerieScanner')
@patch('src.core.SeriesApp.SerieList')
def test_set_db_session_to_none(
self, mock_serie_list, mock_scanner, mock_loaders
):
"""Test setting db_session to None."""
test_dir = "/test/anime"
mock_list = Mock()
mock_list.GetMissingEpisode.return_value = []
mock_scan = Mock()
mock_serie_list.return_value = mock_list
mock_scanner.return_value = mock_scan
mock_db = Mock()
# Create app with db_session
app = SeriesApp(test_dir, db_session=mock_db)
# Set database session to None
app.set_db_session(None)
# Verify all components are updated
assert app._db_session is None
assert app.db_session is None
assert mock_list._db_session is None
assert mock_scan._db_session is None
class TestSeriesAppAsyncDbInit:
"""Test SeriesApp async database initialization."""
@pytest.mark.asyncio
@patch('src.core.SeriesApp.Loaders')
@patch('src.core.SeriesApp.SerieScanner')
@patch('src.core.SeriesApp.SerieList')
async def test_init_from_db_async_loads_from_database(
self, mock_serie_list, mock_scanner, mock_loaders
):
"""Test init_from_db_async loads series from database."""
import warnings
test_dir = "/test/anime"
mock_list = Mock()
mock_list.load_series_from_db = AsyncMock()
mock_list.GetMissingEpisode.return_value = [{"name": "Test"}]
mock_serie_list.return_value = mock_list
mock_db = Mock()
# Create app with db_session
app = SeriesApp(test_dir, db_session=mock_db)
# Initialize from database
await app.init_from_db_async()
# Verify load_series_from_db was called
mock_list.load_series_from_db.assert_called_once_with(mock_db)
# Verify series_list is populated
assert len(app.series_list) == 1
@pytest.mark.asyncio
@patch('src.core.SeriesApp.Loaders')
@patch('src.core.SeriesApp.SerieScanner')
@patch('src.core.SeriesApp.SerieList')
async def test_init_from_db_async_without_session_warns(
self, mock_serie_list, mock_scanner, mock_loaders
):
"""Test init_from_db_async warns without db_session."""
import warnings
"""Test load_series_from_list populates the list correctly."""
from src.core.entities.series import Serie
test_dir = "/test/anime"
mock_list = Mock()
mock_list.GetMissingEpisode.return_value = []
mock_list.keyDict = {}
mock_serie_list.return_value = mock_list
# Create app without db_session
# Create app
app = SeriesApp(test_dir)
# Initialize from database should warn
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always")
await app.init_from_db_async()
# Check warning was raised
assert len(w) == 1
assert "without db_session" in str(w[0].message)
# 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]}
),
]
# 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
class TestSeriesAppGetAllSeriesFromDataFiles: