"""Tests for the A4 theming fix. Root cause being fixed: mutating ``
`` do a full-page navigation, and the POST routes used to return a bare partial with no ``base.html`` wrapper -- an unstyled page. Now the same route renders inline (bare partial) for an htmx request (``HX-Request: true``) and a themed full page (``templates/action_page.html``, extending ``base.html``) for a plain no-JS navigation. Config/CSRF setup mirrors ``test_web_actions.py``'s ``_make_config`` (a bare, non-OIDC config is enough -- these routes only need a CSRF-bearing session, not real authentication). """ from __future__ import annotations import re from pathlib import Path from fastapi.testclient import TestClient from ci_orchestrator.config import BackendConfig, CargoCacheConfig, Config, Paths, WebConfig from ci_orchestrator.web.app import create_app, get_backend def _make_config(root: Path) -> Config: return Config( paths=Paths( root=root, templates=root / "templates", build_vms=root / "build-vms", artifacts=root / "artifacts", keys=root / "keys", ), backend=BackendConfig(), cargo_cache=CargoCacheConfig( enabled=True, root=root / "cache" / "cargo", retention_days=30 ), # audit_log MUST live under tmp_path -- the WebConfig default points # at the real host's /var/lib/ci/logs, which these tests must never # touch (see the module docstring: routes exercised here audit). web=WebConfig(audit_log=root / "web-audit.jsonl"), ) def _csrf_from(text: str) -> str: match = re.search(r'name="csrf" value="([^"]+)"', text) assert match is not None, f"no csrf field found in: {text[:300]!r}" return match.group(1) # ── HX-Request present -> bare partial, no theme wrapper ─────────────────── def test_hx_request_preview_gets_bare_partial(tmp_path: Path) -> None: app = create_app(_make_config(tmp_path)) client = TestClient(app) csrf = _csrf_from(client.get("/").text) resp = client.post( "/cache/purge", data={"csrf": csrf}, headers={"HX-Request": "true"} ) assert resp.status_code == 200 assert " None: app = create_app(_make_config(tmp_path)) client = TestClient(app) csrf = _csrf_from(client.get("/").text) resp = client.post( "/cache/purge", data={"csrf": csrf, "stage": "execute"}, headers={"HX-Request": "true"}, ) assert resp.status_code == 200 assert " full themed page ─────────────────────────────────── def test_no_hx_header_preview_gets_full_themed_page(tmp_path: Path) -> None: app = create_app(_make_config(tmp_path)) client = TestClient(app) csrf = _csrf_from(client.get("/").text) resp = client.post("/cache/purge", data={"csrf": csrf}) assert resp.status_code == 200 assert ' None: app = create_app(_make_config(tmp_path)) client = TestClient(app) csrf = _csrf_from(client.get("/").text) resp = client.post("/cache/purge", data={"csrf": csrf, "stage": "execute"}) assert resp.status_code == 200 assert ' None: config = _make_config(tmp_path) config.paths.build_vms.mkdir(parents=True, exist_ok=True) lock_path = config.paths.build_vms.parent / "vm-start.lock" lock_path.parent.mkdir(parents=True, exist_ok=True) lock_path.write_text("job in progress", encoding="utf-8") app = create_app(config) app.dependency_overrides[get_backend] = lambda: None client = TestClient(app) csrf = _csrf_from(client.get("/").text) resp = client.post("/vms/cleanup", data={"csrf": csrf, "stage": "execute"}) assert resp.status_code == 409 assert ' None: config = _make_config(tmp_path) config.paths.build_vms.mkdir(parents=True, exist_ok=True) lock_path = config.paths.build_vms.parent / "vm-start.lock" lock_path.parent.mkdir(parents=True, exist_ok=True) lock_path.write_text("job in progress", encoding="utf-8") app = create_app(config) app.dependency_overrides[get_backend] = lambda: None client = TestClient(app) csrf = _csrf_from(client.get("/").text) resp = client.post( "/vms/cleanup", data={"csrf": csrf, "stage": "execute"}, headers={"HX-Request": "true"}, ) assert resp.status_code == 409 assert " None: app = create_app(_make_config(tmp_path)) client = TestClient(app) client.get("/cache") # establish a session resp = client.post("/cache/purge", data={}, headers={"HX-Request": "true"}) assert resp.status_code == 403