"""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 " 0 for d, n in days.items())