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)
This commit is contained in:
2026-02-07 18:23:02 +01:00
parent 46dab1dbc1
commit 5b3fbf36b9
5 changed files with 774 additions and 0 deletions

View File

@@ -0,0 +1,110 @@
"""Unit tests for error controller module.
Tests custom 404 and 500 error handlers for both API and HTML responses.
"""
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from fastapi import HTTPException, Request
from src.server.controllers.error_controller import (
not_found_handler,
server_error_handler,
)
def _make_request(path: str = "/") -> MagicMock:
"""Create a mock Request object with a given path."""
request = MagicMock(spec=Request)
url = MagicMock()
url.path = path
request.url = url
return request
class TestNotFoundHandler:
"""Tests for the 404 not_found_handler."""
@pytest.mark.asyncio
async def test_api_path_returns_json(self):
"""API paths get a JSON response with 404 status."""
request = _make_request("/api/anime/123")
exc = HTTPException(status_code=404)
resp = await not_found_handler(request, exc)
assert resp.status_code == 404
assert resp.body is not None
@pytest.mark.asyncio
@patch("src.server.controllers.error_controller.render_template")
async def test_web_path_renders_template(self, mock_render):
"""Non-API paths render the error template."""
mock_render.return_value = MagicMock(status_code=404)
request = _make_request("/anime/details")
exc = HTTPException(status_code=404)
await not_found_handler(request, exc)
mock_render.assert_called_once_with(
"error.html",
request,
context={"error": "Page not found", "status_code": 404},
title="404 - Not Found",
)
@pytest.mark.asyncio
async def test_api_json_structure(self):
"""API 404 response has 'detail' field."""
import json
request = _make_request("/api/missing")
exc = HTTPException(status_code=404)
resp = await not_found_handler(request, exc)
body = json.loads(resp.body)
assert body["detail"] == "API endpoint not found"
class TestServerErrorHandler:
"""Tests for the 500 server_error_handler."""
@pytest.mark.asyncio
async def test_api_path_returns_json(self):
"""API paths get a JSON response with 500 status."""
request = _make_request("/api/download")
exc = RuntimeError("crash")
resp = await server_error_handler(request, exc)
assert resp.status_code == 500
@pytest.mark.asyncio
@patch("src.server.controllers.error_controller.render_template")
async def test_web_path_renders_template(self, mock_render):
"""Non-API paths render the error template."""
mock_render.return_value = MagicMock(status_code=500)
request = _make_request("/settings")
exc = RuntimeError("crash")
await server_error_handler(request, exc)
mock_render.assert_called_once_with(
"error.html",
request,
context={"error": "Internal server error", "status_code": 500},
title="500 - Server Error",
)
@pytest.mark.asyncio
async def test_api_error_does_not_expose_stack_trace(self):
"""API 500 response doesn't contain the actual error message."""
import json
request = _make_request("/api/vulnerable")
exc = RuntimeError("secret database credentials")
resp = await server_error_handler(request, exc)
body = json.loads(resp.body)
assert "secret" not in body["detail"]
assert body["detail"] == "Internal server error"
@pytest.mark.asyncio
async def test_api_path_detection(self):
"""Correctly distinguishes API vs web paths."""
api_request = _make_request("/api/test")
web_request = _make_request("/dashboard")
# API path returns JSONResponse
from fastapi.responses import JSONResponse
resp = await server_error_handler(api_request, Exception("err"))
assert isinstance(resp, JSONResponse)

View File

