"""The admin UI: rendering, auth gating and CSRF.
The templates are f-strings over a dict, so a renamed key is a KeyError at render
time rather than a type error at import time — which is exactly the kind of
breakage that only shows up when somebody opens the page. These tests render every
page for real.
"""
from __future__ import annotations
import pytest
from ytstream import videos
from ytstream.web import auth, templates
from conftest import add_channel, add_video
@pytest.fixture()
def channel_row(conn, settings, channel):
"""The dict shape server.py builds for the channel table."""
add_video(conn, channel["id"], "vid00000001", state=videos.MATERIALISED)
return {
"id": channel["id"],
"title": channel["title"],
"handle": channel["handle"],
"channel_id": channel["channel_id"],
"retention_days": None,
"global_retention": 30,
"last_polled_at": "2026-08-12T14:00:00+00:00",
"last_poll_ok": 1,
"consecutive_poll_failures": 0,
"episodes": 1,
"source": "youtube",
"missing_syncs": 0,
"missing_threshold": 3,
"latest": "2026-08-12",
}
def test_login_page_renders(settings):
html = templates.login_page().decode()
assert "ytstream" in html
assert "youtube-automate" not in html
assert 'type="password"' in html
def test_login_page_shows_an_error():
assert "Wrong" in templates.login_page("Wrong password").decode()
def test_index_page_renders(channel_row):
html = templates.index_page(
channels=[channel_row], settings_values={"retention_days": "30"},
settings_errors={}, csrf="tok", add_error=None, queue_depth=0,
).decode()
assert "clabretro" in html
assert "ytstream" in html
# The provenance column replaced the meaningless size column.
assert "youtube" in html
assert "Size" not in html
def test_index_page_renders_with_no_channels():
html = templates.index_page(
channels=[], settings_values={}, settings_errors={}, csrf="tok",
add_error=None, queue_depth=0,
).decode()
assert "0 channel(s)" in html
def test_absence_badge_appears_before_the_channel_disappears(channel_row):
"""A channel counting towards removal must be visible while it still exists."""
channel_row["missing_syncs"] = 2
html = templates.index_page(
channels=[channel_row], settings_values={}, settings_errors={}, csrf="tok",
add_error=None, queue_depth=0,
).decode()
assert "absent 2/3 syncs" in html
def test_no_absence_badge_when_healthy(channel_row):
html = templates.index_page(
channels=[channel_row], settings_values={}, settings_errors={}, csrf="tok",
add_error=None, queue_depth=0,
).decode()
assert "absent" not in html
def test_hostile_channel_title_is_escaped(channel_row):
channel_row["title"] = ''
html = templates.index_page(
channels=[channel_row], settings_values={}, settings_errors={}, csrf="tok",
add_error=None, queue_depth=0,
).decode()
assert "