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.
245 lines
7.5 KiB
Python
245 lines
7.5 KiB
Python
"""Unit tests for the SQLite media index service."""
|
|
|
|
import pytest
|
|
import tempfile
|
|
from pathlib import Path
|
|
from media_library_viewer_api.services.media_index import MediaIndex
|
|
|
|
|
|
@pytest.fixture
|
|
def index(tmp_path):
|
|
"""Create a temporary media index for testing."""
|
|
db_path = tmp_path / "test_index.sqlite"
|
|
return MediaIndex(db_path)
|
|
|
|
|
|
@pytest.fixture
|
|
def populated_index(index):
|
|
"""Index with sample rows inserted."""
|
|
rows = [
|
|
{
|
|
"id": "m1",
|
|
"title": "The Matrix",
|
|
"series": "",
|
|
"season": "",
|
|
"season_number": None,
|
|
"episode": None,
|
|
"type": "Movie",
|
|
"year": 1999,
|
|
"runtime_ticks": 81600000000,
|
|
"runtime_min": 136,
|
|
"size_bytes": 15_000_000_000,
|
|
"bitrate_bps": 15_000_000,
|
|
"hdr": 0,
|
|
"video": "h264",
|
|
"width": 1920,
|
|
"height": 1080,
|
|
"resolution": "1920x1080",
|
|
"date_added": "2024-01-15",
|
|
"date_added_ts": 1705276800,
|
|
"path": "/media/movies/The Matrix (1999)/file.mkv",
|
|
"library_id": "lib1",
|
|
"library_name": "Movies",
|
|
"size": "15.0 GB",
|
|
"bitrate": "15.0 Mbps",
|
|
},
|
|
{
|
|
"id": "m2",
|
|
"title": "Dune Part Two",
|
|
"series": "",
|
|
"season": "",
|
|
"season_number": None,
|
|
"episode": None,
|
|
"type": "Movie",
|
|
"year": 2024,
|
|
"runtime_ticks": 99600000000,
|
|
"runtime_min": 166,
|
|
"size_bytes": 45_000_000_000,
|
|
"bitrate_bps": 40_000_000,
|
|
"hdr": 1,
|
|
"video": "hevc",
|
|
"width": 3840,
|
|
"height": 2160,
|
|
"resolution": "3840x2160",
|
|
"date_added": "2024-06-01",
|
|
"date_added_ts": 1717200000,
|
|
"path": "/media/movies/Dune Part Two (2024)/file.mkv",
|
|
"library_id": "lib1",
|
|
"library_name": "Movies",
|
|
"size": "45.0 GB",
|
|
"bitrate": "40.0 Mbps",
|
|
},
|
|
{
|
|
"id": "e1",
|
|
"title": "Pilot",
|
|
"series": "Breaking Bad",
|
|
"season": "S01",
|
|
"season_number": 1,
|
|
"episode": 1,
|
|
"type": "Episode",
|
|
"year": 2008,
|
|
"runtime_ticks": 35000000000,
|
|
"runtime_min": 58,
|
|
"size_bytes": 3_000_000_000,
|
|
"bitrate_bps": 8_000_000,
|
|
"hdr": 0,
|
|
"video": "h264",
|
|
"width": 1920,
|
|
"height": 1080,
|
|
"resolution": "1920x1080",
|
|
"date_added": "2024-03-01",
|
|
"date_added_ts": 1709251200,
|
|
"path": "/media/shows/Breaking Bad/S01E01.mkv",
|
|
"library_id": "lib2",
|
|
"library_name": "TV Shows",
|
|
"size": "3.0 GB",
|
|
"bitrate": "8.0 Mbps",
|
|
},
|
|
]
|
|
index.replace_items(rows)
|
|
return index
|
|
|
|
|
|
class TestMediaIndexStatus:
|
|
def test_nonexistent(self, tmp_path):
|
|
idx = MediaIndex(tmp_path / "nonexistent.sqlite")
|
|
status = idx.status()
|
|
assert status.exists is False
|
|
assert status.item_count == 0
|
|
|
|
def test_empty_index(self, index):
|
|
index.init_schema()
|
|
status = index.status()
|
|
assert status.exists is True
|
|
assert status.item_count == 0
|
|
|
|
def test_populated(self, populated_index):
|
|
status = populated_index.status()
|
|
assert status.exists is True
|
|
assert status.item_count == 3
|
|
|
|
|
|
class TestMediaIndexQuery:
|
|
def test_query_all(self, populated_index):
|
|
rows, total = populated_index.query(
|
|
library_ids=["lib1", "lib2"],
|
|
media_types=["Movie", "Episode"],
|
|
)
|
|
assert total == 3
|
|
assert len(rows) == 3
|
|
|
|
def test_filter_by_library(self, populated_index):
|
|
rows, total = populated_index.query(
|
|
library_ids=["lib1"],
|
|
media_types=["Movie", "Episode"],
|
|
)
|
|
assert total == 2
|
|
# Only movies from lib1
|
|
assert all(r["library"] == "Movies" for r in rows)
|
|
|
|
def test_filter_by_type(self, populated_index):
|
|
rows, total = populated_index.query(
|
|
library_ids=["lib1", "lib2"],
|
|
media_types=["Episode"],
|
|
)
|
|
assert total == 1
|
|
assert rows[0]["title"] == "Pilot"
|
|
|
|
def test_search(self, populated_index):
|
|
rows, total = populated_index.query(
|
|
library_ids=["lib1", "lib2"],
|
|
media_types=["Movie", "Episode"],
|
|
search="matrix",
|
|
)
|
|
assert total == 1
|
|
assert rows[0]["title"] == "The Matrix"
|
|
|
|
def test_hdr_filter(self, populated_index):
|
|
rows, total = populated_index.query(
|
|
library_ids=["lib1", "lib2"],
|
|
media_types=["Movie", "Episode"],
|
|
hdr_filter="HDR only",
|
|
)
|
|
assert total == 1
|
|
assert rows[0]["title"] == "Dune Part Two"
|
|
|
|
def test_sdr_filter(self, populated_index):
|
|
rows, total = populated_index.query(
|
|
library_ids=["lib1", "lib2"],
|
|
media_types=["Movie", "Episode"],
|
|
hdr_filter="SDR/unknown only",
|
|
)
|
|
assert total == 2
|
|
|
|
def test_sort_by_size_desc(self, populated_index):
|
|
rows, total = populated_index.query(
|
|
library_ids=["lib1", "lib2"],
|
|
media_types=["Movie", "Episode"],
|
|
sort_key="size",
|
|
sort_order="Descending",
|
|
)
|
|
assert rows[0]["title"] == "Dune Part Two" # largest
|
|
|
|
def test_sort_by_year_asc(self, populated_index):
|
|
rows, total = populated_index.query(
|
|
library_ids=["lib1", "lib2"],
|
|
media_types=["Movie", "Episode"],
|
|
sort_key="year",
|
|
sort_order="Ascending",
|
|
)
|
|
assert rows[0]["title"] == "The Matrix" # 1999
|
|
|
|
def test_pagination(self, populated_index):
|
|
rows, total = populated_index.query(
|
|
library_ids=["lib1", "lib2"],
|
|
media_types=["Movie", "Episode"],
|
|
limit=2,
|
|
offset=0,
|
|
)
|
|
assert total == 3
|
|
assert len(rows) == 2
|
|
|
|
rows2, total2 = populated_index.query(
|
|
library_ids=["lib1", "lib2"],
|
|
media_types=["Movie", "Episode"],
|
|
limit=2,
|
|
offset=2,
|
|
)
|
|
assert total2 == 3
|
|
assert len(rows2) == 1
|
|
|
|
def test_display_row_format(self, populated_index):
|
|
rows, _ = populated_index.query(
|
|
library_ids=["lib1"],
|
|
media_types=["Movie"],
|
|
)
|
|
for row in rows:
|
|
assert "title" in row
|
|
assert "hdr" in row
|
|
assert row["hdr"] in ("yes", "no")
|
|
assert "library" in row
|
|
assert "path" in row
|
|
|
|
|
|
class TestMediaIndexReplace:
|
|
def test_replace_clears_old(self, index):
|
|
index.replace_items([
|
|
{"id": "x", "title": "Old", "type": "Movie", "library_id": "l1", "library_name": "L1"},
|
|
])
|
|
status = index.status()
|
|
assert status.item_count == 1
|
|
|
|
index.replace_items([
|
|
{"id": "y", "title": "New1", "type": "Movie", "library_id": "l1", "library_name": "L1"},
|
|
{"id": "z", "title": "New2", "type": "Movie", "library_id": "l1", "library_name": "L1"},
|
|
])
|
|
status = index.status()
|
|
assert status.item_count == 2
|
|
|
|
|
|
class TestMediaIndexMetadata:
|
|
def test_set_and_read_metadata(self, index):
|
|
index.set_metadata("build_duration_seconds", "12.5")
|
|
status = index.status()
|
|
assert status.build_duration_seconds == 12.5
|