Files
Aniworld/tests/unit/test_series_manager_service.py
Lukas 5b3fbf36b9 Task 4: Add Services & Utilities tests (66 tests)
- 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)
2026-02-15 17:49:11 +01:00

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