Files
siftlode/backend/tests/test_plex_paths.py
T
peter 5806adda7e fix(plex): a listing cut short no longer clears watch state or Hungarian text
The daily watch reconcile reads "absent from Plex's scan" as "Plex un-watched it" and deletes the
row — but it walked the sections without asking _paginate whether the walk reached the server's
total, so a Plex hiccup mid-walk (an empty page early) un-watched every title it failed to reach
until the next complete run. The scan now reports completeness; an incomplete one still pulls in
what it saw, re-pushes dirty rows, and clears nothing (logged as plex.watch.reconcile_incomplete).

The HU overlay had the same shape: clear every row's HU text, then refill from the listing. It now
reads the listing first and clears only after a complete one. The overlay moved to plex/overlay.py
(plex/sync.py drops under the 800-line limit and off the size ratchet's list).

Tests: plex/paths (23% -> 100%: prefix boundary, longest prefix, dead mount), the reconcile's
clear/keep/re-push rules, the cut-short scan and listing (each seen red with the guard removed).
2026-10-03 19:33:30 +02:00

83 lines
3.0 KiB
Python

"""plex/paths — the Plex-side path → local mount translation (E32 S2: 23% covered, and a wrong answer
here reads as "media file not found" for a whole library at once, with nothing else failing).
Pure logic: the admin map is read through sysconfig, stubbed per test.
"""
from pathlib import Path
import pytest
from app.plex import paths
@pytest.fixture
def path_map(monkeypatch):
def use(raw: str) -> None:
monkeypatch.setattr(paths.sysconfig, "get_str", lambda db, key: raw)
return use
def test_the_map_parses_both_separators_drops_junk_and_puts_the_longest_prefix_first():
raw = " /data=/mnt/a ,\n/data/movies = /mnt/m\nnot-a-pair,=/x,/y=, /data/tv=/mnt/tv "
assert paths.parse_map(raw) == [
("/data/movies", "/mnt/m"),
("/data/tv", "/mnt/tv"),
("/data", "/mnt/a"),
]
assert paths.parse_map("") == []
def test_the_longest_matching_prefix_wins(path_map):
path_map("/data=/mnt/a,/data/movies=/mnt/m")
assert paths.map_path(None, "/data/movies/Foo.mkv") == str(Path("/mnt/m") / "Foo.mkv")
assert paths.map_path(None, "/data/tv/Bar.mkv") == str(Path("/mnt/a") / "tv/Bar.mkv")
def test_a_prefix_matches_only_at_a_path_boundary(path_map):
# "/data/movies2" shares the characters of "/data/movies" but is another folder.
path_map("/data/movies=/mnt/m")
assert paths.map_path(None, "/data/movies2/Foo.mkv") == "/data/movies2/Foo.mkv"
def test_the_prefix_itself_and_a_trailing_slash_in_the_map(path_map):
path_map("/data/movies/=/mnt/m")
assert paths.map_path(None, "/data/movies/Foo.mkv") == str(Path("/mnt/m") / "Foo.mkv")
path_map("/data/movies=/mnt/m")
assert paths.map_path(None, "/data/movies") == "/mnt/m"
def test_no_map_leaves_the_path_as_plex_sees_it(path_map):
path_map("")
assert paths.map_path(None, "/data/movies/Foo.mkv") == "/data/movies/Foo.mkv"
def test_the_local_file_must_exist(path_map, tmp_path):
(tmp_path / "movies").mkdir()
(tmp_path / "movies" / "Foo.mkv").write_bytes(b"x")
path_map(f"/data={tmp_path}")
assert (
paths.local_media_path(None, "/data/movies/Foo.mkv")
== (tmp_path / "movies" / "Foo.mkv").resolve()
)
assert paths.local_media_path(None, "/data/movies/Missing.mkv") is None
assert paths.local_media_path(None, "/data/movies") is None # a folder is not a file
assert paths.local_media_path(None, None) is None
assert paths.local_media_path(None, "") is None
def test_a_path_that_cannot_be_resolved_is_logged_and_answered_none(path_map, monkeypatch, caplog):
path_map("")
def broken(self, strict=False):
raise OSError("dead mount")
monkeypatch.setattr(paths.Path, "resolve", broken)
assert paths.local_media_path(None, "/data/movies/Foo.mkv") is None
rec = next(
r for r in caplog.records if getattr(r, "event", None) == "plex.paths.resolve_failed"
)
# The title stays out of the log line: the path carries it.
assert "Foo" not in rec.getMessage() and rec.attrs == {"reason": "OSError"}