Admin routes for the subscription queue, and record the build in plan.md
Phase 4's acceptance criterion is that my brother approves the first import himself, which needs a UI, so /pending now carries source management, sync-now, and multi-select approve/reject. Driven over real HTTP rather than only through the templates: unauthenticated requests redirect to login, all four new routes reject a missing or forged CSRF token, and the live account rendered 117 checkboxes. Approving three at once added three channels, which is the point of the fix underneath. _form() collapses repeated fields to the last value, which is correct for every single-value field but silently wrong for a form of checkboxes all named `id` — it would have approved only the last box ticked. Added _form_list(), with the parsed body cached because rfile can only be read once and the approval path needs both views of it. The approval page is deliberately its own page rather than a section on the index: the first sync of the real account queued 119 channels, and that does not belong inline under the channel table. Source errors are shown in full rather than truncated, because the useful ones say exactly what to do — "subscriptions are private, uncheck Keep all my subscriptions private" — and hiding that behind a log file defeats the purpose of surfacing it. plan.md §13 now reflects what is actually built rather than what was intended, and a new §17 records the three bugs the build turned up, including which of them a test caught and which two needed real data. 337 tests. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
155f05773d
commit
61cc1672ec
@@ -144,3 +144,75 @@ def test_csrf_token_rejects_tampering():
|
||||
def test_csrf_rejects_an_empty_token():
|
||||
secret = auth.new_secret()
|
||||
assert not auth.verify_csrf(secret, "s", "")
|
||||
|
||||
|
||||
# --------------------------------------------------- sources / approval queue
|
||||
|
||||
|
||||
def _source(**kw):
|
||||
base = {"key": "youtube:UCbrother", "label": "C Flux",
|
||||
"channel_id": "UCPcTWaLV8zwx4WP4QExHj4Q", "enabled": 1, "imported": 1,
|
||||
"last_sync_at": "2026-08-12T16:00:00+00:00", "last_sync_ok": 1,
|
||||
"consecutive_failures": 0, "last_error": None}
|
||||
base.update(kw)
|
||||
return base
|
||||
|
||||
|
||||
def _pending(n=3):
|
||||
return [{"id": i, "source": "youtube:UCbrother", "title": f"Channel {i}",
|
||||
"channel_id": f"UC{i:022d}", "seen_at": "2026-08-12T16:00:00+00:00"}
|
||||
for i in range(1, n + 1)]
|
||||
|
||||
|
||||
def test_pending_page_renders_the_queue():
|
||||
html = templates.pending_page(pending=_pending(3), sources=[_source()],
|
||||
csrf="tok").decode()
|
||||
assert "3 channel(s) waiting" in html
|
||||
assert "Channel 1" in html and "Channel 3" in html
|
||||
assert "Approve selected" in html
|
||||
|
||||
|
||||
def test_every_queue_row_shares_the_id_field_name():
|
||||
"""The approve handler reads a repeated `id` field; if the template numbered
|
||||
them uniquely the multi-select would silently approve nothing."""
|
||||
html = templates.pending_page(pending=_pending(3), sources=[_source()],
|
||||
csrf="tok").decode()
|
||||
assert html.count('name="id"') == 3
|
||||
|
||||
|
||||
def test_pending_page_with_an_empty_queue():
|
||||
html = templates.pending_page(pending=[], sources=[_source()], csrf="tok").decode()
|
||||
assert "Nothing awaiting approval" in html
|
||||
|
||||
|
||||
def test_pending_page_with_no_sources():
|
||||
html = templates.pending_page(pending=[], sources=[], csrf="tok").decode()
|
||||
assert "No sources yet" in html
|
||||
|
||||
|
||||
def test_never_synced_source_is_labelled():
|
||||
html = templates.pending_page(pending=[], sources=[_source(last_sync_ok=None)],
|
||||
csrf="tok").decode()
|
||||
assert "never synced" in html
|
||||
|
||||
|
||||
def test_failing_source_shows_the_actionable_error_in_full():
|
||||
"""The useful errors say exactly what to do; truncating them defeats the point."""
|
||||
message = ('subscriptions are private (subscriptionForbidden) — nothing '
|
||||
'changed. Fix: YouTube → Settings → Privacy → uncheck "Keep all '
|
||||
'my subscriptions private".')
|
||||
html = templates.pending_page(
|
||||
pending=[], sources=[_source(last_sync_ok=0, consecutive_failures=3,
|
||||
last_error=message)],
|
||||
csrf="tok").decode()
|
||||
assert "failing (3)" in html
|
||||
assert "Keep all" in html
|
||||
|
||||
|
||||
def test_hostile_pending_title_is_escaped():
|
||||
items = _pending(1)
|
||||
items[0]["title"] = '<img src=x onerror=alert(1)>'
|
||||
html = templates.pending_page(pending=items, sources=[_source()],
|
||||
csrf="tok").decode()
|
||||
assert "<img src=x" not in html
|
||||
assert "<img" in html
|
||||
|
||||
Reference in New Issue
Block a user