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

@@ -13,10 +13,22 @@ from unittest.mock import AsyncMock, MagicMock, Mock, patch
import pytest
from src.core.entities.series import Serie
from src.server.database.models import AnimeSeries, Episode
def make_anime(key, name, site, folder, episodeDict, year=None):
"""Create a mock AnimeSeries with episodeDict cache set."""
mock = MagicMock(spec=AnimeSeries)
mock.key = key
mock.name = name
mock.site = site
mock.folder = folder
mock.year = year
mock.episodeDict = episodeDict
mock._episode_dict_cache = episodeDict
return mock
@pytest.fixture
def mock_series_app():
"""Create a mock SeriesApp with scanner."""
@@ -73,8 +85,8 @@ class TestAddSeriesWithEpisodes:
# Mock scan_single_series to update keyDict
def mock_scan(key, folder):
# Create Serie with episodes
serie = Serie(
# Create anime with episodes
anime = make_anime(
key=key,
name="Test Anime",
site="aniworld.to",
@@ -83,7 +95,7 @@ class TestAddSeriesWithEpisodes:
year=2024
)
# Update scanner's keyDict
mock_series_app.serie_scanner.keyDict[key] = serie
mock_series_app.serie_scanner.keyDict[key] = anime
return {1: [1, 2, 3]}
mock_series_app.serie_scanner.scan_single_series = mock_scan
@@ -106,8 +118,8 @@ class TestAddSeriesWithEpisodes:
# Arrange
key = "test-anime"
# Create Serie in scanner's keyDict with episodes
serie = Serie(
# Create anime in scanner's keyDict with episodes
anime = make_anime(
key=key,
name="Test Anime",
site="aniworld.to",
@@ -115,7 +127,7 @@ class TestAddSeriesWithEpisodes:
episodeDict={1: [1, 2, 3], 2: [1, 2]},
year=2024
)
mock_series_app.serie_scanner.keyDict[key] = serie
mock_series_app.serie_scanner.keyDict[key] = anime
# Mock the database save method
with patch.object(
@@ -153,7 +165,7 @@ class TestAddSeriesWithEpisodes:
):
"""Test that _save_scan_results_to_db creates episodes."""
# Arrange
serie = Serie(
anime = make_anime(
key="test-anime",
name="Test Anime",
site="aniworld.to",
@@ -193,7 +205,7 @@ class TestAddSeriesWithEpisodes:
mock_episode_service.create = AsyncMock(side_effect=track_episode_create)
# Act
result = await mock_anime_service._save_scan_results_to_db([serie])
result = await mock_anime_service._save_scan_results_to_db([anime])
# Assert
assert result == 1 # One series saved
@@ -217,7 +229,7 @@ class TestAddSeriesWithEpisodes:
):
"""Test that _update_series_in_db adds new missing episodes."""
# Arrange
serie = Serie(
anime = make_anime(
key="test-anime",
name="Test Anime",
site="aniworld.to",
@@ -269,7 +281,7 @@ class TestAddSeriesWithEpisodes:
mock_episode_service.delete = AsyncMock()
# Act
result = await mock_anime_service._save_scan_results_to_db([serie])
result = await mock_anime_service._save_scan_results_to_db([anime])
# Assert
assert result == 1
@@ -292,7 +304,7 @@ class TestAddSeriesWithEpisodes:
# Setup mock scanner to populate keyDict
def mock_scan(key, folder):
serie = Serie(
anime = make_anime(
key=key,
name="Test Anime",
site="aniworld.to",
@@ -300,7 +312,7 @@ class TestAddSeriesWithEpisodes:
episodeDict={1: [1, 2, 3]},
year=2024
)
mock_series_app.serie_scanner.keyDict[key] = serie
mock_series_app.serie_scanner.keyDict[key] = anime
return {1: [1, 2, 3]}
mock_series_app.serie_scanner.scan_single_series = mock_scan
@@ -368,8 +380,8 @@ class TestAddSeriesWithEpisodes:
# Arrange
key = "test-anime"
# Create Serie in list.keyDict with episodes
serie = Serie(
# Create anime in list.keyDict with episodes
anime = make_anime(
key=key,
name="Test Anime",
site="aniworld.to",
@@ -377,7 +389,7 @@ class TestAddSeriesWithEpisodes:
episodeDict={1: [1, 2, 3]},
year=2024
)
mock_series_app.list.keyDict[key] = serie
mock_series_app.list.keyDict[key] = anime
# Mock database AnimeSeries with NFO data
mock_db_series = AnimeSeries(

View File

@@ -7,12 +7,26 @@ 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.SeriesApp import SeriesApp
from src.server.database.models import AnimeSeries, Episode
from src.server.services.anime_service import AnimeService
def make_anime(key, name, site, folder, episodeDict=None, year=None):
"""Create a mock AnimeSeries with episodeDict cache set."""
if episodeDict is None:
episodeDict = {}
mock = MagicMock(spec=AnimeSeries)
mock.key = key
mock.name = name
mock.site = site
mock.folder = folder
mock.year = year
mock.episodeDict = episodeDict
mock._episode_dict_cache = episodeDict
return mock
class TestAnimeListLoading:
"""Test suite for anime list loading functionality."""
@@ -98,8 +112,8 @@ class TestAnimeListLoading:
called_series = mock_series_app.load_series_from_list.call_args[0][0]
assert len(called_series) == 2
# Verify Serie objects have correct attributes
assert all(isinstance(s, Serie) for s in called_series)
# Verify AnimeSeries objects have correct attributes
assert all(isinstance(s, AnimeSeries) for s in called_series)
assert called_series[0].key == "test-anime-1"
assert called_series[0].name == "Test Anime 1"
assert called_series[0].folder == "Test Anime 1 (2023)"
@@ -140,14 +154,14 @@ class TestAnimeListLoading:
# Create test series
test_series = [
Serie(
make_anime(
key="test-1",
name="Test Series 1",
site="aniworld.to",
folder="Test Series 1 (2023)",
episodeDict={1: [1, 2, 3]}
),
Serie(
make_anime(
key="test-2",
name="Test Series 2",
site="aniworld.to",
@@ -295,7 +309,7 @@ class TestAnimeListLoading:
"With skip_load=True, list should be empty initially"
# Test that manual loading works
test_serie = Serie(
test_serie = make_anime(
key="test",
name="Test",
site="aniworld.to",

View File

@@ -7,13 +7,13 @@ from unittest.mock import MagicMock, Mock, patch
import pytest
import requests
from src.core.providers.aniworld_provider import AniworldLoader
from src.server.providers.aniworld_provider import AniworldLoader
@pytest.fixture
def loader():
"""Create AniworldLoader with mocked session to prevent real HTTP calls."""
with patch("src.core.providers.aniworld_provider.UserAgent") as mock_ua:
with patch("src.server.providers.aniworld_provider.UserAgent") as mock_ua:
mock_ua.return_value.random = "MockUserAgent/1.0"
instance = AniworldLoader()
instance.session = MagicMock()
@@ -390,7 +390,7 @@ class TestAniworldProviderParsing:
class TestAniworldSeasonEpisodeCount:
"""Test season and episode count retrieval."""
@patch("src.core.providers.aniworld_provider.requests.get")
@patch("src.server.providers.aniworld_provider.requests.get")
def test_get_season_episode_count(self, mock_get, loader):
"""get_season_episode_count should return correct counts."""
# Main page with 2 seasons
@@ -421,7 +421,7 @@ class TestAniworldSeasonEpisodeCount:
result = loader.get_season_episode_count("naruto")
assert result == {1: 3, 2: 2}
@patch("src.core.providers.aniworld_provider.requests.get")
@patch("src.server.providers.aniworld_provider.requests.get")
def test_get_season_episode_count_no_seasons(self, mock_get, loader):
"""get_season_episode_count should return empty dict when no seasons."""
html = "<html><body></body></html>"
@@ -616,7 +616,7 @@ class TestAniworldDownloadFailover:
return ydl
with patch(
"src.core.providers.aniworld_provider.YoutubeDL",
"src.server.providers.aniworld_provider.YoutubeDL",
side_effect=fake_ytdl,
):
result = patched_loader.download(
@@ -649,7 +649,7 @@ class TestAniworldDownloadFailover:
return ydl
with patch(
"src.core.providers.aniworld_provider.YoutubeDL",
"src.server.providers.aniworld_provider.YoutubeDL",
side_effect=fake_ytdl,
):
result = patched_loader.download(
@@ -670,7 +670,7 @@ class TestAniworldDownloadFailover:
patched_loader._try_direct_stream.side_effect = write_direct
with patch(
"src.core.providers.aniworld_provider.YoutubeDL"
"src.server.providers.aniworld_provider.YoutubeDL"
) as mock_ydl:
result = patched_loader.download(
str(tmp_path), "Anime", 1, 1, "k", "German Dub"
@@ -682,7 +682,7 @@ class TestAniworldDownloadFailover:
self, patched_loader, tmp_path, caplog
):
with patch(
"src.core.providers.aniworld_provider.YoutubeDL",
"src.server.providers.aniworld_provider.YoutubeDL",
side_effect=Exception("HTTP 404"),
):
result = patched_loader.download(
@@ -728,7 +728,7 @@ class TestDecodeHtmlContent:
def test_decodes_utf8_content(self):
"""Should correctly decode UTF-8 content."""
from src.core.providers.aniworld_provider import _decode_html_content
from src.server.providers.aniworld_provider import _decode_html_content
html = '<html><body><h1>Titel mit Ümläüten</h1></body></html>'
content = html.encode('utf-8')
result = _decode_html_content(content)
@@ -736,7 +736,7 @@ class TestDecodeHtmlContent:
def test_decodes_latin1_content(self):
"""Should correctly decode Latin-1 content when chardet detects it."""
from src.core.providers.aniworld_provider import _decode_html_content
from src.server.providers.aniworld_provider import _decode_html_content
# Longer content for more reliable chardet detection
html = '<html><body><h1>CafÉ and more text here</h1></body></html>'
content = html.encode('latin-1')
@@ -745,13 +745,13 @@ class TestDecodeHtmlContent:
def test_replaces_invalid_bytes(self):
"""Should replace invalid bytes with replacement character."""
from src.core.providers.aniworld_provider import _decode_html_content
from src.server.providers.aniworld_provider import _decode_html_content
content = b'\xff\xfe Invalid \x80\x81'
result = _decode_html_content(content)
assert isinstance(result, str)
def test_handles_empty_content(self):
"""Should handle empty content gracefully."""
from src.core.providers.aniworld_provider import _decode_html_content
from src.server.providers.aniworld_provider import _decode_html_content
result = _decode_html_content(b'')
assert result == ''

View File

@@ -6,7 +6,7 @@ from unittest.mock import MagicMock
import pytest
from src.core.providers.base_provider import Loader
from src.server.providers.base_provider import Loader
class TestLoaderAbstractInterface:

View File

@@ -7,7 +7,7 @@ functionality.
import unittest
from src.core.interfaces.callbacks import (
from src.server.interfaces.callbacks import (
CallbackManager,
CompletionCallback,
CompletionContext,

View File

@@ -535,7 +535,7 @@ class TestAnimeServiceScanLock:
@pytest.mark.asyncio
async def test_anime_service_ignores_concurrent_rescan_requests(self):
"""Test that AnimeService ignores concurrent rescan requests."""
from src.core.SeriesApp import SeriesApp
from src.server.SeriesApp import SeriesApp
from src.server.services.anime_service import AnimeService
# Mock database

View File

@@ -9,7 +9,7 @@ from unittest.mock import MagicMock, patch
import pytest
from src.core.error_handler import (
from src.server.error_handler import (
DownloadError,
FileCorruptionDetector,
NetworkError,
@@ -259,7 +259,7 @@ class TestWithErrorRecoveryDecorator:
raise RuntimeError("oops")
return "ok"
with patch("src.core.error_handler.logger") as mock_logger:
with patch("src.server.error_handler.logger") as mock_logger:
fail_once()
# Should have logged a warning with context
mock_logger.warning.assert_called()

View File

@@ -430,7 +430,7 @@ class TestExponentialBackoff:
import aiohttp
from src.core.utils.image_downloader import ImageDownloader, ImageDownloadError
from src.server.utils.image_downloader import ImageDownloader, ImageDownloadError
downloader = ImageDownloader(max_retries=3, retry_delay=0.1)

View File

@@ -699,61 +699,58 @@ class TestErrorHandling:
class TestRemoveEpisodeFromMissingList:
"""Test that completed downloads remove episodes from missing list."""
@staticmethod
def make_anime(key, name, folder, episode_dict):
"""Create mock AnimeSeries for testing."""
anime = MagicMock()
anime.key = key
anime.name = name
anime.site = "https://example.com"
anime.folder = folder
anime.episodeDict = episode_dict
return anime
@pytest.mark.asyncio
async def test_remove_episode_from_memory(self, download_service):
"""Test _remove_episode_from_memory updates in-memory state."""
from src.core.entities.series import Serie
# Set up in-memory series with missing episodes
serie = Serie(
key="test-series",
name="Test Series",
site="https://example.com",
folder="Test Series (2024)",
episodeDict={1: [1, 2, 3], 2: [1, 2]},
)
anime = self.make_anime("test-series", "Test Series", "Test Series (2024)", {1: [1, 2, 3], 2: [1, 2]})
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]
download_service._anime_service._app = mock_app
# Remove episode S01E02
download_service._remove_episode_from_memory("test-series", 1, 2)
# Episode should be removed from episodeDict
assert 2 not in serie.episodeDict[1]
assert serie.episodeDict[1] == [1, 3]
assert 2 not in anime.episodeDict[1]
assert anime.episodeDict[1] == [1, 3]
# Season 2 should be untouched
assert serie.episodeDict[2] == [1, 2]
assert anime.episodeDict[2] == [1, 2]
@pytest.mark.asyncio
async def test_remove_last_episode_in_season_removes_season(
self, download_service
):
"""Test removing the last episode in a season removes the season key."""
from src.core.entities.series import Serie
serie = Serie(
key="test-series",
name="Test Series",
site="https://example.com",
folder="Test Series (2024)",
episodeDict={1: [5], 2: [1, 2]},
)
anime = self.make_anime("test-series", "Test Series", "Test Series (2024)", {1: [5], 2: [1, 2]})
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]
download_service._anime_service._app = mock_app
# Remove the only episode in season 1
download_service._remove_episode_from_memory("test-series", 1, 5)
# Season 1 should be completely removed
assert 1 not in serie.episodeDict
assert 1 not in anime.episodeDict
# Season 2 untouched
assert serie.episodeDict[2] == [1, 2]
assert anime.episodeDict[2] == [1, 2]
# GetMissingEpisode should have been called to refresh
mock_app.list.GetMissingEpisode.assert_called()
@@ -778,20 +775,12 @@ class TestRemoveEpisodeFromMissingList:
"""Test _remove_episode_from_missing_list updates both DB and memory."""
from unittest.mock import patch
from src.core.entities.series import Serie
# Set up in-memory state
serie = Serie(
key="test-series",
name="Test Series",
site="https://example.com",
folder="Test Series (2024)",
episodeDict={1: [1, 2, 3]},
)
anime = self.make_anime("test-series", "Test Series", "Test Series (2024)", {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]
download_service._anime_service._app = mock_app
download_service._anime_service._cached_list_missing = MagicMock()
@@ -845,8 +834,8 @@ class TestRemoveEpisodeFromMissingList:
),
)
# In-memory update happened
assert 2 not in serie.episodeDict[1]
assert serie.episodeDict[1] == [1, 3]
assert 2 not in anime.episodeDict[1]
assert anime.episodeDict[1] == [1, 3]
# Cache was cleared
download_service._anime_service._cached_list_missing.cache_clear.assert_called()
# Broadcast was sent so frontend gets real-time update
@@ -862,25 +851,17 @@ class TestRemoveEpisodeFromMissingList:
"""Test full flow: download success removes episode from missing list."""
from unittest.mock import patch
from src.core.entities.series import Serie
# Setup mock anime service to return success
download_service._anime_service.download = AsyncMock(
return_value=True
)
# Set up in-memory series state
serie = Serie(
key="series-1",
name="Test Series",
site="https://example.com",
folder="series",
episodeDict={1: [1, 2, 3]},
)
anime = self.make_anime("series-1", "Test Series", "series", {1: [1, 2, 3]})
mock_app = MagicMock()
mock_app.list.keyDict = {"series-1": serie}
mock_app.list.GetMissingEpisode.return_value = [serie]
mock_app.series_list = [serie]
mock_app.list.keyDict = {"series-1": anime}
mock_app.list.GetMissingEpisode.return_value = [anime]
mock_app.series_list = [anime]
download_service._anime_service._app = mock_app
download_service._anime_service._cached_list_missing = MagicMock()
@@ -936,8 +917,8 @@ class TestRemoveEpisodeFromMissingList:
assert download_service._completed_items[0].status == DownloadStatus.COMPLETED
# Episode 2 should be removed from in-memory missing list
assert 2 not in serie.episodeDict[1]
assert serie.episodeDict[1] == [1, 3]
assert 2 not in anime.episodeDict[1]
assert anime.episodeDict[1] == [1, 3]
class TestQueueDeduplication:

View File

@@ -7,16 +7,16 @@ from unittest.mock import MagicMock, Mock, PropertyMock, patch
import pytest
from src.core.error_handler import (
from src.server.error_handler import (
DownloadError,
NetworkError,
NonRetryableError,
RetryableError,
)
from src.core.providers.base_provider import Loader
from src.server.providers.base_provider import Loader
# Import the class but we need a concrete subclass to test it
from src.core.providers.enhanced_provider import EnhancedAniWorldLoader
from src.server.providers.enhanced_provider import EnhancedAniWorldLoader
class ConcreteEnhancedLoader(EnhancedAniWorldLoader):
@@ -50,9 +50,9 @@ class ConcreteEnhancedLoader(EnhancedAniWorldLoader):
def enhanced_loader():
"""Create ConcreteEnhancedLoader with mocked externals."""
with patch(
"src.core.providers.enhanced_provider.UserAgent"
"src.server.providers.enhanced_provider.UserAgent"
) as mock_ua, patch(
"src.core.providers.enhanced_provider.get_integrity_manager"
"src.server.providers.enhanced_provider.get_integrity_manager"
):
mock_ua.return_value.random = "MockAgent/1.0"
loader = ConcreteEnhancedLoader()
@@ -360,7 +360,7 @@ class TestDownloadStatistics:
class TestEnhancedDownloadValidation:
"""Test download input validation."""
@patch("src.core.providers.enhanced_provider.get_integrity_manager")
@patch("src.server.providers.enhanced_provider.get_integrity_manager")
def test_download_missing_base_directory_raises(
self, mock_integrity, enhanced_loader
):
@@ -368,7 +368,7 @@ class TestEnhancedDownloadValidation:
with pytest.raises((ValueError, DownloadError)):
enhanced_loader.Download("", "folder", 1, 1, "key")
@patch("src.core.providers.enhanced_provider.get_integrity_manager")
@patch("src.server.providers.enhanced_provider.get_integrity_manager")
def test_download_missing_serie_folder_raises(
self, mock_integrity, enhanced_loader
):
@@ -376,7 +376,7 @@ class TestEnhancedDownloadValidation:
with pytest.raises((ValueError, DownloadError)):
enhanced_loader.Download("/base", "", 1, 1, "key")
@patch("src.core.providers.enhanced_provider.get_integrity_manager")
@patch("src.server.providers.enhanced_provider.get_integrity_manager")
def test_download_negative_season_raises(
self, mock_integrity, enhanced_loader
):
@@ -384,7 +384,7 @@ class TestEnhancedDownloadValidation:
with pytest.raises((ValueError, DownloadError)):
enhanced_loader.Download("/base", "folder", -1, 1, "key")
@patch("src.core.providers.enhanced_provider.get_integrity_manager")
@patch("src.server.providers.enhanced_provider.get_integrity_manager")
def test_download_negative_episode_raises(
self, mock_integrity, enhanced_loader
):
@@ -392,7 +392,7 @@ class TestEnhancedDownloadValidation:
with pytest.raises((ValueError, DownloadError)):
enhanced_loader.Download("/base", "folder", 1, -1, "key")
@patch("src.core.providers.enhanced_provider.get_integrity_manager")
@patch("src.server.providers.enhanced_provider.get_integrity_manager")
def test_download_increments_total_count(
self, mock_integrity, enhanced_loader
):
@@ -459,7 +459,7 @@ class TestFetchAnimeListWithRecovery:
mock_response.text = json.dumps([{"title": "Naruto"}])
with patch(
"src.core.providers.enhanced_provider.recovery_strategies"
"src.server.providers.enhanced_provider.recovery_strategies"
) as mock_rs:
mock_rs.handle_network_failure.return_value = mock_response
result = enhanced_loader._fetch_anime_list_with_recovery(
@@ -476,7 +476,7 @@ class TestFetchAnimeListWithRecovery:
mock_response.status_code = 404
with patch(
"src.core.providers.enhanced_provider.recovery_strategies"
"src.server.providers.enhanced_provider.recovery_strategies"
) as mock_rs:
mock_rs.handle_network_failure.return_value = mock_response
with pytest.raises(NonRetryableError, match="not found"):
@@ -491,7 +491,7 @@ class TestFetchAnimeListWithRecovery:
mock_response.status_code = 403
with patch(
"src.core.providers.enhanced_provider.recovery_strategies"
"src.server.providers.enhanced_provider.recovery_strategies"
) as mock_rs:
mock_rs.handle_network_failure.return_value = mock_response
with pytest.raises(NonRetryableError, match="forbidden"):
@@ -506,7 +506,7 @@ class TestFetchAnimeListWithRecovery:
mock_response.status_code = 500
with patch(
"src.core.providers.enhanced_provider.recovery_strategies"
"src.server.providers.enhanced_provider.recovery_strategies"
) as mock_rs:
mock_rs.handle_network_failure.return_value = mock_response
with pytest.raises(RetryableError, match="Server error"):
@@ -519,7 +519,7 @@ class TestFetchAnimeListWithRecovery:
import requests as req
with patch(
"src.core.providers.enhanced_provider.recovery_strategies"
"src.server.providers.enhanced_provider.recovery_strategies"
) as mock_rs:
mock_rs.handle_network_failure.side_effect = (
req.RequestException("timeout")
@@ -548,7 +548,7 @@ class TestGetKeyHTML:
mock_response.ok = True
with patch(
"src.core.providers.enhanced_provider.recovery_strategies"
"src.server.providers.enhanced_provider.recovery_strategies"
) as mock_rs:
mock_rs.handle_network_failure.return_value = mock_response
result = enhanced_loader._GetKeyHTML("new-key")
@@ -563,7 +563,7 @@ class TestGetKeyHTML:
mock_response.status_code = 404
with patch(
"src.core.providers.enhanced_provider.recovery_strategies"
"src.server.providers.enhanced_provider.recovery_strategies"
) as mock_rs:
mock_rs.handle_network_failure.return_value = mock_response
with pytest.raises(NonRetryableError, match="not found"):
@@ -628,7 +628,7 @@ class TestGetEmbeddedLink:
"_get_redirect_link",
return_value=("https://aniworld.to/redirect/100", "VOE"),
), patch(
"src.core.providers.enhanced_provider.recovery_strategies"
"src.server.providers.enhanced_provider.recovery_strategies"
) as mock_rs:
mock_rs.handle_network_failure.return_value = mock_response
result = enhanced_loader._get_embeded_link(
@@ -718,11 +718,11 @@ class TestDownloadWithRecovery:
return True
with patch(
"src.core.providers.enhanced_provider.recovery_strategies"
"src.server.providers.enhanced_provider.recovery_strategies"
) as mock_rs, patch(
"src.core.providers.enhanced_provider.file_corruption_detector"
"src.server.providers.enhanced_provider.file_corruption_detector"
) as mock_fcd, patch(
"src.core.providers.enhanced_provider.get_integrity_manager"
"src.server.providers.enhanced_provider.get_integrity_manager"
) as mock_im:
mock_rs.handle_network_failure.return_value = (
"https://direct.example.com/v.mp4",
@@ -746,7 +746,7 @@ class TestDownloadWithRecovery:
output_path = str(tmp_path / "output.mp4")
with patch(
"src.core.providers.enhanced_provider.recovery_strategies"
"src.server.providers.enhanced_provider.recovery_strategies"
) as mock_rs:
mock_rs.handle_network_failure.side_effect = Exception("fail")
@@ -769,9 +769,9 @@ class TestDownloadWithRecovery:
return True
with patch(
"src.core.providers.enhanced_provider.recovery_strategies"
"src.server.providers.enhanced_provider.recovery_strategies"
) as mock_rs, patch(
"src.core.providers.enhanced_provider.file_corruption_detector"
"src.server.providers.enhanced_provider.file_corruption_detector"
) as mock_fcd:
mock_rs.handle_network_failure.return_value = (
"https://direct.example.com/v.mp4",
@@ -816,7 +816,7 @@ class TestGetSeasonEpisodeCount:
]
with patch(
"src.core.providers.enhanced_provider.recovery_strategies"
"src.server.providers.enhanced_provider.recovery_strategies"
) as mock_rs:
mock_rs.handle_network_failure.side_effect = responses
result = enhanced_loader.get_season_episode_count("test")
@@ -828,7 +828,7 @@ class TestGetSeasonEpisodeCount:
base_html = b"<html><body>No seasons</body></html>"
with patch(
"src.core.providers.enhanced_provider.recovery_strategies"
"src.server.providers.enhanced_provider.recovery_strategies"
) as mock_rs:
mock_rs.handle_network_failure.return_value = MagicMock(
content=base_html
@@ -844,7 +844,7 @@ class TestPerformYtdlDownload:
def test_success(self, enhanced_loader):
"""Should return True on successful download."""
with patch(
"src.core.providers.enhanced_provider.YoutubeDL"
"src.server.providers.enhanced_provider.YoutubeDL"
) as MockYDL:
mock_ydl = MagicMock()
MockYDL.return_value.__enter__ = MagicMock(return_value=mock_ydl)
@@ -858,7 +858,7 @@ class TestPerformYtdlDownload:
def test_failure_raises_download_error(self, enhanced_loader):
"""yt-dlp failure should raise DownloadError."""
with patch(
"src.core.providers.enhanced_provider.YoutubeDL"
"src.server.providers.enhanced_provider.YoutubeDL"
) as MockYDL:
mock_ydl = MagicMock()
mock_ydl.download.side_effect = Exception("yt-dlp crash")
@@ -873,7 +873,7 @@ class TestPerformYtdlDownload:
class TestDownloadFlow:
"""Test full Download method flow."""
@patch("src.core.providers.enhanced_provider.get_integrity_manager")
@patch("src.server.providers.enhanced_provider.get_integrity_manager")
def test_existing_valid_file_returns_true(
self, mock_integrity, enhanced_loader, tmp_path
):
@@ -889,7 +889,7 @@ class TestDownloadFlow:
)
with patch(
"src.core.providers.enhanced_provider.file_corruption_detector"
"src.server.providers.enhanced_provider.file_corruption_detector"
) as mock_fcd:
mock_fcd.is_valid_video_file.return_value = True
mock_integrity.return_value.has_checksum.return_value = False
@@ -901,7 +901,7 @@ class TestDownloadFlow:
assert result is True
assert enhanced_loader.download_stats["successful_downloads"] == 1
@patch("src.core.providers.enhanced_provider.get_integrity_manager")
@patch("src.server.providers.enhanced_provider.get_integrity_manager")
def test_missing_key_raises_value_error(
self, mock_integrity, enhanced_loader, tmp_path
):
@@ -915,7 +915,7 @@ class TestAniworldLoaderCompat:
def test_inherits_from_enhanced(self):
"""AniworldLoader should extend EnhancedAniWorldLoader."""
from src.core.providers.enhanced_provider import AniworldLoader
from src.server.providers.enhanced_provider import AniworldLoader
assert issubclass(AniworldLoader, EnhancedAniWorldLoader)
@@ -936,11 +936,11 @@ class TestFfmpegHlsOptions:
return True
with patch(
"src.core.providers.enhanced_provider.recovery_strategies"
"src.server.providers.enhanced_provider.recovery_strategies"
) as mock_rs, patch(
"src.core.providers.enhanced_provider.file_corruption_detector"
"src.server.providers.enhanced_provider.file_corruption_detector"
) as mock_fcd, patch(
"src.core.providers.enhanced_provider.get_integrity_manager"
"src.server.providers.enhanced_provider.get_integrity_manager"
) as mock_im:
mock_rs.handle_network_failure.return_value = (
"https://direct.example.com/v.mp4",
@@ -969,7 +969,7 @@ class TestHlsUrlDetection:
def test_voe_hls_pattern_extracts_hls_url(self):
"""HLS_PATTERN should extract HLS URL from VOE embedded player HTML."""
import re
from src.core.providers.streaming.voe import HLS_PATTERN
from src.server.providers.streaming.voe import HLS_PATTERN
html_with_hls = """
var playerConfig = {
@@ -984,7 +984,7 @@ class TestHlsUrlDetection:
def test_voe_hls_pattern_returns_none_when_no_hls(self):
"""HLS_PATTERN should return None when no HLS URL in HTML."""
import re
from src.core.providers.streaming.voe import HLS_PATTERN
from src.server.providers.streaming.voe import HLS_PATTERN
html_no_hls = """
var playerConfig = {
@@ -997,7 +997,7 @@ class TestHlsUrlDetection:
def test_hls_url_detection_in_provider_flow(self, enhanced_loader, tmp_path):
"""Provider should detect and handle HLS URLs from VOE extractor."""
import re
from src.core.providers.streaming.voe import HLS_PATTERN
from src.server.providers.streaming.voe import HLS_PATTERN
# Simulate VOE returning an HLS URL (base64 encoded .m3u8)
encoded_hls = "aHR0cHM6Ly9leGFtcGxlLmNvbS92aWRlby5tM3U4"

View File

@@ -107,8 +107,8 @@ class TestSerieScannerIgnorePatterns:
def test_scanner_skips_ignored_folders(self, tmp_path):
"""Test scanner skips folders matching ignore patterns."""
from src.core.SerieScanner import SerieScanner
from src.core.providers.aniworld_provider import AniworldLoader
from src.server.SerieScanner import SerieScanner
from src.server.providers.aniworld_provider import AniworldLoader
# Create test folders
ignored_folder = tmp_path / "The Last of Us"
@@ -131,8 +131,8 @@ class TestSerieScannerIgnorePatterns:
def test_scanner_normal_folders_not_ignored(self, tmp_path):
"""Test normal folders are not skipped."""
from src.core.SerieScanner import SerieScanner
from src.core.providers.aniworld_provider import AniworldLoader
from src.server.SerieScanner import SerieScanner
from src.server.providers.aniworld_provider import AniworldLoader
folder1 = tmp_path / "Attack on Titan"
folder1.mkdir()
@@ -153,8 +153,8 @@ class TestSerieScannerIgnorePatterns:
def test_scanner_respects_default_ignore_patterns(self, tmp_path):
"""Test scanner respects default ignore patterns."""
from src.core.SerieScanner import SerieScanner
from src.core.providers.aniworld_provider import AniworldLoader
from src.server.SerieScanner import SerieScanner
from src.server.providers.aniworld_provider import AniworldLoader
# Create folder matching default ignore pattern (Chernobyl)
ignored_folder = tmp_path / "Chernobyl Complete Series"
@@ -175,48 +175,20 @@ class TestSerieScannerIgnorePatterns:
class TestSerieListIgnorePatterns:
"""Test SerieList respects ignore patterns."""
"""Test SerieList ignore pattern filtering - DB mode tests removed.
Note: File-based load_series() has been removed from SerieList.
This test class is kept for reference but the test now verifies
that DB-only SerieList doesn't load anything from disk.
"""
def test_load_series_skips_ignored_folders(self, tmp_path):
"""Test load_series skips folders matching ignore patterns."""
from src.core.entities.SerieList import SerieList
from src.core.entities.series import Serie
# Create ignored folder with data file
ignored_folder = tmp_path / "The Last of Us"
ignored_folder.mkdir()
ignored_data = ignored_folder / "data"
def test_serie_list_db_mode_creates_empty_list(self, tmp_path):
"""Test that DB-only SerieList creates empty keyDict on init."""
from src.server.database.SerieList import SerieList
ignored_serie = Serie(
key="the-last-of-us",
name="The Last of Us",
site="https://aniworld.to/anime/stream/the-last-of-us",
folder="The Last of Us",
episodeDict={1: [1, 2, 3]}
)
with warnings.catch_warnings():
warnings.simplefilter("ignore", DeprecationWarning)
ignored_serie.save_to_file(str(ignored_data))
# Create normal folder with data file
normal_folder = tmp_path / "Attack on Titan"
normal_folder.mkdir()
normal_data = normal_folder / "data"
normal_serie = Serie(
key="attack-on-titan",
name="Attack on Titan",
site="https://aniworld.to/anime/stream/attack-on-titan",
folder="Attack on Titan",
episodeDict={1: [1, 2]}
)
with warnings.catch_warnings():
warnings.simplefilter("ignore", DeprecationWarning)
normal_serie.save_to_file(str(normal_data))
# Load series
# DB-only SerieList doesn't auto-load from disk
serie_list = SerieList(str(tmp_path))
# Verify ignored folder was skipped
assert serie_list.contains("attack-on-titan") is True
assert serie_list.contains("the-last-of-us") is False
# keyDict should be empty (no auto-loading)
assert len(serie_list.keyDict) == 0
assert not serie_list.contains("attack-on-titan")

View File

@@ -8,7 +8,7 @@ import aiohttp
import pytest
from PIL import Image
from src.core.utils.image_downloader import ImageDownloader, ImageDownloadError
from src.server.utils.image_downloader import ImageDownloader, ImageDownloadError
@pytest.fixture

View File

@@ -3,7 +3,7 @@ Unit tests for key generation utilities.
"""
import pytest
from src.core.utils.key_utils import (
from src.server.utils.key_utils import (
generate_key_from_folder,
normalize_key,
is_valid_key,

View File

@@ -5,8 +5,8 @@ from unittest.mock import MagicMock, patch
import pytest
from src.core.providers.base_provider import Loader
from src.core.providers.monitored_provider import (
from src.server.providers.base_provider import Loader
from src.server.providers.monitored_provider import (
MonitoredProviderWrapper,
wrap_provider,
)
@@ -84,7 +84,7 @@ def mock_health_monitor():
def monitored_wrapper(mock_provider, mock_health_monitor):
"""Create a monitored wrapper with mock health monitor."""
with patch(
"src.core.providers.monitored_provider.get_health_monitor",
"src.server.providers.monitored_provider.get_health_monitor",
return_value=mock_health_monitor,
):
wrapper = ConcreteMonitoredWrapper(
@@ -100,7 +100,7 @@ class TestMonitoredProviderWrapperInit:
def test_wrapper_stores_provider(self, mock_provider):
"""Wrapper should store the wrapped provider."""
with patch(
"src.core.providers.monitored_provider.get_health_monitor"
"src.server.providers.monitored_provider.get_health_monitor"
):
wrapper = ConcreteMonitoredWrapper(mock_provider)
assert wrapper._provider is mock_provider
@@ -108,7 +108,7 @@ class TestMonitoredProviderWrapperInit:
def test_wrapper_monitoring_enabled_by_default(self, mock_provider):
"""Monitoring should be enabled by default."""
with patch(
"src.core.providers.monitored_provider.get_health_monitor"
"src.server.providers.monitored_provider.get_health_monitor"
):
wrapper = ConcreteMonitoredWrapper(mock_provider)
assert wrapper._enable_monitoring is True
@@ -320,7 +320,7 @@ class TestWrapProviderFunction:
def test_wrap_creates_monitored_wrapper(self, mock_provider):
"""wrap_provider should return MonitoredProviderWrapper."""
with patch(
"src.core.providers.monitored_provider.get_health_monitor"
"src.server.providers.monitored_provider.get_health_monitor"
):
# wrap_provider returns MonitoredProviderWrapper which can't be
# instantiated directly due to missing abstract methods.

View File

@@ -6,7 +6,7 @@ from pathlib import Path
import pytest
from src.core.providers.config_manager import (
from src.server.providers.config_manager import (
ProviderConfigManager,
ProviderSettings,
get_config_manager,
@@ -407,7 +407,7 @@ class TestGetConfigManagerSingleton:
def test_returns_instance(self):
"""get_config_manager should return a ProviderConfigManager."""
# Reset global state for test
import src.core.providers.config_manager as cm
import src.server.providers.config_manager as cm
cm._config_manager = None
manager = get_config_manager()
@@ -418,7 +418,7 @@ class TestGetConfigManagerSingleton:
def test_returns_same_instance(self):
"""get_config_manager should return same instance on repeated calls."""
import src.core.providers.config_manager as cm
import src.server.providers.config_manager as cm
cm._config_manager = None
first = get_config_manager()

View File

@@ -8,7 +8,7 @@ from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from src.core.providers.aniworld_provider import AniworldLoader
from src.server.providers.aniworld_provider import AniworldLoader
def _mock_response(content: str) -> MagicMock:
@@ -202,7 +202,7 @@ class TestEmptyResponses:
"""No season meta tag returns empty dict or zero."""
loader = _loader()
html_str = "<html><head></head><body></body></html>"
with patch("src.core.providers.aniworld_provider.requests.get", return_value=_mock_response(html_str)):
with patch("src.server.providers.aniworld_provider.requests.get", return_value=_mock_response(html_str)):
result = loader.get_season_episode_count("some-anime")
# Either empty dict or {1: 0} depending on implementation
assert isinstance(result, (dict, int))

View File

@@ -4,21 +4,21 @@ from unittest.mock import MagicMock, patch
import pytest
from src.core.providers.base_provider import Loader
from src.core.providers.provider_factory import Loaders
from src.server.providers.base_provider import Loader
from src.server.providers.provider_factory import Loaders
class TestLoadersInit:
"""Test Loaders factory initialization."""
@patch("src.core.providers.provider_factory.AniworldLoader")
@patch("src.server.providers.provider_factory.AniworldLoader")
def test_factory_initializes_with_default_providers(self, mock_aniworld):
"""Factory should register aniworld.to provider by default."""
mock_aniworld.return_value = MagicMock(spec=Loader)
factory = Loaders()
assert "aniworld.to" in factory.dict
@patch("src.core.providers.provider_factory.AniworldLoader")
@patch("src.server.providers.provider_factory.AniworldLoader")
def test_factory_dict_contains_loader_instances(self, mock_aniworld):
"""Factory dict values should be Loader instances."""
mock_instance = MagicMock(spec=Loader)
@@ -31,7 +31,7 @@ class TestLoadersInit:
class TestLoadersGetLoader:
"""Test GetLoader method."""
@patch("src.core.providers.provider_factory.AniworldLoader")
@patch("src.server.providers.provider_factory.AniworldLoader")
def test_get_loader_returns_registered_provider(self, mock_aniworld):
"""GetLoader should return provider for known key."""
mock_instance = MagicMock(spec=Loader)
@@ -40,7 +40,7 @@ class TestLoadersGetLoader:
loader = factory.GetLoader("aniworld.to")
assert loader is mock_instance
@patch("src.core.providers.provider_factory.AniworldLoader")
@patch("src.server.providers.provider_factory.AniworldLoader")
def test_get_loader_raises_key_error_for_unknown(self, mock_aniworld):
"""GetLoader should raise KeyError for unknown provider key."""
mock_aniworld.return_value = MagicMock(spec=Loader)
@@ -48,7 +48,7 @@ class TestLoadersGetLoader:
with pytest.raises(KeyError):
factory.GetLoader("nonexistent.provider")
@patch("src.core.providers.provider_factory.AniworldLoader")
@patch("src.server.providers.provider_factory.AniworldLoader")
def test_get_loader_returns_same_instance(self, mock_aniworld):
"""GetLoader should return same instance on repeated calls."""
mock_instance = MagicMock(spec=Loader)
@@ -58,7 +58,7 @@ class TestLoadersGetLoader:
second = factory.GetLoader("aniworld.to")
assert first is second
@patch("src.core.providers.provider_factory.AniworldLoader")
@patch("src.server.providers.provider_factory.AniworldLoader")
def test_get_loader_empty_key(self, mock_aniworld):
"""GetLoader should raise KeyError for empty string key."""
mock_aniworld.return_value = MagicMock(spec=Loader)
@@ -70,14 +70,14 @@ class TestLoadersGetLoader:
class TestLoadersProviderRegistry:
"""Test the provider registry within the factory."""
@patch("src.core.providers.provider_factory.AniworldLoader")
@patch("src.server.providers.provider_factory.AniworldLoader")
def test_registry_size(self, mock_aniworld):
"""Factory should have exactly one default provider."""
mock_aniworld.return_value = MagicMock(spec=Loader)
factory = Loaders()
assert len(factory.dict) == 1
@patch("src.core.providers.provider_factory.AniworldLoader")
@patch("src.server.providers.provider_factory.AniworldLoader")
def test_can_add_custom_provider(self, mock_aniworld):
"""Custom providers can be added to the factory registry."""
mock_aniworld.return_value = MagicMock(spec=Loader)
@@ -86,7 +86,7 @@ class TestLoadersProviderRegistry:
factory.dict["custom.provider"] = custom_provider
assert factory.GetLoader("custom.provider") is custom_provider
@patch("src.core.providers.provider_factory.AniworldLoader")
@patch("src.server.providers.provider_factory.AniworldLoader")
def test_can_override_existing_provider(self, mock_aniworld):
"""Existing providers can be overridden in the registry."""
mock_aniworld.return_value = MagicMock(spec=Loader)
@@ -95,7 +95,7 @@ class TestLoadersProviderRegistry:
factory.dict["aniworld.to"] = new_provider
assert factory.GetLoader("aniworld.to") is new_provider
@patch("src.core.providers.provider_factory.AniworldLoader")
@patch("src.server.providers.provider_factory.AniworldLoader")
def test_multiple_factories_are_independent(self, mock_aniworld):
"""Multiple factory instances should have independent registries."""
mock_aniworld.return_value = MagicMock(spec=Loader)

View File

@@ -1,7 +1,7 @@
"""Unit tests for provider failover system."""
import pytest
from src.core.providers.failover import (
from src.server.providers.failover import (
ProviderFailover,
configure_failover,
get_failover,

View File

@@ -4,7 +4,7 @@ from datetime import datetime
import pytest
from src.core.providers.health_monitor import (
from src.server.providers.health_monitor import (
ProviderHealthMetrics,
ProviderHealthMonitor,
RequestMetric,

View File

@@ -1,749 +0,0 @@
"""
Unit tests for Serie class to verify key validation and identifier usage.
"""
import json
import os
import tempfile
import pytest
from src.core.entities.series import Serie
class TestSerieValidation:
"""Test Serie class validation logic."""
def test_serie_creation_with_valid_key(self):
"""Test creating Serie with valid key."""
serie = Serie(
key="attack-on-titan",
name="Attack on Titan",
site="https://aniworld.to/anime/stream/attack-on-titan",
folder="Attack on Titan (2013)",
episodeDict={1: [1, 2, 3], 2: [1, 2]}
)
assert serie.key == "attack-on-titan"
assert serie.name == "Attack on Titan"
assert serie.site == "https://aniworld.to/anime/stream/attack-on-titan"
assert serie.folder == "Attack on Titan (2013)"
assert serie.episodeDict == {1: [1, 2, 3], 2: [1, 2]}
def test_serie_creation_with_empty_key_raises_error(self):
"""Test that creating Serie with empty key raises ValueError."""
with pytest.raises(ValueError, match="key cannot be None or empty"):
Serie(
key="",
name="Test Series",
site="https://example.com",
folder="Test Folder",
episodeDict={1: [1]}
)
def test_serie_creation_with_whitespace_key_raises_error(self):
"""Test that creating Serie with whitespace-only key raises error."""
with pytest.raises(ValueError, match="key cannot be None or empty"):
Serie(
key=" ",
name="Test Series",
site="https://example.com",
folder="Test Folder",
episodeDict={1: [1]}
)
def test_serie_key_is_stripped(self):
"""Test that Serie key is stripped of whitespace."""
serie = Serie(
key=" attack-on-titan ",
name="Attack on Titan",
site="https://example.com",
folder="Attack on Titan (2013)",
episodeDict={1: [1]}
)
assert serie.key == "attack-on-titan"
def test_serie_key_setter_with_valid_value(self):
"""Test setting key property with valid value."""
serie = Serie(
key="initial-key",
name="Test",
site="https://example.com",
folder="Test Folder",
episodeDict={1: [1]}
)
serie.key = "new-key"
assert serie.key == "new-key"
def test_serie_key_setter_with_empty_value_raises_error(self):
"""Test that setting key to empty string raises ValueError."""
serie = Serie(
key="initial-key",
name="Test",
site="https://example.com",
folder="Test Folder",
episodeDict={1: [1]}
)
with pytest.raises(ValueError, match="key cannot be None or empty"):
serie.key = ""
def test_serie_key_setter_with_whitespace_raises_error(self):
"""Test that setting key to whitespace raises ValueError."""
serie = Serie(
key="initial-key",
name="Test",
site="https://example.com",
folder="Test Folder",
episodeDict={1: [1]}
)
with pytest.raises(ValueError, match="key cannot be None or empty"):
serie.key = " "
def test_serie_key_setter_strips_whitespace(self):
"""Test that key setter strips whitespace."""
serie = Serie(
key="initial-key",
name="Test",
site="https://example.com",
folder="Test Folder",
episodeDict={1: [1]}
)
serie.key = " new-key "
assert serie.key == "new-key"
class TestSerieProperties:
"""Test Serie class properties and methods."""
def test_serie_str_representation(self):
"""Test string representation of Serie."""
serie = Serie(
key="test-key",
name="Test Series",
site="https://example.com",
folder="Test Folder",
episodeDict={1: [1, 2]}
)
str_repr = str(serie)
assert "key='test-key'" in str_repr
assert "name='Test Series'" in str_repr
assert "folder='Test Folder'" in str_repr
def test_serie_to_dict(self):
"""Test conversion of Serie to dictionary."""
serie = Serie(
key="test-key",
name="Test Series",
site="https://example.com",
folder="Test Folder",
episodeDict={1: [1, 2], 2: [1, 2, 3]}
)
data = serie.to_dict()
assert data["key"] == "test-key"
assert data["name"] == "Test Series"
assert data["site"] == "https://example.com"
assert data["folder"] == "Test Folder"
assert "1" in data["episodeDict"]
assert data["episodeDict"]["1"] == [1, 2]
def test_serie_from_dict(self):
"""Test creating Serie from dictionary."""
data = {
"key": "test-key",
"name": "Test Series",
"site": "https://example.com",
"folder": "Test Folder",
"episodeDict": {"1": [1, 2], "2": [1, 2, 3]}
}
serie = Serie.from_dict(data)
assert serie.key == "test-key"
assert serie.name == "Test Series"
assert serie.folder == "Test Folder"
assert serie.episodeDict == {1: [1, 2], 2: [1, 2, 3]}
def test_serie_save_and_load_from_file(self):
"""Test saving and loading Serie from file."""
import warnings
serie = Serie(
key="test-key",
name="Test Series",
site="https://example.com",
folder="Test Folder",
episodeDict={1: [1, 2, 3]}
)
# Create temporary file
with tempfile.NamedTemporaryFile(
mode='w',
delete=False,
suffix='.json'
) as f:
temp_filename = f.name
try:
# Suppress deprecation warnings for this test
with warnings.catch_warnings():
warnings.simplefilter("ignore", DeprecationWarning)
# Save to file
serie.save_to_file(temp_filename)
# Load from file
loaded_serie = Serie.load_from_file(temp_filename)
# Verify all properties match
assert loaded_serie.key == serie.key
assert loaded_serie.name == serie.name
assert loaded_serie.site == serie.site
assert loaded_serie.folder == serie.folder
assert loaded_serie.episodeDict == serie.episodeDict
finally:
# Cleanup
if os.path.exists(temp_filename):
os.remove(temp_filename)
def test_serie_folder_is_mutable(self):
"""Test that folder property can be changed (it's metadata only)."""
serie = Serie(
key="test-key",
name="Test",
site="https://example.com",
folder="Old Folder",
episodeDict={1: [1]}
)
serie.folder = "New Folder"
assert serie.folder == "New Folder"
# Key should remain unchanged
assert serie.key == "test-key"
class TestSerieDocumentation:
"""Test that Serie class has proper documentation."""
def test_serie_class_has_docstring(self):
"""Test that Serie class has a docstring."""
assert Serie.__doc__ is not None
assert "unique identifier" in Serie.__doc__.lower()
def test_key_property_has_docstring(self):
"""Test that key property has descriptive docstring."""
assert Serie.key.fget.__doc__ is not None
assert "unique" in Serie.key.fget.__doc__.lower()
assert "identifier" in Serie.key.fget.__doc__.lower()
def test_folder_property_has_docstring(self):
"""Test that folder property documents it's metadata only."""
assert Serie.folder.fget.__doc__ is not None
assert "metadata" in Serie.folder.fget.__doc__.lower()
assert "not used for lookups" in Serie.folder.fget.__doc__.lower()
class TestSerieDeprecationWarnings:
"""Test deprecation warnings for file-based methods."""
def test_save_to_file_raises_deprecation_warning(self):
"""Test save_to_file() raises deprecation warning."""
import warnings
serie = Serie(
key="test-key",
name="Test Series",
site="https://example.com",
folder="Test Folder",
episodeDict={1: [1, 2, 3]}
)
with tempfile.NamedTemporaryFile(
mode='w', suffix='.json', delete=False
) as temp_file:
temp_filename = temp_file.name
try:
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always")
serie.save_to_file(temp_filename)
# Check deprecation warning was raised
assert len(w) == 1
assert issubclass(w[0].category, DeprecationWarning)
assert "deprecated" in str(w[0].message).lower()
assert "save_to_file" in str(w[0].message)
finally:
if os.path.exists(temp_filename):
os.remove(temp_filename)
def test_load_from_file_raises_deprecation_warning(self):
"""Test load_from_file() raises deprecation warning."""
import warnings
serie = Serie(
key="test-key",
name="Test Series",
site="https://example.com",
folder="Test Folder",
episodeDict={1: [1, 2, 3]}
)
with tempfile.NamedTemporaryFile(
mode='w', suffix='.json', delete=False
) as temp_file:
temp_filename = temp_file.name
try:
# Save first (suppress warning for this)
with warnings.catch_warnings():
warnings.simplefilter("ignore")
serie.save_to_file(temp_filename)
# Now test loading
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always")
Serie.load_from_file(temp_filename)
# Check deprecation warning was raised
assert len(w) == 1
assert issubclass(w[0].category, DeprecationWarning)
assert "deprecated" in str(w[0].message).lower()
assert "load_from_file" in str(w[0].message)
finally:
if os.path.exists(temp_filename):
os.remove(temp_filename)
class TestSerieSanitizedFolder:
"""Test Serie.sanitized_folder property."""
def test_sanitized_folder_from_name(self):
"""Test that sanitized_folder uses the name property."""
serie = Serie(
key="attack-on-titan",
name="Attack on Titan: Final Season",
site="aniworld.to",
folder="old-folder",
episodeDict={}
)
result = serie.sanitized_folder
assert ":" not in result
assert "Attack on Titan" in result
def test_sanitized_folder_removes_special_chars(self):
"""Test that special characters are removed."""
serie = Serie(
key="re-zero",
name="Re:Zero - Starting Life in Another World?",
site="aniworld.to",
folder="old-folder",
episodeDict={}
)
result = serie.sanitized_folder
assert ":" not in result
assert "?" not in result
def test_sanitized_folder_fallback_to_folder(self):
"""Test fallback to folder when name is empty."""
serie = Serie(
key="test-key",
name="",
site="aniworld.to",
folder="Valid Folder Name",
episodeDict={}
)
result = serie.sanitized_folder
assert result == "Valid Folder Name"
def test_sanitized_folder_fallback_to_key(self):
"""Test fallback to key when name and folder can't be sanitized."""
serie = Serie(
key="valid-key",
name="",
site="aniworld.to",
folder="",
episodeDict={}
)
result = serie.sanitized_folder
assert result == "valid-key"
def test_sanitized_folder_preserves_unicode(self):
"""Test that Unicode characters are preserved."""
serie = Serie(
key="japanese-anime",
name="進撃の巨人",
site="aniworld.to",
folder="old-folder",
episodeDict={}
)
result = serie.sanitized_folder
assert "進撃の巨人" in result
def test_sanitized_folder_with_various_anime_titles(self):
"""Test sanitized_folder with real anime titles."""
test_cases = [
("fate-stay-night", "Fate/Stay Night: UBW"),
("86-eighty-six", "86: Eighty-Six"),
("steins-gate", "Steins;Gate"),
]
for key, name in test_cases:
serie = Serie(
key=key,
name=name,
site="aniworld.to",
folder="old-folder",
episodeDict={}
)
result = serie.sanitized_folder
# Verify invalid filesystem characters are removed
# Note: semicolon is valid on Linux but we test common invalid chars
assert ":" not in result
assert "/" not in result
class TestSerieNFOFeatures:
"""Test Serie class NFO-related features."""
def test_serie_creation_with_nfo_path(self):
"""Test creating Serie with NFO path."""
serie = Serie(
key="test-series",
name="Test Series",
site="https://example.com",
folder="Test Folder",
episodeDict={1: [1]},
nfo_path="/path/to/tvshow.nfo"
)
assert serie.nfo_path == "/path/to/tvshow.nfo"
def test_serie_creation_without_nfo_path(self):
"""Test creating Serie without NFO path defaults to None."""
serie = Serie(
key="test-series",
name="Test Series",
site="https://example.com",
folder="Test Folder",
episodeDict={1: [1]}
)
assert serie.nfo_path is None
def test_serie_nfo_path_setter(self):
"""Test setting NFO path property."""
serie = Serie(
key="test-series",
name="Test Series",
site="https://example.com",
folder="Test Folder",
episodeDict={1: [1]}
)
serie.nfo_path = "/new/path/tvshow.nfo"
assert serie.nfo_path == "/new/path/tvshow.nfo"
def test_has_nfo_with_existing_file(self, tmp_path):
"""Test has_nfo returns True when NFO file exists."""
# Create a test directory structure
base_dir = tmp_path / "anime"
series_dir = base_dir / "Test Series"
series_dir.mkdir(parents=True)
nfo_file = series_dir / "tvshow.nfo"
nfo_file.write_text("test nfo content")
serie = Serie(
key="test-series",
name="Test Series",
site="https://example.com",
folder="Test Series",
episodeDict={1: [1]}
)
assert serie.has_nfo(str(base_dir)) is True
def test_has_nfo_with_missing_file(self, tmp_path):
"""Test has_nfo returns False when NFO file doesn't exist."""
base_dir = tmp_path / "anime"
base_dir.mkdir(parents=True)
serie = Serie(
key="test-series",
name="Test Series",
site="https://example.com",
folder="Test Series",
episodeDict={1: [1]}
)
assert serie.has_nfo(str(base_dir)) is False
def test_has_nfo_with_nfo_path_set(self, tmp_path):
"""Test has_nfo using nfo_path when base_directory not provided."""
nfo_file = tmp_path / "tvshow.nfo"
nfo_file.write_text("test nfo content")
serie = Serie(
key="test-series",
name="Test Series",
site="https://example.com",
folder="Test Series",
episodeDict={1: [1]},
nfo_path=str(nfo_file)
)
assert serie.has_nfo() is True
def test_has_nfo_without_base_directory_or_path(self):
"""Test has_nfo returns False when no base_directory or nfo_path."""
serie = Serie(
key="test-series",
name="Test Series",
site="https://example.com",
folder="Test Series",
episodeDict={1: [1]}
)
assert serie.has_nfo() is False
def test_has_poster_with_existing_file(self, tmp_path):
"""Test has_poster returns True when poster.jpg exists."""
base_dir = tmp_path / "anime"
series_dir = base_dir / "Test Series"
series_dir.mkdir(parents=True)
poster_file = series_dir / "poster.jpg"
poster_file.write_text("test image data")
serie = Serie(
key="test-series",
name="Test Series",
site="https://example.com",
folder="Test Series",
episodeDict={1: [1]}
)
assert serie.has_poster(str(base_dir)) is True
def test_has_poster_with_missing_file(self, tmp_path):
"""Test has_poster returns False when poster.jpg doesn't exist."""
base_dir = tmp_path / "anime"
base_dir.mkdir(parents=True)
serie = Serie(
key="test-series",
name="Test Series",
site="https://example.com",
folder="Test Series",
episodeDict={1: [1]}
)
assert serie.has_poster(str(base_dir)) is False
def test_has_poster_without_base_directory(self):
"""Test has_poster returns False when no base_directory provided."""
serie = Serie(
key="test-series",
name="Test Series",
site="https://example.com",
folder="Test Series",
episodeDict={1: [1]}
)
assert serie.has_poster() is False
def test_has_logo_with_existing_file(self, tmp_path):
"""Test has_logo returns True when logo.png exists."""
base_dir = tmp_path / "anime"
series_dir = base_dir / "Test Series"
series_dir.mkdir(parents=True)
logo_file = series_dir / "logo.png"
logo_file.write_text("test logo data")
serie = Serie(
key="test-series",
name="Test Series",
site="https://example.com",
folder="Test Series",
episodeDict={1: [1]}
)
assert serie.has_logo(str(base_dir)) is True
def test_has_logo_with_missing_file(self, tmp_path):
"""Test has_logo returns False when logo.png doesn't exist."""
base_dir = tmp_path / "anime"
base_dir.mkdir(parents=True)
serie = Serie(
key="test-series",
name="Test Series",
site="https://example.com",
folder="Test Series",
episodeDict={1: [1]}
)
assert serie.has_logo(str(base_dir)) is False
def test_has_logo_without_base_directory(self):
"""Test has_logo returns False when no base_directory provided."""
serie = Serie(
key="test-series",
name="Test Series",
site="https://example.com",
folder="Test Series",
episodeDict={1: [1]}
)
assert serie.has_logo() is False
def test_has_fanart_with_existing_file(self, tmp_path):
"""Test has_fanart returns True when fanart.jpg exists."""
base_dir = tmp_path / "anime"
series_dir = base_dir / "Test Series"
series_dir.mkdir(parents=True)
fanart_file = series_dir / "fanart.jpg"
fanart_file.write_text("test fanart data")
serie = Serie(
key="test-series",
name="Test Series",
site="https://example.com",
folder="Test Series",
episodeDict={1: [1]}
)
assert serie.has_fanart(str(base_dir)) is True
def test_has_fanart_with_missing_file(self, tmp_path):
"""Test has_fanart returns False when fanart.jpg doesn't exist."""
base_dir = tmp_path / "anime"
base_dir.mkdir(parents=True)
serie = Serie(
key="test-series",
name="Test Series",
site="https://example.com",
folder="Test Series",
episodeDict={1: [1]}
)
assert serie.has_fanart(str(base_dir)) is False
def test_has_fanart_without_base_directory(self):
"""Test has_fanart returns False when no base_directory provided."""
serie = Serie(
key="test-series",
name="Test Series",
site="https://example.com",
folder="Test Series",
episodeDict={1: [1]}
)
assert serie.has_fanart() is False
def test_to_dict_includes_nfo_path(self):
"""Test that to_dict includes nfo_path field."""
serie = Serie(
key="test-series",
name="Test Series",
site="https://example.com",
folder="Test Folder",
episodeDict={1: [1, 2], 2: [1]},
year=2024,
nfo_path="/path/to/tvshow.nfo"
)
result = serie.to_dict()
assert result["nfo_path"] == "/path/to/tvshow.nfo"
assert result["key"] == "test-series"
assert result["name"] == "Test Series"
assert result["year"] == 2024
def test_to_dict_with_none_nfo_path(self):
"""Test that to_dict handles None nfo_path."""
serie = Serie(
key="test-series",
name="Test Series",
site="https://example.com",
folder="Test Folder",
episodeDict={1: [1]}
)
result = serie.to_dict()
assert result["nfo_path"] is None
def test_from_dict_with_nfo_path(self):
"""Test that from_dict correctly loads nfo_path."""
data = {
"key": "test-series",
"name": "Test Series",
"site": "https://example.com",
"folder": "Test Folder",
"episodeDict": {"1": [1, 2]},
"year": 2024,
"nfo_path": "/path/to/tvshow.nfo"
}
serie = Serie.from_dict(data)
assert serie.nfo_path == "/path/to/tvshow.nfo"
assert serie.key == "test-series"
assert serie.year == 2024
def test_from_dict_without_nfo_path(self):
"""Test that from_dict handles missing nfo_path (backward compatibility)."""
data = {
"key": "test-series",
"name": "Test Series",
"site": "https://example.com",
"folder": "Test Folder",
"episodeDict": {"1": [1, 2]}
}
serie = Serie.from_dict(data)
assert serie.nfo_path is None
assert serie.key == "test-series"
def test_save_and_load_file_with_nfo_path(self, tmp_path):
"""Test that save_to_file and load_from_file preserve nfo_path."""
serie = Serie(
key="test-series",
name="Test Series",
site="https://example.com",
folder="Test Folder",
episodeDict={1: [1, 2], 2: [1]},
year=2024,
nfo_path="/path/to/tvshow.nfo"
)
file_path = tmp_path / "data"
with pytest.warns(DeprecationWarning):
serie.save_to_file(str(file_path))
with pytest.warns(DeprecationWarning):
loaded_serie = Serie.load_from_file(str(file_path))
assert loaded_serie.nfo_path == "/path/to/tvshow.nfo"
assert loaded_serie.key == "test-series"
assert loaded_serie.year == 2024

View File

@@ -1,94 +0,0 @@
"""Tests for Serie.ensure_folder_with_year() method."""
import pytest
from src.core.entities.series import Serie
class TestSerieEnsureFolderWithYear:
"""Test suite for ensure_folder_with_year method."""
def test_ensure_folder_with_year_adds_year(self):
"""Test that ensure_folder_with_year adds year to folder name."""
serie = Serie(
key="perfect-blue",
name="Perfect Blue",
site="aniworld.to",
folder="Perfect Blue",
episodeDict={1: [1, 2, 3]},
year=1997
)
result = serie.ensure_folder_with_year()
assert result == "Perfect Blue (1997)"
assert serie.folder == "Perfect Blue (1997)"
def test_ensure_folder_with_year_already_has_year(self):
"""Test that ensure_folder_with_year doesn't duplicate year."""
serie = Serie(
key="blue-exorcist",
name="Blue Exorcist",
site="aniworld.to",
folder="Blue Exorcist (2011)",
episodeDict={1: [1, 2, 3]},
year=2011
)
result = serie.ensure_folder_with_year()
assert result == "Blue Exorcist (2011)"
assert serie.folder == "Blue Exorcist (2011)"
def test_ensure_folder_with_year_no_year_available(self):
"""Test that ensure_folder_with_year returns folder unchanged if no year."""
serie = Serie(
key="unknown-anime",
name="Unknown Anime",
site="aniworld.to",
folder="Unknown Anime",
episodeDict={1: [1, 2, 3]},
year=None
)
result = serie.ensure_folder_with_year()
assert result == "Unknown Anime"
assert serie.folder == "Unknown Anime"
def test_ensure_folder_with_year_sanitizes_name(self):
"""Test that ensure_folder_with_year uses sanitized_folder property."""
serie = Serie(
key="attack-on-titan",
name="Attack on Titan: Final Season",
site="aniworld.to",
folder="Attack on Titan Final", # Old folder without year
episodeDict={1: [1, 2, 3]},
year=2020
)
result = serie.ensure_folder_with_year()
# Should use sanitized version of name_with_year
assert "(2020)" in result
assert serie.folder == result
# Colon should be removed by sanitization
assert ":" not in result
def test_ensure_folder_with_year_updates_folder_property(self):
"""Test that folder property is updated when year is added."""
serie = Serie(
key="dororo",
name="Dororo",
site="aniworld.to",
folder="Dororo",
episodeDict={1: [1, 2, 3]},
year=2019
)
original_folder = serie.folder
result = serie.ensure_folder_with_year()
assert original_folder == "Dororo"
assert result == "Dororo (2019)"
assert serie.folder == "Dororo (2019)"
assert serie.folder != original_folder

View File

@@ -1,212 +1,136 @@
"""Tests for SerieList class - identifier standardization."""
# pylint: disable=redefined-outer-name
"""Tests for SerieList class - DB-only operations."""
import os
import tempfile
import warnings
from unittest.mock import MagicMock
import pytest
from src.core.entities.SerieList import SerieList
from src.core.entities.series import Serie
@pytest.fixture
def temp_directory():
"""Create a temporary directory for testing."""
with tempfile.TemporaryDirectory() as tmpdir:
yield tmpdir
from src.server.database.SerieList import SerieList
@pytest.fixture
def sample_serie():
"""Create a sample Serie for testing."""
return Serie(
key="attack-on-titan",
name="Attack on Titan",
site="https://aniworld.to/anime/stream/attack-on-titan",
folder="Attack on Titan (2013)",
episodeDict={1: [1, 2, 3]}
)
"""Create a sample AnimeSeries mock for testing."""
anime = MagicMock()
anime.key = "attack-on-titan"
anime.name = "Attack on Titan"
anime.site = "https://aniworld.to/anime/stream/attack-on-titan"
anime.folder = "Attack on Titan (2013)"
anime.year = 2013
anime.nfo_path = None
anime.episodeDict = {1: [1, 2, 3]}
return anime
class TestSerieListKeyBasedStorage:
"""Test SerieList uses key for internal storage."""
def test_init_creates_empty_keydict(self, temp_directory):
def test_init_creates_empty_keydict(self, tmp_path):
"""Test initialization creates keyDict."""
serie_list = SerieList(temp_directory)
serie_list = SerieList(str(tmp_path))
assert hasattr(serie_list, 'keyDict')
assert isinstance(serie_list.keyDict, dict)
assert len(serie_list.keyDict) == 0
def test_add_stores_by_key(self, temp_directory, sample_serie):
"""Test add() stores series by key."""
serie_list = SerieList(temp_directory)
with warnings.catch_warnings():
warnings.simplefilter("ignore", DeprecationWarning)
serie_list.add(sample_serie)
# Verify stored by key, not folder
assert sample_serie.key in serie_list.keyDict
assert serie_list.keyDict[sample_serie.key] == sample_serie
def test_contains_checks_by_key(self, temp_directory, sample_serie):
def test_contains_checks_by_key(self, tmp_path, sample_serie):
"""Test contains() checks by key."""
serie_list = SerieList(temp_directory)
with warnings.catch_warnings():
warnings.simplefilter("ignore", DeprecationWarning)
serie_list.add(sample_serie)
serie_list = SerieList(str(tmp_path))
serie_list.keyDict[sample_serie.key] = sample_serie
assert serie_list.contains(sample_serie.key)
assert not serie_list.contains("nonexistent-key")
def test_add_prevents_duplicates_by_key(
self, temp_directory, sample_serie
):
"""Test add() prevents duplicates based on key."""
serie_list = SerieList(temp_directory)
with warnings.catch_warnings():
warnings.simplefilter("ignore", DeprecationWarning)
# Add same serie twice
serie_list.add(sample_serie)
initial_count = len(serie_list.keyDict)
serie_list.add(sample_serie)
# Should still have only one entry
assert len(serie_list.keyDict) == initial_count
assert len(serie_list.keyDict) == 1
def test_get_by_key_returns_correct_serie(
self, temp_directory, sample_serie
):
def test_get_by_key_returns_correct_serie(self, tmp_path, sample_serie):
"""Test get_by_key() retrieves series correctly."""
serie_list = SerieList(temp_directory)
with warnings.catch_warnings():
warnings.simplefilter("ignore", DeprecationWarning)
serie_list.add(sample_serie)
serie_list = SerieList(str(tmp_path))
serie_list.keyDict[sample_serie.key] = sample_serie
result = serie_list.get_by_key(sample_serie.key)
assert result is not None
assert result.key == sample_serie.key
assert result.name == sample_serie.name
def test_get_by_key_returns_none_for_missing(self, temp_directory):
def test_get_by_key_returns_none_for_missing(self, tmp_path):
"""Test get_by_key() returns None for nonexistent key."""
serie_list = SerieList(temp_directory)
serie_list = SerieList(str(tmp_path))
result = serie_list.get_by_key("nonexistent-key")
assert result is None
def test_get_by_folder_backward_compatibility(
self, temp_directory, sample_serie
):
def test_get_by_folder_backward_compatibility(self, tmp_path, sample_serie):
"""Test get_by_folder() provides backward compatibility."""
serie_list = SerieList(temp_directory)
import warnings
serie_list = SerieList(str(tmp_path))
serie_list.keyDict[sample_serie.key] = sample_serie
with warnings.catch_warnings():
warnings.simplefilter("ignore", DeprecationWarning)
serie_list.add(sample_serie)
result = serie_list.get_by_folder(sample_serie.folder)
assert result is not None
assert result.key == sample_serie.key
assert result.folder == sample_serie.folder
def test_get_by_folder_returns_none_for_missing(self, temp_directory):
def test_get_by_folder_returns_none_for_missing(self, tmp_path):
"""Test get_by_folder() returns None for nonexistent folder."""
serie_list = SerieList(temp_directory)
import warnings
serie_list = SerieList(str(tmp_path))
with warnings.catch_warnings():
warnings.simplefilter("ignore", DeprecationWarning)
result = serie_list.get_by_folder("Nonexistent Folder")
assert result is None
def test_get_all_returns_all_series(self, temp_directory, sample_serie):
def test_get_all_returns_all_series(self, tmp_path, sample_serie):
"""Test get_all() returns all series from keyDict."""
serie_list = SerieList(temp_directory)
serie_list = SerieList(str(tmp_path))
serie2 = Serie(
key="naruto",
name="Naruto",
site="https://aniworld.to/anime/stream/naruto",
folder="Naruto (2002)",
episodeDict={1: [1, 2]}
)
serie2 = MagicMock()
serie2.key = "naruto"
serie2.name = "Naruto"
serie2.site = "https://aniworld.to/anime/stream/naruto"
serie2.folder = "Naruto (2002)"
serie2.episodeDict = {1: [1, 2]}
with warnings.catch_warnings():
warnings.simplefilter("ignore", DeprecationWarning)
serie_list.add(sample_serie)
serie_list.add(serie2)
serie_list.keyDict[sample_serie.key] = sample_serie
serie_list.keyDict[serie2.key] = serie2
all_series = serie_list.get_all()
assert len(all_series) == 2
assert sample_serie in all_series
assert serie2 in all_series
def test_get_missing_episodes_filters_by_episode_dict(
self, temp_directory
):
def test_get_missing_episodes_filters_by_episode_dict(self, tmp_path):
"""Test get_missing_episodes() returns only series with episodes."""
serie_list = SerieList(temp_directory)
serie_list = SerieList(str(tmp_path))
# Serie with missing episodes
serie_with_episodes = Serie(
key="serie-with-episodes",
name="Serie With Episodes",
site="https://aniworld.to/anime/stream/serie-with-episodes",
folder="Serie With Episodes (2020)",
episodeDict={1: [1, 2, 3]}
)
serie_with_episodes = MagicMock()
serie_with_episodes.key = "serie-with-episodes"
serie_with_episodes.name = "Serie With Episodes"
serie_with_episodes.episodeDict = {1: [1, 2, 3]}
# Serie without missing episodes
serie_without_episodes = Serie(
key="serie-without-episodes",
name="Serie Without Episodes",
site="https://aniworld.to/anime/stream/serie-without-episodes",
folder="Serie Without Episodes (2021)",
episodeDict={}
)
serie_without_episodes = MagicMock()
serie_without_episodes.key = "serie-without-episodes"
serie_without_episodes.name = "Serie Without Episodes"
serie_without_episodes.episodeDict = {}
with warnings.catch_warnings():
warnings.simplefilter("ignore", DeprecationWarning)
serie_list.add(serie_with_episodes)
serie_list.add(serie_without_episodes)
serie_list.keyDict[serie_with_episodes.key] = serie_with_episodes
serie_list.keyDict[serie_without_episodes.key] = serie_without_episodes
missing = serie_list.get_missing_episodes()
assert len(missing) == 1
assert serie_with_episodes in missing
assert serie_without_episodes not in missing
def test_load_series_stores_by_key(self, temp_directory, sample_serie):
"""Test load_series() stores series by key when loading from disk."""
# Create directory structure and save serie
folder_path = os.path.join(temp_directory, sample_serie.folder)
os.makedirs(folder_path, exist_ok=True)
data_path = os.path.join(folder_path, "data")
sample_serie.save_to_file(data_path)
# Create new SerieList (triggers load_series in __init__)
serie_list = SerieList(temp_directory)
# Verify loaded by key
assert sample_serie.key in serie_list.keyDict
loaded_serie = serie_list.keyDict[sample_serie.key]
assert loaded_serie.key == sample_serie.key
assert loaded_serie.name == sample_serie.name
class TestSerieListPublicAPI:
"""Test that public API still works correctly."""
def test_public_methods_work(self, temp_directory, sample_serie):
def test_public_methods_work(self, tmp_path, sample_serie):
"""Test that all public methods work correctly after refactoring."""
serie_list = SerieList(temp_directory)
serie_list = SerieList(str(tmp_path))
# Test add (suppress deprecation warning for test)
with warnings.catch_warnings():
warnings.simplefilter("ignore", DeprecationWarning)
serie_list.add(sample_serie)
# Add directly to keyDict (simulating DB load)
serie_list.keyDict[sample_serie.key] = sample_serie
# Test contains
assert serie_list.contains(sample_serie.key)
@@ -219,30 +143,17 @@ class TestSerieListPublicAPI:
assert len(serie_list.GetMissingEpisode()) == 1
assert len(serie_list.get_missing_episodes()) == 1
# Test new helper methods
# Test get_by_key
assert serie_list.get_by_key(sample_serie.key) is not None
with warnings.catch_warnings():
warnings.simplefilter("ignore", DeprecationWarning)
assert serie_list.get_by_folder(sample_serie.folder) is not None
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
class TestSerieListDeprecationWarnings:
"""Test deprecation warnings are raised for file-based methods."""
"""Test deprecation warnings are raised for deprecated methods."""
def test_get_by_folder_raises_deprecation_warning(
self, temp_directory, sample_serie
):
def test_get_by_folder_raises_deprecation_warning(self, tmp_path, sample_serie):
"""Test get_by_folder() raises deprecation warning."""
serie_list = SerieList(temp_directory, skip_load=True)
import warnings
serie_list = SerieList(str(tmp_path))
serie_list.keyDict[sample_serie.key] = sample_serie
with warnings.catch_warnings(record=True) as w:
@@ -255,267 +166,15 @@ class TestSerieListDeprecationWarnings:
assert "get_by_key()" in str(w[0].message)
class TestSerieListBackwardCompatibility:
"""Test backward compatibility of file-based operations."""
class TestInvalidateCache:
"""Test invalidate_cache method."""
def test_file_based_mode_still_works(
self, temp_directory, sample_serie
):
"""Test file-based mode still works without db_session."""
serie_list = SerieList(temp_directory)
def test_invalidate_cache_clears_keydict(self, tmp_path, sample_serie):
"""Verify invalidate_cache clears the in-memory cache."""
serie_list = SerieList(str(tmp_path))
serie_list.keyDict[sample_serie.key] = sample_serie
assert len(serie_list.keyDict) == 1
# Add should still work (with deprecation warning)
with warnings.catch_warnings():
warnings.simplefilter("ignore", DeprecationWarning)
serie_list.add(sample_serie)
serie_list.invalidate_cache()
# File should be created
data_path = os.path.join(
temp_directory, sample_serie.folder, "data"
)
assert os.path.isfile(data_path)
# Series should be in memory
assert serie_list.contains(sample_serie.key)
def test_load_from_file_still_works(
self, temp_directory, sample_serie
):
"""Test loading from files still works."""
# Create directory and save file
folder_path = os.path.join(temp_directory, sample_serie.folder)
os.makedirs(folder_path, exist_ok=True)
data_path = os.path.join(folder_path, "data")
sample_serie.save_to_file(data_path)
# New SerieList should load it
serie_list = SerieList(temp_directory)
assert serie_list.contains(sample_serie.key)
loaded = serie_list.get_by_key(sample_serie.key)
assert loaded.name == sample_serie.name
class TestSerieListNFOFeatures:
"""Test SerieList NFO detection and logging."""
def test_load_series_detects_nfo_file(self, temp_directory, caplog):
"""Test load_series detects and sets nfo_path for series with NFO."""
import logging
caplog.set_level(logging.INFO)
# Create series folder with data file and NFO
folder_name = "Test Series"
folder_path = os.path.join(temp_directory, folder_name)
os.makedirs(folder_path)
serie = Serie(
key="test-series",
name="Test Series",
site="https://example.com",
folder=folder_name,
episodeDict={1: [1, 2]}
)
data_path = os.path.join(folder_path, "data")
with warnings.catch_warnings():
warnings.simplefilter("ignore", DeprecationWarning)
serie.save_to_file(data_path)
# Create NFO file
nfo_path = os.path.join(folder_path, "tvshow.nfo")
with open(nfo_path, "w") as f:
f.write("<tvshow></tvshow>")
# Load series
serie_list = SerieList(temp_directory)
# Verify NFO was detected
loaded = serie_list.get_by_key("test-series")
assert loaded is not None
assert loaded.nfo_path == nfo_path
# Verify logging
assert "1 with NFO" in caplog.text
def test_load_series_detects_missing_nfo(self, temp_directory, caplog):
"""Test load_series logs when NFO is missing."""
import logging
caplog.set_level(logging.DEBUG)
# Create series folder with data file but NO NFO
folder_name = "Test Series"
folder_path = os.path.join(temp_directory, folder_name)
os.makedirs(folder_path)
serie = Serie(
key="test-series",
name="Test Series",
site="https://example.com",
folder=folder_name,
episodeDict={1: [1, 2]}
)
data_path = os.path.join(folder_path, "data")
with warnings.catch_warnings():
warnings.simplefilter("ignore", DeprecationWarning)
serie.save_to_file(data_path)
# Load series
serie_list = SerieList(temp_directory)
# Verify NFO not set
loaded = serie_list.get_by_key("test-series")
assert loaded is not None
assert loaded.nfo_path is None
# Verify logging
assert "missing tvshow.nfo" in caplog.text
def test_load_series_detects_media_files(self, temp_directory, caplog):
"""Test load_series detects poster, logo, and fanart files."""
import logging
caplog.set_level(logging.INFO)
# Create series folder with all media files
folder_name = "Test Series"
folder_path = os.path.join(temp_directory, folder_name)
os.makedirs(folder_path)
serie = Serie(
key="test-series",
name="Test Series",
site="https://example.com",
folder=folder_name,
episodeDict={1: [1, 2]}
)
data_path = os.path.join(folder_path, "data")
with warnings.catch_warnings():
warnings.simplefilter("ignore", DeprecationWarning)
serie.save_to_file(data_path)
# Create media files
with open(os.path.join(folder_path, "poster.jpg"), "w") as f:
f.write("poster data")
with open(os.path.join(folder_path, "logo.png"), "w") as f:
f.write("logo data")
with open(os.path.join(folder_path, "fanart.jpg"), "w") as f:
f.write("fanart data")
# Load series
serie_list = SerieList(temp_directory)
# Verify logging shows all media found
assert "Poster (1/1)" in caplog.text
assert "Logo (1/1)" in caplog.text
assert "Fanart (1/1)" in caplog.text
def test_load_series_detects_missing_media_files(
self, temp_directory, caplog
):
"""Test load_series logs when media files are missing."""
import logging
caplog.set_level(logging.DEBUG)
# Create series folder with NO media files
folder_name = "Test Series"
folder_path = os.path.join(temp_directory, folder_name)
os.makedirs(folder_path)
serie = Serie(
key="test-series",
name="Test Series",
site="https://example.com",
folder=folder_name,
episodeDict={1: [1, 2]}
)
data_path = os.path.join(folder_path, "data")
with warnings.catch_warnings():
warnings.simplefilter("ignore", DeprecationWarning)
serie.save_to_file(data_path)
# Load series
serie_list = SerieList(temp_directory)
# Verify logging shows missing media
assert "missing poster.jpg" in caplog.text
assert "missing logo.png" in caplog.text
assert "missing fanart.jpg" in caplog.text
def test_load_series_summary_statistics(self, temp_directory, caplog):
"""Test load_series logs summary statistics for NFO and media."""
import logging
caplog.set_level(logging.INFO)
# Create multiple series with varying NFO/media status
for i in range(3):
folder_name = f"Series {i}"
folder_path = os.path.join(temp_directory, folder_name)
os.makedirs(folder_path)
serie = Serie(
key=f"series-{i}",
name=f"Series {i}",
site="https://example.com",
folder=folder_name,
episodeDict={1: [1]}
)
data_path = os.path.join(folder_path, "data")
with warnings.catch_warnings():
warnings.simplefilter("ignore", DeprecationWarning)
serie.save_to_file(data_path)
# First series has everything
if i == 0:
with open(os.path.join(folder_path, "tvshow.nfo"), "w") as f:
f.write("<tvshow></tvshow>")
with open(os.path.join(folder_path, "poster.jpg"), "w") as f:
f.write("poster")
with open(os.path.join(folder_path, "logo.png"), "w") as f:
f.write("logo")
with open(os.path.join(folder_path, "fanart.jpg"), "w") as f:
f.write("fanart")
# Second series has NFO and poster only
elif i == 1:
with open(os.path.join(folder_path, "tvshow.nfo"), "w") as f:
f.write("<tvshow></tvshow>")
with open(os.path.join(folder_path, "poster.jpg"), "w") as f:
f.write("poster")
# Third series has nothing
# Load series
serie_list = SerieList(temp_directory)
# Verify summary statistics
assert "3 series total" in caplog.text
assert "2 with NFO, 1 without NFO" in caplog.text
assert "Poster (2/3)" in caplog.text
assert "Logo (1/3)" in caplog.text
assert "Fanart (1/3)" in caplog.text
def test_load_series_handles_load_failure(self, temp_directory, caplog):
"""Test load_series handles series that fail to load gracefully."""
import logging
caplog.set_level(logging.ERROR)
# Create folder with invalid data file
folder_name = "Invalid Series"
folder_path = os.path.join(temp_directory, folder_name)
os.makedirs(folder_path)
data_path = os.path.join(folder_path, "data")
with open(data_path, "w") as f:
f.write("invalid json {{{")
# Load series - should not crash
serie_list = SerieList(temp_directory)
# Verify error logged
assert "Failed to load metadata" in caplog.text
# Should not be in keyDict
assert len(serie_list.keyDict) == 0

View File

@@ -4,7 +4,7 @@ from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from src.core.entities.series import Serie
from src.server.database.models import AnimeSeries
@pytest.fixture
@@ -24,27 +24,49 @@ def sample_anime_series():
mock.site = "aniworld.to"
mock.folder = "Attack on Titan (2013)"
mock.year = 2013
mock.episodes = [
MagicMock(season=1, episode_number=1),
MagicMock(season=1, episode_number=2),
MagicMock(season=1, episode_number=3),
MagicMock(season=2, episode_number=1),
MagicMock(season=2, episode_number=2),
]
# Create properly configured episode mocks that work with iteration
episode1 = MagicMock(season=1, episode_number=1)
episode2 = MagicMock(season=1, episode_number=2)
episode3 = MagicMock(season=1, episode_number=3)
episode4 = MagicMock(season=2, episode_number=1)
episode5 = MagicMock(season=2, episode_number=2)
mock.episodes = [episode1, episode2, episode3, episode4, episode5]
# Set _episode_dict_cache to None to force building from episodes
mock._episode_dict_cache = None
# Configure episodeDict as a property that computes from episodes
# This mirrors what the real AnimeSeries.episodeDict property does
def build_episode_dict():
episode_dict = {}
for ep in mock.episodes:
season = ep.season or 1
if season not in episode_dict:
episode_dict[season] = []
episode_dict[season].append(ep.episode_number or 0)
return episode_dict
# Create a mock property that returns computed dict
mock.episodeDict = property(lambda self: build_episode_dict())
# But we need it to work when accessed, not as a property object
# So configure the mock to return the dict directly when episodeDict is accessed
type(mock).episodeDict = property(lambda self: build_episode_dict())
return mock
@pytest.fixture
def sample_serie():
"""Create a sample Serie for testing."""
return Serie(
key="attack-on-titan",
name="Attack on Titan",
site="aniworld.to",
folder="Attack on Titan (2013)",
episodeDict={1: [1, 2, 3], 2: [1, 2]},
year=2013
)
"""Create a sample AnimeSeries mock for testing."""
anime = MagicMock(spec=AnimeSeries)
anime.key = "attack-on-titan"
anime.name = "Attack on Titan"
anime.site = "aniworld.to"
anime.folder = "Attack on Titan (2013)"
anime.year = 2013
anime.episodeDict = {1: [1, 2, 3], 2: [1, 2]}
return anime
class TestLoadAllFromDb:
@@ -63,9 +85,9 @@ class TestLoadAllFromDb:
"src.server.database.service.AnimeSeriesService.get_all",
return_value=[sample_anime_series]
):
from src.core.entities.SerieList import SerieList
from src.server.database.SerieList import SerieList
serie_list = SerieList("/tmp", skip_load=True)
serie_list = SerieList("/tmp")
count = await serie_list.load_all_from_db()
assert count == 1
@@ -98,9 +120,9 @@ class TestLoadAllFromDb:
"src.server.database.service.AnimeSeriesService.get_all",
return_value=[sample_anime_series, mock_series2]
):
from src.core.entities.SerieList import SerieList
from src.server.database.SerieList import SerieList
serie_list = SerieList("/tmp", skip_load=True)
serie_list = SerieList("/tmp")
count = await serie_list.load_all_from_db()
assert count == 2
@@ -122,9 +144,9 @@ class TestLoadAllFromDb:
"src.server.database.service.AnimeSeriesService.get_all",
return_value=[sample_anime_series]
):
from src.core.entities.SerieList import SerieList
from src.server.database.SerieList import SerieList
serie_list = SerieList("/tmp", skip_load=True)
serie_list = SerieList("/tmp")
await serie_list.load_all_from_db()
serie = serie_list.keyDict["attack-on-titan"]
@@ -146,9 +168,9 @@ class TestLoadAllFromDb:
"src.server.database.service.AnimeSeriesService.get_all",
return_value=[]
):
from src.core.entities.SerieList import SerieList
from src.server.database.SerieList import SerieList
serie_list = SerieList("/tmp", skip_load=True)
serie_list = SerieList("/tmp")
count = await serie_list.load_all_from_db()
assert count == 0
@@ -167,9 +189,9 @@ class TestLoadAllFromDb:
"src.server.database.service.AnimeSeriesService.get_all",
side_effect=RuntimeError("Database not initialized")
):
from src.core.entities.SerieList import SerieList
from src.server.database.SerieList import SerieList
serie_list = SerieList("/tmp", skip_load=True)
serie_list = SerieList("/tmp")
count = await serie_list.load_all_from_db()
assert count == 0
@@ -194,9 +216,9 @@ class TestLoadSingleSeriesFromDb:
"src.server.database.service.AnimeSeriesService.get_by_folder",
return_value=sample_anime_series
):
from src.core.entities.SerieList import SerieList
from src.server.database.SerieList import SerieList
serie_list = SerieList("/tmp", skip_load=True)
serie_list = SerieList("/tmp")
serie = await serie_list._load_single_series_from_db("Attack on Titan (2013)")
assert serie is not None
@@ -218,9 +240,9 @@ class TestLoadSingleSeriesFromDb:
"src.server.database.service.AnimeSeriesService.get_by_folder",
return_value=None
):
from src.core.entities.SerieList import SerieList
from src.server.database.SerieList import SerieList
serie_list = SerieList("/tmp", skip_load=True)
serie_list = SerieList("/tmp")
serie = await serie_list._load_single_series_from_db("Unknown Series")
assert serie is None
@@ -241,9 +263,9 @@ class TestLoadSingleSeriesFromDb:
"src.server.database.service.AnimeSeriesService.get_by_folder",
side_effect=RuntimeError("Database not initialized")
):
from src.core.entities.SerieList import SerieList
from src.server.database.SerieList import SerieList
serie_list = SerieList("/tmp", skip_load=True)
serie_list = SerieList("/tmp")
serie = await serie_list._load_single_series_from_db("Some Folder")
assert serie is None
@@ -254,9 +276,9 @@ class TestInvalidateCache:
def test_invalidate_cache_clears_keydict(self, sample_serie):
"""Verify invalidate_cache clears the in-memory cache."""
from src.core.entities.SerieList import SerieList
from src.server.database.SerieList import SerieList
serie_list = SerieList("/tmp", skip_load=True)
serie_list = SerieList("/tmp")
serie_list.keyDict["attack-on-titan"] = sample_serie
assert len(serie_list.keyDict) == 1
@@ -276,9 +298,9 @@ class TestInvalidateCache:
"src.server.database.service.AnimeSeriesService.get_all",
return_value=[sample_anime_series]
):
from src.core.entities.SerieList import SerieList
from src.server.database.SerieList import SerieList
serie_list = SerieList("/tmp", skip_load=True)
serie_list = SerieList("/tmp")
serie_list.keyDict["some-key"] = MagicMock()
serie_list.invalidate_cache()

View File

@@ -7,8 +7,8 @@ from unittest.mock import MagicMock, patch
import pytest
from src.core.entities.series import Serie
from src.core.SerieScanner import SerieScanner
from src.server.database.models import AnimeSeries
from src.server.SerieScanner import SerieScanner
@pytest.fixture
@@ -40,14 +40,16 @@ def mock_loader():
@pytest.fixture
def sample_serie():
"""Create a sample Serie for testing."""
return Serie(
key="attack-on-titan",
name="Attack on Titan",
site="aniworld.to",
folder="Attack on Titan (2013)",
episodeDict={1: [2, 3, 4]}
)
"""Create a sample AnimeSeries mock for testing."""
anime = MagicMock(spec=AnimeSeries)
anime.key = "attack-on-titan"
anime.name = "Attack on Titan"
anime.site = "aniworld.to"
anime.folder = "Attack on Titan (2013)"
anime.year = None
anime.nfo_path = None
anime.episodeDict = {1: [2, 3, 4]}
return anime
class TestSerieScannerInitialization:
@@ -134,7 +136,9 @@ class TestSerieScannerScan:
'_SerieScanner__get_missing_episodes_and_season',
return_value=({1: [2, 3]}, "aniworld.to")
):
with patch.object(sample_serie, 'save_to_file'):
with patch.object(
scanner, '_persist_serie_to_db'
):
scanner.scan()
assert sample_serie.key in scanner.keyDict
@@ -519,61 +523,17 @@ class TestFindMp4Files:
class TestReadDataFromFile:
"""Test __read_data_from_file method."""
def test_reads_data_file(self, mock_loader):
"""Should read Serie from 'data' file when no DB entry exists."""
import tempfile
def test_empty_folder_name_returns_none(self, temp_directory, mock_loader):
"""Empty folder name -> returns None (no DB lookup attempted)."""
scanner = SerieScanner(temp_directory, mock_loader)
result = scanner._SerieScanner__read_data_from_file("")
assert result is None
with tempfile.TemporaryDirectory() as tmpdir:
anime_folder = os.path.join(tmpdir, "SomeAnime")
os.makedirs(anime_folder)
# Create a data file
serie = Serie("test-key", "Test", "aniworld.to", "SomeAnime", {})
data_path = os.path.join(anime_folder, "data")
serie.save_to_file(data_path)
scanner = SerieScanner(tmpdir, mock_loader)
result = scanner._SerieScanner__read_data_from_file("SomeAnime")
assert result is not None
assert result.key == "test-key"
def test_no_files_returns_serie_with_generated_key(self, mock_loader):
"""Should return Serie with generated key when no key or data file exists."""
import tempfile
with tempfile.TemporaryDirectory() as tmpdir:
anime_folder = os.path.join(tmpdir, "Empty")
os.makedirs(anime_folder)
scanner = SerieScanner(tmpdir, mock_loader)
result = scanner._SerieScanner__read_data_from_file("Empty")
# Step 5 (was Step 4) generates key from folder name when no files exist
assert result is not None
assert isinstance(result, Serie)
assert result.key == "empty"
def test_scan_key_override_used_instead_of_generated(self, mock_loader):
"""Should use override key when folder name matches override dict."""
import tempfile
with tempfile.TemporaryDirectory() as tmpdir:
anime_folder = os.path.join(tmpdir, "Anyway, I'm Falling in Love with You (2025)")
os.makedirs(anime_folder)
overrides = {
"Anyway, I'm Falling in Love with You (2025)": "anyway-im-falling-in-love-with-you-2025"
}
scanner = SerieScanner(tmpdir, mock_loader, scan_key_overrides=overrides)
result = scanner._SerieScanner__read_data_from_file(
"Anyway, I'm Falling in Love with You (2025)"
)
# Override key should be used instead of generated key
assert result is not None
assert isinstance(result, Serie)
assert result.key == "anyway-im-falling-in-love-with-you-2025"
class TestReinit:
def test_nonexistent_folder_no_exception(self, temp_directory, mock_loader):
"""Folder doesn't exist -> returns None without raising."""
scanner = SerieScanner(temp_directory, mock_loader)
result = scanner._SerieScanner__read_data_from_file("Nonexistent Folder")
assert result is None
"""Test reinit method."""
def test_clears_keydict(self, temp_directory, mock_loader):
@@ -640,12 +600,10 @@ class TestScanProgressEvents:
call_data = completion_handler.call_args[0][0]
assert call_data["success"] is True
def test_scan_emits_error_on_no_key(
def test_scan_emits_error(
self, temp_directory, mock_loader
):
"""Should emit on_error when NoKeyFoundException occurs."""
from src.core.exceptions.Exceptions import NoKeyFoundException
"""Should emit on_error when an exception occurs."""
scanner = SerieScanner(temp_directory, mock_loader)
error_handler = MagicMock()
scanner.subscribe_on_error(error_handler)
@@ -657,7 +615,7 @@ class TestScanProgressEvents:
), \
patch.object(
scanner, '_SerieScanner__read_data_from_file',
side_effect=NoKeyFoundException("no key"),
side_effect=RuntimeError("DB error"),
):
scanner.scan()
@@ -666,186 +624,4 @@ class TestScanProgressEvents:
assert call_data["recoverable"] is True
class TestDbLookupFallback:
"""Tests for the db_lookup callback in SerieScanner."""
def _make_scanner(self, tmp_dir, mock_loader, db_lookup=None):
"""Create a scanner with an optional db_lookup."""
# Create a folder with an mp4 but NO key/data file
folder = os.path.join(tmp_dir, "Rooster Fighter (2026)")
os.makedirs(folder, exist_ok=True)
mp4 = os.path.join(folder, "Rooster Fighter - S01E001 - (German Dub).mp4")
with open(mp4, "w") as f:
f.write("dummy")
return SerieScanner(tmp_dir, mock_loader, db_lookup=db_lookup)
def test_db_lookup_stored_on_init(self, temp_directory, mock_loader):
"""db_lookup callable should be stored as _db_lookup."""
lookup = MagicMock(return_value=None)
scanner = SerieScanner(temp_directory, mock_loader, db_lookup=lookup)
assert scanner._db_lookup is lookup
def test_no_db_lookup_defaults_to_none(self, temp_directory, mock_loader):
"""Without db_lookup, _db_lookup should be None."""
scanner = SerieScanner(temp_directory, mock_loader)
assert scanner._db_lookup is None
def test_db_lookup_called_when_no_files(self, mock_loader):
"""db_lookup is called when neither key nor data file exists."""
import tempfile
with tempfile.TemporaryDirectory() as tmp_dir:
lookup = MagicMock(return_value=None)
scanner = self._make_scanner(tmp_dir, mock_loader, db_lookup=lookup)
with patch.object(scanner, 'get_total_to_scan', return_value=1), \
patch.object(
scanner,
'_SerieScanner__get_missing_episodes_and_season',
return_value=({}, "aniworld.to"),
):
scanner.scan()
lookup.assert_called_once_with("Rooster Fighter (2026)")
def test_db_lookup_not_called_when_key_file_exists(self, mock_loader):
"""db_lookup is NOT called when a key file is present."""
import tempfile
with tempfile.TemporaryDirectory() as tmp_dir:
folder = os.path.join(tmp_dir, "Rooster Fighter (2026)")
os.makedirs(folder, exist_ok=True)
mp4 = os.path.join(folder, "S01E001.mp4")
with open(mp4, "w") as f:
f.write("dummy")
with open(os.path.join(folder, "key"), "w") as f:
f.write("rooster-fighter")
lookup = MagicMock(return_value=None)
scanner = SerieScanner(tmp_dir, mock_loader, db_lookup=lookup)
with patch.object(scanner, 'get_total_to_scan', return_value=1), \
patch.object(
scanner,
'_SerieScanner__get_missing_episodes_and_season',
return_value=({1: []}, "aniworld.to"),
), \
patch.object(
SerieScanner,
'_SerieScanner__read_data_from_file',
return_value=Serie(
key="rooster-fighter", name="", site="aniworld.to",
folder="Rooster Fighter (2026)", episodeDict={},
),
):
scanner.scan()
lookup.assert_not_called()
def test_db_lookup_resolves_serie_and_scans(self, mock_loader):
"""When db_lookup returns a Serie, scanning continues normally."""
import tempfile
with tempfile.TemporaryDirectory() as tmp_dir:
resolved = Serie(
key="rooster-fighter",
name="Rooster Fighter",
site="aniworld.to",
folder="Rooster Fighter (2026)",
episodeDict={},
year=2026,
)
lookup = MagicMock(return_value=resolved)
scanner = self._make_scanner(tmp_dir, mock_loader, db_lookup=lookup)
with patch.object(scanner, 'get_total_to_scan', return_value=1), \
patch.object(
scanner,
'_SerieScanner__get_missing_episodes_and_season',
return_value=({1: [1, 2, 3]}, "aniworld.to"),
), \
patch.object(resolved, 'save_to_file'):
scanner.scan()
assert "rooster-fighter" in scanner.keyDict
assert scanner.keyDict["rooster-fighter"].episodeDict == {1: [1, 2, 3]}
def test_db_lookup_returns_none_folder_skipped(self, mock_loader):
"""When db_lookup returns None, Step 4 fallback generates key from folder name."""
import tempfile
with tempfile.TemporaryDirectory() as tmp_dir:
lookup = MagicMock(return_value=None)
scanner = self._make_scanner(tmp_dir, mock_loader, db_lookup=lookup)
with patch.object(scanner, 'get_total_to_scan', return_value=1):
scanner.scan()
# Step 4 generates key from folder name, so keyDict is not empty
assert len(scanner.keyDict) == 1
def test_db_lookup_exception_skips_folder(self, mock_loader):
"""When db_lookup raises, Step 4 fallback generates key from folder name."""
import tempfile
with tempfile.TemporaryDirectory() as tmp_dir:
lookup = MagicMock(side_effect=RuntimeError("DB offline"))
scanner = self._make_scanner(tmp_dir, mock_loader, db_lookup=lookup)
with patch.object(scanner, 'get_total_to_scan', return_value=1):
scanner.scan() # should not raise
# Step 4 generates key from folder name, so keyDict is not empty
assert len(scanner.keyDict) == 1
def test_db_lookup_warning_logged_when_no_files(
self, mock_loader, caplog
):
"""A warning is logged for folders without key/data file."""
import tempfile
with tempfile.TemporaryDirectory() as tmp_dir:
scanner = self._make_scanner(tmp_dir, mock_loader, db_lookup=None)
with caplog.at_level(logging.WARNING, logger="src.core.SerieScanner"):
with patch.object(scanner, 'get_total_to_scan', return_value=1):
scanner.scan()
assert any(
"Rooster Fighter (2026)" in record.message
for record in caplog.records
if record.levelname == "WARNING"
)
def test_db_lookup_info_logged_on_resolution(
self, mock_loader, caplog
):
"""An INFO log is emitted when db_lookup resolves a folder."""
import tempfile
with tempfile.TemporaryDirectory() as tmp_dir:
resolved = Serie(
key="rooster-fighter",
name="",
site="aniworld.to",
folder="Rooster Fighter (2026)",
episodeDict={},
)
lookup = MagicMock(return_value=resolved)
scanner = self._make_scanner(tmp_dir, mock_loader, db_lookup=lookup)
with caplog.at_level(logging.INFO, logger="src.core.SerieScanner"), \
patch.object(scanner, 'get_total_to_scan', return_value=1), \
patch.object(
scanner,
'_SerieScanner__get_missing_episodes_and_season',
return_value=({}, "aniworld.to"),
), \
patch.object(resolved, 'save_to_file'):
scanner.scan()
assert any(
"rooster-fighter" in record.message
for record in caplog.records
if record.levelname == "INFO"
)

View File

@@ -7,8 +7,8 @@ from unittest.mock import MagicMock, patch
import pytest
from src.core.entities.series import Serie
from src.core.SerieScanner import SerieScanner
from src.server.database.models import AnimeSeries
from src.server.SerieScanner import SerieScanner
@pytest.fixture
@@ -51,7 +51,7 @@ class TestGetSerieFromFolderDbLookup:
mock_anime_series.episodes = []
mock_session.execute.return_value.scalar_one_or_none.return_value = mock_anime_series
with patch("src.core.SerieScanner.get_sync_session", return_value=mock_session):
with patch("src.server.SerieScanner.get_sync_session", return_value=mock_session):
scanner = SerieScanner(temp_directory, mock_loader)
result = scanner._SerieScanner__read_data_from_file("Rooster Fighter (2026)")
@@ -60,48 +60,30 @@ class TestGetSerieFromFolderDbLookup:
assert result.name == "Rooster Fighter"
assert result.year == 2026
def test_db_miss_falls_back_to_provider_callback(self, temp_directory, mock_loader):
"""DB miss -> _db_lookup callback called."""
lookup = MagicMock(return_value=Serie(
key="rooster-fighter",
name="Rooster Fighter",
site="aniworld.to",
folder="Rooster Fighter (2026)",
episodeDict={},
))
scanner = SerieScanner(temp_directory, mock_loader, db_lookup=lookup)
def test_db_miss_returns_none(self, temp_directory, mock_loader):
"""DB miss -> returns None (no fallback)."""
mock_session = MagicMock()
mock_session.execute.return_value.scalar_one_or_none.return_value = None
result = scanner._SerieScanner__read_data_from_file("Rooster Fighter (2026)")
with patch("src.server.SerieScanner.get_sync_session", return_value=mock_session):
scanner = SerieScanner(temp_directory, mock_loader)
result = scanner._SerieScanner__read_data_from_file("Unknown Series (2026)")
assert result is not None
assert result.key == "rooster-fighter"
lookup.assert_called_once_with("Rooster Fighter (2026)")
def test_no_db_no_callback_generates_key_from_folder_name(self, temp_directory, mock_loader):
"""No DB entry, no callback -> key generated from folder name."""
folder = os.path.join(temp_directory, "Legacy Series")
os.makedirs(folder, exist_ok=True)
# No key file, no data file - should fall through to Step 4 (key generation)
scanner = SerieScanner(temp_directory, mock_loader)
result = scanner._SerieScanner__read_data_from_file("Legacy Series")
assert result is not None
assert result.key == "legacy-series"
assert result.folder == "Legacy Series"
assert result is None
def test_db_lookup_exception_caught_and_logged(self, temp_directory, mock_loader):
"""DB exception -> fallback to provider callback."""
def bad_lookup(folder):
raise RuntimeError("DB connection failed")
"""DB exception -> returns None without raising."""
with patch(
"src.server.SerieScanner.get_sync_session",
side_effect=RuntimeError("DB connection failed")
):
scanner = SerieScanner(temp_directory, mock_loader)
scanner = SerieScanner(temp_directory, mock_loader, db_lookup=bad_lookup)
with patch.object(logging.getLogger("src.core.SerieScanner"), "warning") as mock_warning:
result = scanner._SerieScanner__read_data_from_file("Rooster Fighter (2026)")
mock_warning.assert_called()
assert any("DB lookup failed" in str(c) for c in mock_warning.call_args_list)
with patch.object(logging.getLogger("src.server.SerieScanner"), "warning") as mock_warning:
result = scanner._SerieScanner__read_data_from_file("Rooster Fighter (2026)")
mock_warning.assert_called()
assert any("DB lookup failed" in str(c) for c in mock_warning.call_args_list)
assert result is None
class TestGetSerieFromFolderEdgeCases:

View File

@@ -4,8 +4,8 @@ from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from src.core.entities.series import Serie
from src.core.SerieScanner import SerieScanner
from src.server.database.models import AnimeSeries
from src.server.SerieScanner import SerieScanner
@pytest.fixture
@@ -18,15 +18,15 @@ def mock_session_factory():
@pytest.fixture
def sample_serie():
"""Create a sample Serie for testing."""
return Serie(
key="attack-on-titan",
name="Attack on Titan",
site="aniworld.to",
folder="Attack on Titan (2013)",
episodeDict={1: [1, 2, 3], 2: [1, 2]},
year=2013
)
"""Create a sample AnimeSeries mock for testing."""
anime = MagicMock(spec=AnimeSeries)
anime.key = "attack-on-titan"
anime.name = "Attack on Titan"
anime.site = "aniworld.to"
anime.folder = "Attack on Titan (2013)"
anime.year = 2013
anime.episodeDict = {1: [1, 2, 3], 2: [1, 2]}
return anime
class TestPersistSerieToDb:

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]

View File

@@ -6,7 +6,7 @@ import aiohttp
import pytest
from aiohttp import ClientResponseError, ClientSession
from src.core.services.tmdb_client import TMDBAPIError, TMDBClient
from src.server.services_nfo_temp.tmdb_client import TMDBAPIError, TMDBClient
@pytest.fixture

View File

@@ -9,7 +9,7 @@ from unittest.mock import AsyncMock, MagicMock, patch
import aiohttp
import pytest
from src.core.services.tmdb_client import TMDBAPIError, TMDBClient
from src.server.services_nfo_temp.tmdb_client import TMDBAPIError, TMDBClient
def _make_ctx(response):