From 4ed07388e842378cf57eba60488c9f37b3f7967e Mon Sep 17 00:00:00 2001 From: npeter83 Date: Sat, 7 Nov 2026 21:48:17 +0100 Subject: [PATCH] test(feed): an off-catalog video's length, and async readers of the raw column The YouTube lookup's length had no test, and the guard on who reads the raw duration skipped async functions and module-level code. --- backend/tests/test_video_length.py | 39 ++++++++++++++++++++++++++++-- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/backend/tests/test_video_length.py b/backend/tests/test_video_length.py index 85ecebc5..5dbdda47 100644 --- a/backend/tests/test_video_length.py +++ b/backend/tests/test_video_length.py @@ -95,6 +95,32 @@ def test_a_years_long_archive_has_no_length_in_the_feed_or_a_playlist(db, make_u assert one.json()["duration_seconds"] is None +def test_an_off_catalog_archive_has_no_length_either(db, make_user, as_user, monkeypatch): + # A video not in the catalog is looked up on YouTube (videos.list); its length is judged the same. + from app.routes import feed as feed_routes + + class Yt: + def __init__(self, *_): + pass + + def __enter__(self): + return self + + def __exit__(self, *exc): + return False + + def get_videos(self, ids): + hours = {"offyears": YEARS // HOUR, "offshort": 1}[ids[0]] + return [ + {"snippet": {}, "statistics": {}, "contentDetails": {"duration": f"PT{hours}H"}} + ] + + monkeypatch.setattr(feed_routes, "YouTubeClient", Yt) + client = as_user(make_user()) + assert client.get("/api/videos/offyears").json()["duration_seconds"] is None + assert client.get("/api/videos/offshort").json()["duration_seconds"] == HOUR + + def test_the_totals_leave_it_out_and_count_the_live_ones(db, make_user, as_user): user = make_user() pl = _seed(db, user) @@ -165,15 +191,24 @@ def _raw_reads() -> set[tuple[str, str]]: continue rel = path.relative_to(root).as_posix() tree = ast.parse(path.read_text(encoding="utf-8")) + in_a_function: set[int] = set() for fn in ast.walk(tree): - if not isinstance(fn, ast.FunctionDef): + if not isinstance(fn, (ast.FunctionDef, ast.AsyncFunctionDef)): continue for node in ast.walk(fn): - if isinstance(node, ast.Attribute) and node.attr == "duration_seconds": + if _reads_raw(node): found.add((rel, fn.name)) + in_a_function.add(id(node)) + # A read outside every def (module or class level, a lambda there) is named ``. + if any(_reads_raw(n) and id(n) not in in_a_function for n in ast.walk(tree)): + found.add((rel, "")) return found +def _reads_raw(node: ast.AST) -> bool: + return isinstance(node, ast.Attribute) and node.attr == "duration_seconds" + + def test_only_the_listed_readers_take_the_raw_column(): found = _raw_reads() stray = sorted(found - RAW_READERS)