@@ -0,0 +1,236 @@
"""Unit tests for media file utilities.
Tests media file checking, path resolution, video file counting,
and media file existence validation.
"""
from pathlib import Path
import pytest
from src.server.utils.media import (
FANART_FILENAME,
LOGO_FILENAME,
NFO_FILENAME,
POSTER_FILENAME,
VIDEO_EXTENSIONS,
check_media_files,
count_video_files,
get_media_file_paths,
has_all_images,
has_video_files,
)
class TestCheckMediaFiles:
"""Tests for check_media_files function."""
def test_all_files_exist(self, tmp_path: Path):
"""Returns True for all file types when they exist."""
(tmp_path / POSTER_FILENAME).touch()
(tmp_path / LOGO_FILENAME).touch()
(tmp_path / FANART_FILENAME).touch()
(tmp_path / NFO_FILENAME).touch()
result = check_media_files(tmp_path)
assert result == {
"poster": True, "logo": True,
"fanart": True, "nfo": True,
}
def test_no_files_exist(self, tmp_path: Path):
"""Returns False for all when no media files exist."""
result = check_media_files(tmp_path)
assert all(v is False for v in result.values())
def test_partial_files(self, tmp_path: Path):
"""Returns mixed results when some files exist."""
(tmp_path / POSTER_FILENAME).touch()
result = check_media_files(tmp_path)
assert result["poster"] is True
assert result["logo"] is False
def test_accepts_string_path(self, tmp_path: Path):
"""Works with string path arguments."""
(tmp_path / POSTER_FILENAME).touch()
result = check_media_files(str(tmp_path))
assert result["poster"] is True
def test_selective_checks(self, tmp_path: Path):
"""Only checks requested file types."""
result = check_media_files(
tmp_path, check_poster=True, check_logo=False,
check_fanart=False, check_nfo=False,
)
assert "poster" in result
assert "logo" not in result
assert "fanart" not in result
assert "nfo" not in result
class TestGetMediaFilePaths:
"""Tests for get_media_file_paths function."""
def test_returns_paths_for_existing_files(self, tmp_path: Path):
"""Returns Path objects for files that exist."""
(tmp_path / POSTER_FILENAME).touch()
result = get_media_file_paths(tmp_path)
assert result["poster"] == tmp_path / POSTER_FILENAME
def test_returns_none_for_missing_files(self, tmp_path: Path):
"""Returns None for files that don't exist."""
result = get_media_file_paths(tmp_path)
assert result["poster"] is None
assert result["logo"] is None
def test_selective_includes(self, tmp_path: Path):
"""Only includes requested file types."""
result = get_media_file_paths(
tmp_path, include_poster=True, include_logo=False,
include_fanart=False, include_nfo=False,
)
assert "poster" in result
assert "logo" not in result
def test_accepts_string_path(self, tmp_path: Path):
"""Works with string path arguments."""
(tmp_path / NFO_FILENAME).touch()
result = get_media_file_paths(str(tmp_path))
assert result["nfo"] is not None
class TestHasAllImages:
"""Tests for has_all_images function."""
def test_true_when_all_images_present(self, tmp_path: Path):
"""Returns True when poster, logo, and fanart all exist."""
(tmp_path / POSTER_FILENAME).touch()
(tmp_path / LOGO_FILENAME).touch()
(tmp_path / FANART_FILENAME).touch()
assert has_all_images(tmp_path) is True
def test_false_when_missing_one(self, tmp_path: Path):
"""Returns False when any image is missing."""
(tmp_path / POSTER_FILENAME).touch()
(tmp_path / LOGO_FILENAME).touch()
# fanart missing
assert has_all_images(tmp_path) is False
def test_false_when_no_images(self, tmp_path: Path):
"""Returns False when no images exist."""
assert has_all_images(tmp_path) is False
def test_nfo_not_required(self, tmp_path: Path):
"""NFO file presence doesn't affect image check."""
(tmp_path / POSTER_FILENAME).touch()
(tmp_path / LOGO_FILENAME).touch()
(tmp_path / FANART_FILENAME).touch()
# NFO is not an image - shouldn't matter
assert has_all_images(tmp_path) is True
def test_accepts_string_path(self, tmp_path: Path):
"""Works with string path arguments."""
assert has_all_images(str(tmp_path)) is False
class TestCountVideoFiles:
"""Tests for count_video_files function."""
def test_counts_mp4_files(self, tmp_path: Path):
"""Counts .mp4 video files."""
(tmp_path / "ep01.mp4").touch()
(tmp_path / "ep02.mp4").touch()
assert count_video_files(tmp_path) == 2
def test_counts_multiple_extensions(self, tmp_path: Path):
"""Counts video files with various extensions."""
(tmp_path / "ep01.mp4").touch()
(tmp_path / "ep02.mkv").touch()
(tmp_path / "ep03.avi").touch()
assert count_video_files(tmp_path) == 3
def test_ignores_non_video_files(self, tmp_path: Path):
"""Non-video files are not counted."""
(tmp_path / "ep01.mp4").touch()
(tmp_path / "poster.jpg").touch()
(tmp_path / "readme.txt").touch()
assert count_video_files(tmp_path) == 1
def test_recursive_counts_subdirectories(self, tmp_path: Path):
"""Recursive mode counts videos in subdirectories."""
s1 = tmp_path / "Season 1"
s1.mkdir()
(s1 / "ep01.mp4").touch()
(s1 / "ep02.mp4").touch()
s2 = tmp_path / "Season 2"
s2.mkdir()
(s2 / "ep01.mkv").touch()
assert count_video_files(tmp_path, recursive=True) == 3
def test_non_recursive_skips_subdirectories(self, tmp_path: Path):
"""Non-recursive mode only counts root-level files."""
(tmp_path / "ep01.mp4").touch()
sub = tmp_path / "Season 1"
sub.mkdir()
(sub / "ep02.mp4").touch()
assert count_video_files(tmp_path, recursive=False) == 1
def test_nonexistent_folder_returns_zero(self):
"""Returns 0 for nonexistent folder."""
assert count_video_files("/nonexistent/path") == 0
def test_empty_folder_returns_zero(self, tmp_path: Path):
"""Returns 0 for empty folder."""
assert count_video_files(tmp_path) == 0
def test_case_insensitive_extensions(self, tmp_path: Path):
"""Video extensions are matched case-insensitively."""
(tmp_path / "ep01.MP4").touch()
(tmp_path / "ep02.Mkv").touch()
assert count_video_files(tmp_path) == 2
class TestHasVideoFiles:
"""Tests for has_video_files function."""
def test_true_when_videos_exist(self, tmp_path: Path):
"""Returns True when video files are present."""
(tmp_path / "ep01.mp4").touch()
assert has_video_files(tmp_path) is True
def test_false_when_no_videos(self, tmp_path: Path):
"""Returns False when no video files exist."""
(tmp_path / "readme.txt").touch()
assert has_video_files(tmp_path) is False
def test_false_for_nonexistent_folder(self):
"""Returns False for nonexistent folder."""
assert has_video_files("/nonexistent/path") is False
def test_finds_videos_in_subdirectories(self, tmp_path: Path):
"""Detects videos in nested subdirectories."""
sub = tmp_path / "Season 1"
sub.mkdir()
(sub / "ep01.mkv").touch()
assert has_video_files(tmp_path) is True
def test_all_supported_extensions(self, tmp_path: Path):
"""All supported video extensions are recognized."""
for ext in VIDEO_EXTENSIONS:
(tmp_path / f"test{ext}").touch()
assert has_video_files(tmp_path) is True
class TestConstants:
"""Tests for module constants."""
def test_standard_filenames(self):
"""Standard media filenames match Kodi/Plex conventions."""
assert POSTER_FILENAME == "poster.jpg"
assert LOGO_FILENAME == "logo.png"
assert FANART_FILENAME == "fanart.jpg"
assert NFO_FILENAME == "tvshow.nfo"
def test_video_extensions_includes_common_formats(self):
"""VIDEO_EXTENSIONS includes all common video formats."""
for ext in [".mp4", ".mkv", ".avi", ".webm"]:
assert ext in VIDEO_EXTENSIONS

