- 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)
237 lines
8.3 KiB
Python
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
|