47baee854b
Test coverage: - test_utils.py: formatting helpers (human_size, ticks_to_minutes, format_duration, format_bitrate, ffprobe summaries, stream parsing) - test_path_utils.py: path resolution (prefix, media root mapping, edge cases with spaces/special chars) - test_domain_media.py: Jellyfin item normalization (HDR detection, media sources, stream extraction, display formatting) - test_jobs.py: job template rendering and shell quoting safety - test_media_index.py: SQLite index CRUD, querying, filtering, sorting, pagination - test_config.py: pydantic-settings env loading - test_api.py: full FastAPI integration tests with mocked SSH/Jellyfin (all endpoints: dashboard, monitoring, media, files, jobs) All tests run without network/SSH dependencies using mocks.
247 lines
7.1 KiB
Python
247 lines
7.1 KiB
Python
"""Unit tests for utils.py formatting helpers."""
|
|
|
|
import pytest
|
|
from media_library_viewer_api.utils import (
|
|
ticks_to_minutes,
|
|
human_size,
|
|
is_known_video_file,
|
|
format_duration,
|
|
format_bitrate,
|
|
ffprobe_format_summary,
|
|
summarize_video_streams,
|
|
summarize_audio_streams,
|
|
summarize_subtitle_streams,
|
|
summarize_streams,
|
|
timestamp_to_local,
|
|
)
|
|
|
|
|
|
class TestTicksToMinutes:
|
|
def test_none(self):
|
|
assert ticks_to_minutes(None) is None
|
|
|
|
def test_zero(self):
|
|
assert ticks_to_minutes(0) is None
|
|
|
|
def test_one_hour(self):
|
|
# 1 hour = 3600 * 10_000_000 ticks
|
|
assert ticks_to_minutes(3600 * 10_000_000) == 60
|
|
|
|
def test_90_minutes(self):
|
|
assert ticks_to_minutes(90 * 60 * 10_000_000) == 90
|
|
|
|
def test_rounding(self):
|
|
# 45.7 minutes
|
|
ticks = int(45.7 * 60 * 10_000_000)
|
|
assert ticks_to_minutes(ticks) == 46
|
|
|
|
|
|
class TestHumanSize:
|
|
def test_none(self):
|
|
assert human_size(None) == ""
|
|
|
|
def test_zero(self):
|
|
assert human_size(0) == "0 B"
|
|
|
|
def test_bytes(self):
|
|
assert human_size(500) == "500 B"
|
|
|
|
def test_kilobytes(self):
|
|
result = human_size(2048)
|
|
assert "KB" in result
|
|
assert "2.0" in result
|
|
|
|
def test_megabytes(self):
|
|
result = human_size(5 * 1024 * 1024)
|
|
assert "MB" in result
|
|
|
|
def test_gigabytes(self):
|
|
result = human_size(3 * 1024**3)
|
|
assert "GB" in result
|
|
|
|
def test_terabytes(self):
|
|
result = human_size(2 * 1024**4)
|
|
assert "TB" in result
|
|
|
|
|
|
class TestIsKnownVideoFile:
|
|
def test_none(self):
|
|
assert is_known_video_file(None) is False
|
|
|
|
def test_empty(self):
|
|
assert is_known_video_file("") is False
|
|
|
|
def test_mkv(self):
|
|
assert is_known_video_file("/media/movie.mkv") is True
|
|
|
|
def test_mp4(self):
|
|
assert is_known_video_file("file.MP4") is True
|
|
|
|
def test_txt(self):
|
|
assert is_known_video_file("/path/to/notes.txt") is False
|
|
|
|
def test_srt(self):
|
|
assert is_known_video_file("subtitle.srt") is False
|
|
|
|
def test_spaces_in_path(self):
|
|
assert is_known_video_file("/media/My Movie (2024)/file.mkv") is True
|
|
|
|
|
|
class TestFormatDuration:
|
|
def test_none(self):
|
|
assert format_duration(None) == ""
|
|
|
|
def test_empty(self):
|
|
assert format_duration("") == ""
|
|
|
|
def test_seconds(self):
|
|
assert format_duration(90) == "00:01:30"
|
|
|
|
def test_hours(self):
|
|
assert format_duration(7200) == "02:00:00"
|
|
|
|
def test_string_input(self):
|
|
assert format_duration("3661.5") == "01:01:01"
|
|
|
|
|
|
class TestFormatBitrate:
|
|
def test_none(self):
|
|
assert format_bitrate(None) == ""
|
|
|
|
def test_empty(self):
|
|
assert format_bitrate("") == ""
|
|
|
|
def test_low(self):
|
|
assert format_bitrate(500) == "500 bps"
|
|
|
|
def test_kbps(self):
|
|
result = format_bitrate(128000)
|
|
assert "128 kbps" in result
|
|
|
|
def test_mbps(self):
|
|
result = format_bitrate(5_000_000)
|
|
assert "Mbps" in result
|
|
|
|
def test_string_input(self):
|
|
result = format_bitrate("2500000")
|
|
assert "Mbps" in result
|
|
|
|
|
|
class TestFfprobeSummary:
|
|
def test_empty(self):
|
|
result = ffprobe_format_summary({})
|
|
assert result["filename"] == ""
|
|
assert result["format"] == ""
|
|
|
|
def test_with_format(self):
|
|
data = {
|
|
"format": {
|
|
"filename": "/path/to/file.mkv",
|
|
"format_name": "matroska,webm",
|
|
"format_long_name": "Matroska / WebM",
|
|
"duration": "7200.5",
|
|
"size": "5368709120",
|
|
"bit_rate": "5000000",
|
|
"nb_streams": 3,
|
|
}
|
|
}
|
|
result = ffprobe_format_summary(data)
|
|
assert result["filename"] == "/path/to/file.mkv"
|
|
assert result["format"] == "matroska,webm"
|
|
assert "02:00:00" in result["duration"]
|
|
assert "GB" in result["size"]
|
|
assert "Mbps" in result["bit_rate"]
|
|
assert result["stream_count"] == "3"
|
|
|
|
|
|
class TestSummarizeStreams:
|
|
SAMPLE_FFPROBE = {
|
|
"streams": [
|
|
{
|
|
"index": 0,
|
|
"codec_type": "video",
|
|
"codec_name": "hevc",
|
|
"profile": "Main 10",
|
|
"width": 3840,
|
|
"height": 2160,
|
|
"pix_fmt": "yuv420p10le",
|
|
"bit_rate": "15000000",
|
|
"avg_frame_rate": "24/1",
|
|
"color_range": "tv",
|
|
"color_space": "bt2020nc",
|
|
"color_transfer": "smpte2084",
|
|
"color_primaries": "bt2020",
|
|
"tags": {"language": "eng", "title": "Main"},
|
|
"disposition": {"default": 1},
|
|
"side_data_list": [{"side_data_type": "Mastering display metadata"}],
|
|
},
|
|
{
|
|
"index": 1,
|
|
"codec_type": "audio",
|
|
"codec_name": "truehd",
|
|
"profile": "TrueHD+Atmos",
|
|
"channels": 8,
|
|
"channel_layout": "7.1",
|
|
"sample_rate": "48000",
|
|
"bit_rate": "4500000",
|
|
"tags": {"language": "eng", "title": "Atmos"},
|
|
"disposition": {"default": 1, "forced": 0},
|
|
},
|
|
{
|
|
"index": 2,
|
|
"codec_type": "subtitle",
|
|
"codec_name": "subrip",
|
|
"codec_long_name": "SubRip subtitle",
|
|
"tags": {"language": "eng", "title": "English"},
|
|
"disposition": {"default": 0, "forced": 1, "hearing_impaired": 0},
|
|
},
|
|
]
|
|
}
|
|
|
|
def test_video_streams(self):
|
|
rows = summarize_video_streams(self.SAMPLE_FFPROBE)
|
|
assert len(rows) == 1
|
|
v = rows[0]
|
|
assert v["codec"] == "hevc"
|
|
assert v["resolution"] == "3840x2160"
|
|
assert v["color_transfer"] == "smpte2084"
|
|
assert v["language"] == "eng"
|
|
assert v["default"] == "yes"
|
|
assert "Mastering" in v["side_data"]
|
|
|
|
def test_audio_streams(self):
|
|
rows = summarize_audio_streams(self.SAMPLE_FFPROBE)
|
|
assert len(rows) == 1
|
|
a = rows[0]
|
|
assert a["codec"] == "truehd"
|
|
assert a["channels"] == 8
|
|
assert a["layout"] == "7.1"
|
|
assert a["default"] == "yes"
|
|
assert a["forced"] == ""
|
|
|
|
def test_subtitle_streams(self):
|
|
rows = summarize_subtitle_streams(self.SAMPLE_FFPROBE)
|
|
assert len(rows) == 1
|
|
s = rows[0]
|
|
assert s["codec"] == "subrip"
|
|
assert s["language"] == "eng"
|
|
assert s["forced"] == "yes"
|
|
assert s["default"] == ""
|
|
|
|
def test_summarize_all(self):
|
|
rows = summarize_streams(self.SAMPLE_FFPROBE)
|
|
assert len(rows) == 3
|
|
types = [r["type"] for r in rows]
|
|
assert types == ["video", "audio", "subtitle"]
|
|
|
|
|
|
class TestTimestampToLocal:
|
|
def test_none(self):
|
|
assert timestamp_to_local(None) == ""
|
|
|
|
def test_valid(self):
|
|
# Just verify it returns a non-empty formatted string
|
|
result = timestamp_to_local(1700000000.0)
|
|
assert result != ""
|
|
assert "-" in result # date format contains dashes
|