Files
Aniworld/tests/unit/test_media_utils.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

237 lines
8.3 KiB
Python

"""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