"""Page smoke tests — every HTML route loads (or redirects) per its auth tier.
Catches the failures that take a page from 200 to 500: template syntax errors,
missing data files, broken route wiring, import-time crashes. Does NOT exercise
LLM calls or mutate data — pure GETs plus the two login endpoints.
Run (server is up locally):
SMOKE_BASE_URL=http://localhost:8080 pytest tests/ -q
"""
import pytest
from conftest import API_KEY, TURNSTILE_SECRET, HTML_ACCEPT
# Public — no auth, must render. Only the front doors + login bootstrap stay open.
PUBLIC_PAGES = ["/", "/recruiter", "/login", "/guest"]
# require_auth — no auth redirects to /login; admin Bearer renders.
# /hosaka moved guest->owner 2026-11-10 (its health probe + sockets stay guest:
# /tarot narrates through them).
PROTECTED_PAGES = ["/rd", "/hq", "/debug", "/hosaka"]
# require_guest_auth — no auth redirects to /guest; guest or admin Bearer renders.
# /graph, /UI, /security, /nightfall moved public->guest 2026-07-03; /printer
# owner->guest 2026-08-30 (READ-ONLY for guests — see the tier tests below).
# /zombo is UNLINKED (no nav entry, no landing spoke) but still gated: unlisted
# is not a tier, so it is asserted here like every other guest page.
GUEST_PAGES = ["/mtg", "/tarot", "/graph", "/UI", "/security", "/nightfall",
"/printer", "/zombo", "/noodle", "/noodle-tc", "/spire"]
def _is_page(r) -> bool:
return r.status_code == 200 and len(r.text) > 100 and "<" in r.text
# ── public pages ──────────────────────────────────────────────────────────────
@pytest.mark.parametrize("path", PUBLIC_PAGES)
def test_public_page_loads(client, path):
r = client.get(path, headers=HTML_ACCEPT)
assert r.status_code == 200, f"{path} -> {r.status_code}"
assert _is_page(r), f"{path} returned 200 but body looks empty/broken"
def test_guest_login_alias_redirects(client):
r = client.get("/guest-login", headers=HTML_ACCEPT)
assert r.status_code == 302
assert r.headers["location"].startswith("/guest")
# ── protected pages (require_auth) ──────────────────────────────────────────────
@pytest.mark.parametrize("path", PROTECTED_PAGES)
def test_protected_redirects_without_auth(client, path):
r = client.get(path, headers=HTML_ACCEPT)
assert r.status_code == 302, f"{path} -> {r.status_code} (expected login redirect)"
assert r.headers["location"].startswith("/login")
@pytest.mark.parametrize("path", PROTECTED_PAGES)
def test_protected_refuses_guest_cookie(client, guest_cookie, path):
r = client.get(path, headers={**guest_cookie, **HTML_ACCEPT})
assert r.status_code == 302, f"{path} -> {r.status_code} with guest cookie"
assert r.headers["location"].startswith("/login")
@pytest.mark.parametrize("path", PROTECTED_PAGES)
def test_protected_loads_with_admin(client, admin_headers, path):
r = client.get(path, headers=admin_headers)
assert r.status_code == 200, f"{path} -> {r.status_code} with admin Bearer"
assert _is_page(r), f"{path} returned 200 but body looks empty/broken"
# ── guest pages (require_guest_auth) ────────────────────────────────────────────
@pytest.mark.parametrize("path", GUEST_PAGES)
def test_guest_redirects_without_auth(client, path):
r = client.get(path, headers=HTML_ACCEPT)
assert r.status_code == 302, f"{path} -> {r.status_code} (expected guest redirect)"
assert r.headers["location"].startswith("/guest")
@pytest.mark.parametrize("path", GUEST_PAGES)
def test_guest_loads_with_guest_cookie(client, guest_cookie, path):
r = client.get(path, headers=guest_cookie)
assert r.status_code == 200, f"{path} -> {r.status_code} with guest cookie"
assert _is_page(r)
@pytest.mark.parametrize("path", GUEST_PAGES)
def test_guest_loads_with_admin_bearer(client, admin_headers, path):
# Admin auth also satisfies the guest gate.
r = client.get(path, headers=admin_headers)
assert r.status_code == 200, f"{path} -> {r.status_code} with admin Bearer"
# ── auth endpoints ──────────────────────────────────────────────────────────────
def test_login_post_good_key_sets_cookie(client):
if not API_KEY:
pytest.skip("API_KEY not available")
r = client.post("/login", data={"key": API_KEY})
assert r.status_code == 303
assert "session=" in r.headers.get("set-cookie", "")
def test_login_post_bad_key_rejected(client):
r = client.post("/login", data={"key": "wrong-key"})
assert r.status_code == 401
def test_guest_post_valid_turnstile_sets_cookie(client):
# Only the Cloudflare test secret (1x0000…) attests an arbitrary token; with a
# real secret a passing token can't be minted offline, so skip there.
if not (TURNSTILE_SECRET or "").startswith("1x0000000000000000000000000000000"):
pytest.skip("not using the Cloudflare test secret; cannot mint a passing token")
r = client.post("/guest", data={"cf-turnstile-response": "dummy"})
assert r.status_code == 303
assert "guest_session=" in r.headers.get("set-cookie", "")
def test_guest_post_no_token_rejected(client):
# Empty/missing Turnstile token is a fast 401 (no siteverify call) under any secret.
r = client.post("/guest", data={"cf-turnstile-response": ""})
assert r.status_code == 401
# ── read-only JSON API (a couple, as a wiring check) ────────────────────────────
def test_public_json_api_loads(client):
r = client.get("/api/ui/usage")
assert r.status_code == 200
assert r.headers["content-type"].startswith("application/json")
def test_protected_json_api_requires_auth(client):
# JSON Accept -> 401 (no html redirect).
r = client.get("/api/debug/logs", headers={"Accept": "application/json"})
assert r.status_code == 401
def test_protected_json_api_loads_with_admin(client, admin_headers):
r = client.get("/api/debug/logs", headers={**admin_headers, "Accept": "application/json"})
assert r.status_code == 200
# ── gpu-mode owner-only routes ──────────────────────────────────────────────────
def test_hosaka_mode_route_authed(client, admin_headers):
r = client.get("/api/hosaka/mode", headers=admin_headers)
assert r.status_code == 200
assert r.json()["mode"] in {"homo", "emo", "idle", "gone"}
def test_hosaka_mode_route_requires_auth(client):
# no cookie / no bearer -> 401 from require_auth
r = client.get("/api/hosaka/mode")
assert r.status_code == 401
# ── printer owner-only routes ───────────────────────────────────────────────────
def test_printer_health_route_authed(client, admin_headers):
# 200 {ok:true} when the printer answers through the tunnel, 503 {ok:false}
# when it (or the home box) is off -- both are the route working.
r = client.get("/api/printer/health", headers={**admin_headers, "Accept": "application/json"})
assert r.status_code in (200, 503)
assert r.json()["ok"] is (r.status_code == 200)
def test_printer_health_route_requires_auth(client):
r = client.get("/api/printer/health")
assert r.status_code == 401
def test_printer_proxy_requires_auth(client):
# The proxied SPA + its websocket-adjacent routes are full-session only.
r = client.get("/printer/network-device-manager/network/control", headers=HTML_ACCEPT)
assert r.status_code == 302
assert r.headers["location"].startswith("/login")
r = client.get("/printer/video")
assert r.status_code == 401
# ── printer tier split: guests view, only the owner controls ────────────────────
def test_printer_control_routes_deny_guest(client, guest_cookie):
"""The whole point of the public page: nothing a guest sends reaches the
LAN. The SPA proxy (the only browser->printer HTTP path) must not serve a
guest -- it bounces to the ADMIN login, not the guest gate."""
for path in ("/printer/network-device-manager/network/control",
"/printer/index.html",
"/printer/uploadFile/upload"):
r = client.get(path, headers=guest_cookie)
assert r.status_code == 302, f"{path} -> {r.status_code} for a guest"
assert r.headers["location"].startswith("/login"), path
def test_printer_page_is_readonly_for_guest(client, guest_cookie, admin_cookie):
# The server decides the tier; printer.js reads data-readonly and never
# mounts the SPA frame for a guest.
assert 'data-readonly="1"' in client.get("/printer", headers=guest_cookie).text
assert 'data-readonly="1"' not in client.get("/printer", headers=admin_cookie).text
def test_printer_status_is_guest_readable_and_scrubbed(client, guest_cookie):
"""Status is pushed by the printer (no command sent) and whitelisted: no
identifiers, nothing naming what is being printed."""
r = client.get("/api/printer/status", headers={**guest_cookie, "Accept": "application/json"})
assert r.status_code == 200
body = r.json()
assert "online" in body
for leaked in ("Filename", "TaskId", "MainboardID", "filename"):
assert leaked not in r.text, f"{leaked} leaked into the public status payload"
if body["online"]:
assert {"state", "nozzle", "bed", "progress"} <= set(body)
def test_printer_video_streams_for_guest(client, guest_cookie):
"""Guests get the camera -- a one-way read the server opens, fanned out
from a single shared upstream stream."""
with client.stream("GET", "/printer/video", headers=guest_cookie) as r:
if r.status_code == 503:
pytest.skip("printer offline / too many viewers")
assert r.status_code == 200
assert "multipart/x-mixed-replace" in r.headers["content-type"]
for chunk in r.iter_bytes():
assert chunk.startswith(b"--printerframe")
break
# ── noodle: public by unguessable link, owner-only to create ───────────────────
def test_noodle_poll_pages_are_public(client, noodle_slug):
r = client.get(f"/noodle/{noodle_slug}", headers=HTML_ACCEPT)
assert r.status_code == 200 and _is_page(r)
assert "noindex" in r.headers.get("x-robots-tag", "")
assert 'data-kdf=' in r.text and 'id="nd-voters"' in r.text
# the feedback mailto sits under the make-another link
assert r.text.index("make your own noodle") < r.text.index('href="mailto:wl.wailau@gmail.com?subject=noodle')
# results moved onto the vote page; old links redirect there
r = client.get(f"/noodle/{noodle_slug}/results", headers=HTML_ACCEPT)
assert r.status_code == 301 and r.headers["location"] == f"/noodle/{noodle_slug}"
def test_noodle_poll_page_has_a_link_preview(client, noodle_slug):
"""Open Graph tags (Messenger etc.): absolute urls, the fixed dark card."""
r = client.get(f"/noodle/{noodle_slug}", headers=HTML_ACCEPT)
assert '' in r.text
assert f'' in r.text
assert 'content="https://wai-lau.net/noodle-card.jpg?v=' in r.text
assert 'name="twitter:card" content="summary_large_image"' in r.text
assert '' in r.text
img = client.get("/noodle-card.jpg")
assert img.status_code == 200 and img.headers["content-type"] == "image/jpeg"
def test_noodle_page_has_no_demo_video(client, guest_cookie, admin_cookie):
"""/noodle is the title and the create button, nothing else: the demo video
was dropped 2026-10-03 (owner feedback)."""
for h in (guest_cookie, admin_cookie):
r = client.get("/noodle", headers=h)
assert r.status_code == 200 and "