View File

@@ -0,0 +1,189 @@
"""Unit tests for NFO service factory module.
Tests factory instantiation, configuration precedence, singleton pattern,
and convenience functions for creating NFOService instances.
"""
from unittest.mock import MagicMock, patch
import pytest
from src.core.services.nfo_factory import (
NFOServiceFactory,
create_nfo_service,
get_nfo_factory,
)
class TestNFOServiceFactoryCreate:
"""Tests for NFOServiceFactory.create method."""
@patch("src.core.services.nfo_factory.NFOService")
@patch("src.core.services.nfo_factory.settings")
def test_create_with_explicit_api_key(self, mock_settings, mock_nfo_cls):
"""Explicit API key takes priority over settings."""
mock_settings.tmdb_api_key = "settings_key"
mock_settings.anime_directory = "/anime"
mock_settings.nfo_image_size = "original"
mock_settings.nfo_auto_create = False
factory = NFOServiceFactory()
factory.create(tmdb_api_key="explicit_key")
mock_nfo_cls.assert_called_once_with(
tmdb_api_key="explicit_key",
anime_directory="/anime",
image_size="original",
auto_create=False,
)
@patch("src.core.services.nfo_factory.NFOService")
@patch("src.core.services.nfo_factory.settings")
def test_create_falls_back_to_settings(self, mock_settings, mock_nfo_cls):
"""Falls back to settings when no explicit key provided."""
mock_settings.tmdb_api_key = "settings_key"
mock_settings.anime_directory = "/anime"
mock_settings.nfo_image_size = "w500"
mock_settings.nfo_auto_create = True
factory = NFOServiceFactory()
factory.create()
mock_nfo_cls.assert_called_once_with(
tmdb_api_key="settings_key",
anime_directory="/anime",
image_size="w500",
auto_create=True,
)
@patch("src.core.services.nfo_factory.settings")
def test_create_raises_without_api_key(self, mock_settings):
"""Raises ValueError when no API key available from any source."""
mock_settings.tmdb_api_key = None
factory = NFOServiceFactory()
factory._get_api_key_from_config = MagicMock(return_value=None)
with pytest.raises(ValueError, match="TMDB API key not configured"):
factory.create()
@patch("src.core.services.nfo_factory.NFOService")
@patch("src.core.services.nfo_factory.settings")
def test_create_with_all_custom_params(self, mock_settings, mock_nfo_cls):
"""All parameters can be overridden."""
mock_settings.tmdb_api_key = "default"
mock_settings.anime_directory = "/default"
mock_settings.nfo_image_size = "original"
mock_settings.nfo_auto_create = False
factory = NFOServiceFactory()
factory.create(
tmdb_api_key="custom",
anime_directory="/custom",
image_size="w300",
auto_create=True,
)
mock_nfo_cls.assert_called_once_with(
tmdb_api_key="custom",
anime_directory="/custom",
image_size="w300",
auto_create=True,
)
@patch("src.core.services.nfo_factory.NFOService")
@patch("src.core.services.nfo_factory.settings")
def test_create_uses_config_json_fallback(self, mock_settings, mock_nfo_cls):
"""Falls back to config.json when settings has no key."""
mock_settings.tmdb_api_key = None
mock_settings.anime_directory = "/anime"
mock_settings.nfo_image_size = "original"
mock_settings.nfo_auto_create = False
factory = NFOServiceFactory()
factory._get_api_key_from_config = MagicMock(return_value="config_key")
factory.create()
mock_nfo_cls.assert_called_once()
call_kwargs = mock_nfo_cls.call_args[1]
assert call_kwargs["tmdb_api_key"] == "config_key"
class TestNFOServiceFactoryCreateOptional:
"""Tests for NFOServiceFactory.create_optional method."""
@patch("src.core.services.nfo_factory.settings")
def test_returns_none_without_api_key(self, mock_settings):
"""Returns None instead of raising when no API key."""
mock_settings.tmdb_api_key = None
factory = NFOServiceFactory()
factory._get_api_key_from_config = MagicMock(return_value=None)
result = factory.create_optional()
assert result is None
@patch("src.core.services.nfo_factory.NFOService")
@patch("src.core.services.nfo_factory.settings")
def test_returns_service_when_configured(self, mock_settings, mock_nfo_cls):
"""Returns NFOService when configuration is available."""
mock_settings.tmdb_api_key = "key123"
mock_settings.anime_directory = "/anime"
mock_settings.nfo_image_size = "original"
mock_settings.nfo_auto_create = False
factory = NFOServiceFactory()
result = factory.create_optional()
assert result is not None
class TestGetNfoFactory:
"""Tests for get_nfo_factory singleton function."""
def test_returns_factory_instance(self):
"""Returns an NFOServiceFactory instance."""
import src.core.services.nfo_factory as mod
old = mod._factory_instance
try:
mod._factory_instance = None
factory = get_nfo_factory()
assert isinstance(factory, NFOServiceFactory)
finally:
mod._factory_instance = old
def test_returns_same_instance(self):
"""Repeated calls return the same singleton."""
import src.core.services.nfo_factory as mod
old = mod._factory_instance
try:
mod._factory_instance = None
f1 = get_nfo_factory()
f2 = get_nfo_factory()
assert f1 is f2
finally:
mod._factory_instance = old
class TestCreateNfoService:
"""Tests for create_nfo_service convenience function."""
@patch("src.core.services.nfo_factory.NFOService")
@patch("src.core.services.nfo_factory.settings")
def test_convenience_function_creates_service(
self, mock_settings, mock_nfo_cls
):
"""Convenience function delegates to factory.create()."""
mock_settings.tmdb_api_key = "key"
mock_settings.anime_directory = "/anime"
mock_settings.nfo_image_size = "original"
mock_settings.nfo_auto_create = False
result = create_nfo_service()
mock_nfo_cls.assert_called_once()
@patch("src.core.services.nfo_factory.settings")
def test_convenience_function_raises_without_key(self, mock_settings):
"""Convenience function raises ValueError without key."""
mock_settings.tmdb_api_key = None
import src.core.services.nfo_factory as mod
old = mod._factory_instance
try:
mod._factory_instance = None
factory = get_nfo_factory()
factory._get_api_key_from_config = MagicMock(return_value=None)
with pytest.raises(ValueError):
factory.create()
finally:
mod._factory_instance = old

View File

@@ -0,0 +1,209 @@
"""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

View File

@@ -0,0 +1,30 @@
"""Unit tests for template utilities and configuration.
Tests the templates module constants and the TEMPLATES_DIR path resolution.
"""
from pathlib import Path
import pytest
from src.server.utils.templates import TEMPLATES_DIR
class TestTemplatesConfig:
"""Tests for templates module configuration."""
def test_templates_dir_is_path(self):
"""TEMPLATES_DIR is a Path object."""
assert isinstance(TEMPLATES_DIR, Path)
def test_templates_dir_ends_with_templates(self):
"""TEMPLATES_DIR path ends with 'templates'."""
assert TEMPLATES_DIR.name == "templates"
def test_templates_dir_parent_is_web(self):
"""TEMPLATES_DIR parent is 'web' directory."""
assert TEMPLATES_DIR.parent.name == "web"
def test_templates_dir_grandparent_is_server(self):
"""TEMPLATES_DIR grandparent is 'server' directory."""
assert TEMPLATES_DIR.parent.parent.name == "server"