- test_media_utils.py: 29 tests for check_media_files, get_media_file_paths, has_all_images, count_video_files, has_video_files, constants - test_nfo_factory.py: 11 tests for NFOServiceFactory.create, create_optional, get_nfo_factory singleton, create_nfo_service convenience - test_series_manager_service.py: 15 tests for init, from_settings, process_nfo_for_series, scan_and_process_nfo, close - test_templates_utils.py: 4 tests for TEMPLATES_DIR path resolution - test_error_controller.py: 7 tests for 404/500 handlers (API vs HTML)
210 lines
8.0 KiB
Python
210 lines
8.0 KiB
Python
"""Unit tests for series manager service.
|
|
|
|
Tests series orchestration, NFO processing, configuration handling,
|
|
and async batch processing.
|
|
"""
|
|
|
|
import asyncio
|
|
from pathlib import Path
|
|
from unittest.mock import AsyncMock, MagicMock, patch
|
|
|
|
import pytest
|
|
|
|
from src.core.services.series_manager_service import SeriesManagerService
|
|
|
|
|
|
class TestSeriesManagerServiceInit:
|
|
"""Tests for SeriesManagerService initialization."""
|
|
|
|
@patch("src.core.services.series_manager_service.SerieList")
|
|
def test_init_without_nfo_service(self, mock_serie_list):
|
|
"""Service initializes without NFO when no API key provided."""
|
|
svc = SeriesManagerService(
|
|
anime_directory="/anime",
|
|
tmdb_api_key=None,
|
|
auto_create_nfo=False,
|
|
)
|
|
assert svc.nfo_service is None
|
|
assert svc.anime_directory == "/anime"
|
|
|
|
@patch("src.core.services.series_manager_service.SerieList")
|
|
def test_init_with_nfo_disabled(self, mock_serie_list):
|
|
"""NFO service not created when auto_create and update both False."""
|
|
svc = SeriesManagerService(
|
|
anime_directory="/anime",
|
|
tmdb_api_key="key123",
|
|
auto_create_nfo=False,
|
|
update_on_scan=False,
|
|
)
|
|
assert svc.nfo_service is None
|
|
|
|
@patch("src.core.services.nfo_factory.get_nfo_factory")
|
|
@patch("src.core.services.series_manager_service.SerieList")
|
|
def test_init_creates_nfo_service_when_enabled(
|
|
self, mock_serie_list, mock_factory_fn
|
|
):
|
|
"""NFO service is created when auto_create is True and key exists."""
|
|
mock_factory = MagicMock()
|
|
mock_nfo = MagicMock()
|
|
mock_factory.create.return_value = mock_nfo
|
|
mock_factory_fn.return_value = mock_factory
|
|
|
|
svc = SeriesManagerService(
|
|
anime_directory="/anime",
|
|
tmdb_api_key="key123",
|
|
auto_create_nfo=True,
|
|
)
|
|
assert svc.nfo_service is mock_nfo
|
|
|
|
@patch("src.core.services.nfo_factory.get_nfo_factory")
|
|
@patch("src.core.services.series_manager_service.SerieList")
|
|
def test_init_handles_nfo_factory_error(
|
|
self, mock_serie_list, mock_factory_fn
|
|
):
|
|
"""NFO service set to None if factory raises."""
|
|
mock_factory_fn.side_effect = ValueError("bad config")
|
|
svc = SeriesManagerService(
|
|
anime_directory="/anime",
|
|
tmdb_api_key="key123",
|
|
auto_create_nfo=True,
|
|
)
|
|
assert svc.nfo_service is None
|
|
|
|
@patch("src.core.services.series_manager_service.SerieList")
|
|
def test_init_stores_config_flags(self, mock_serie_list):
|
|
"""Configuration flags are stored correctly."""
|
|
svc = SeriesManagerService(
|
|
anime_directory="/anime",
|
|
auto_create_nfo=True,
|
|
update_on_scan=True,
|
|
download_poster=False,
|
|
download_logo=False,
|
|
download_fanart=True,
|
|
)
|
|
assert svc.auto_create_nfo is True
|
|
assert svc.update_on_scan is True
|
|
assert svc.download_poster is False
|
|
assert svc.download_logo is False
|
|
assert svc.download_fanart is True
|
|
|
|
@patch("src.core.services.series_manager_service.SerieList")
|
|
def test_serie_list_created_with_skip_load(self, mock_serie_list):
|
|
"""SerieList is created with skip_load=True."""
|
|
SeriesManagerService(anime_directory="/anime")
|
|
mock_serie_list.assert_called_once_with("/anime", skip_load=True)
|
|
|
|
|
|
class TestFromSettings:
|
|
"""Tests for from_settings classmethod."""
|
|
|
|
@patch("src.core.services.series_manager_service.settings")
|
|
@patch("src.core.services.series_manager_service.SerieList")
|
|
def test_from_settings_uses_all_settings(self, mock_serie_list, mock_settings):
|
|
"""from_settings passes all relevant settings to constructor."""
|
|
mock_settings.anime_directory = "/anime"
|
|
mock_settings.tmdb_api_key = None
|
|
mock_settings.nfo_auto_create = False
|
|
mock_settings.nfo_update_on_scan = False
|
|
mock_settings.nfo_download_poster = True
|
|
mock_settings.nfo_download_logo = True
|
|
mock_settings.nfo_download_fanart = True
|
|
mock_settings.nfo_image_size = "original"
|
|
|
|
svc = SeriesManagerService.from_settings()
|
|
assert isinstance(svc, SeriesManagerService)
|
|
assert svc.anime_directory == "/anime"
|
|
|
|
|
|
class TestProcessNfoForSeries:
|
|
"""Tests for process_nfo_for_series method."""
|
|
|
|
@pytest.fixture
|
|
def service(self):
|
|
"""Create a service with mocked dependencies."""
|
|
with patch("src.core.services.series_manager_service.SerieList"):
|
|
svc = SeriesManagerService(anime_directory="/anime")
|
|
svc.nfo_service = AsyncMock()
|
|
svc.auto_create_nfo = True
|
|
return svc
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_returns_early_without_nfo_service(self):
|
|
"""Does nothing when nfo_service is None."""
|
|
with patch("src.core.services.series_manager_service.SerieList"):
|
|
svc = SeriesManagerService(anime_directory="/anime")
|
|
svc.nfo_service = None
|
|
# Should not raise
|
|
await svc.process_nfo_for_series("folder", "Name", "key")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_creates_nfo_when_not_exists(self, service):
|
|
"""Creates NFO file when it doesn't exist and auto_create is True."""
|
|
service.nfo_service.check_nfo_exists = AsyncMock(return_value=False)
|
|
service.nfo_service.create_tvshow_nfo = AsyncMock()
|
|
|
|
await service.process_nfo_for_series("folder", "Name", "key", year=2024)
|
|
service.nfo_service.create_tvshow_nfo.assert_awaited_once()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_skips_creation_when_exists(self, service):
|
|
"""Skips NFO creation when file already exists."""
|
|
service.nfo_service.check_nfo_exists = AsyncMock(return_value=True)
|
|
service.nfo_service.parse_nfo_ids = MagicMock(
|
|
return_value={"tmdb_id": None, "tvdb_id": None}
|
|
)
|
|
service.nfo_service.create_tvshow_nfo = AsyncMock()
|
|
|
|
await service.process_nfo_for_series("folder", "Name", "key")
|
|
service.nfo_service.create_tvshow_nfo.assert_not_awaited()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_handles_tmdb_api_error(self, service):
|
|
"""TMDBAPIError is caught and logged (not re-raised)."""
|
|
from src.core.services.tmdb_client import TMDBAPIError
|
|
service.nfo_service.check_nfo_exists = AsyncMock(
|
|
side_effect=TMDBAPIError("rate limited")
|
|
)
|
|
# Should not raise
|
|
await service.process_nfo_for_series("folder", "Name", "key")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_handles_unexpected_error(self, service):
|
|
"""Unexpected exceptions are caught and logged."""
|
|
service.nfo_service.check_nfo_exists = AsyncMock(
|
|
side_effect=RuntimeError("unexpected")
|
|
)
|
|
await service.process_nfo_for_series("folder", "Name", "key")
|
|
|
|
|
|
class TestScanAndProcessNfo:
|
|
"""Tests for scan_and_process_nfo method."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_skips_when_no_nfo_service(self):
|
|
"""Returns early when nfo_service is None."""
|
|
with patch("src.core.services.series_manager_service.SerieList"):
|
|
svc = SeriesManagerService(anime_directory="/anime")
|
|
svc.nfo_service = None
|
|
await svc.scan_and_process_nfo()
|
|
|
|
|
|
class TestClose:
|
|
"""Tests for close method."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_close_with_nfo_service(self):
|
|
"""Closes NFO service when present."""
|
|
with patch("src.core.services.series_manager_service.SerieList"):
|
|
svc = SeriesManagerService(anime_directory="/anime")
|
|
svc.nfo_service = AsyncMock()
|
|
await svc.close()
|
|
svc.nfo_service.close.assert_awaited_once()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_close_without_nfo_service(self):
|
|
"""Close works fine when no NFO service."""
|
|
with patch("src.core.services.series_manager_service.SerieList"):
|
|
svc = SeriesManagerService(anime_directory="/anime")
|
|
svc.nfo_service = None
|
|
await svc.close() # Should not raise
|