Files
local-ci-cd-system/tests/python/test_web_theming.py
T
Simone 9d7bbee35d feat(web): dashboard redesign — Gitea palette, health panels, VM/audit enrichment (stages A–C)
Stage A: swap CSS tokens to the real Gitea dark palette (accent blue #4183c4); add Jinja formatting filters (humanbytes/duration/relative_time/localtime/shorten_path/thousands), a health status-header, and an inline SVG icon sprite; fix two UI bugs — unthemed action pages (htmx swap into #action-slot with a full-page HX-Request fallback) and polling wiping form input (hx-preserve).

Stage B: four read-only panels that degrade to notices (never 500) — host resources (/proc, two-sample CPU%), VMware module health, template inventory (.vmem warn + last backup), and a path-contained file browser; backup card becomes a validated template dropdown; expensive dir-size walk is TTL-cached. Adds operations/inventory.py plus host_resources()/vmware_module_health() in operations/host.py.

Stage C: parse_clone_name() adds a Job column (job_id + started) to the VM table; nav gains a Gitea link-out derived from the OIDC issuer origin (no fabricated run URLs); audit page gets action icons, relative time, and a client-side filter.

Gates: 989 tests pass, coverage 94.12%, ruff + mypy --strict clean. Stage D (service management) intentionally deferred.
2026-07-24 11:46:50 +02:00

164 lines
5.7 KiB
Python

"""Tests for the A4 theming fix.
Root cause being fixed: mutating ``<form method="post">`` 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("/cache").text)
resp = client.post(
"/cache/purge", data={"csrf": csrf}, headers={"HX-Request": "true"}
)
assert resp.status_code == 200
assert "<html" not in resp.text.lower()
assert '<link rel="stylesheet"' not in resp.text
assert "Confirm" in resp.text
def test_hx_request_execute_gets_bare_partial(tmp_path: Path) -> None:
app = create_app(_make_config(tmp_path))
client = TestClient(app)
csrf = _csrf_from(client.get("/cache").text)
resp = client.post(
"/cache/purge",
data={"csrf": csrf, "stage": "execute"},
headers={"HX-Request": "true"},
)
assert resp.status_code == 200
assert "<html" not in resp.text.lower()
assert '<link rel="stylesheet"' not in resp.text
# ── HX-Request absent -> 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("/cache").text)
resp = client.post("/cache/purge", data={"csrf": csrf})
assert resp.status_code == 200
assert '<link rel="stylesheet" href="/static/app.css">' in resp.text
assert 'id="action-slot"' in resp.text
assert "Confirm" in resp.text
def test_no_hx_header_execute_gets_full_themed_page(tmp_path: Path) -> None:
app = create_app(_make_config(tmp_path))
client = TestClient(app)
csrf = _csrf_from(client.get("/cache").text)
resp = client.post("/cache/purge", data={"csrf": csrf, "stage": "execute"})
assert resp.status_code == 200
assert '<link rel="stylesheet"' in resp.text
assert 'id="action-slot"' in resp.text
# ── wrapper never changes status code, CSRF, or audit behavior ────────────
def test_lock_conflict_status_code_preserved_in_full_page(tmp_path: Path) -> 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("/cache").text)
resp = client.post("/vms/cleanup", data={"csrf": csrf, "stage": "execute"})
assert resp.status_code == 409
assert '<link rel="stylesheet"' in resp.text
assert 'id="action-slot"' in resp.text
def test_lock_conflict_status_code_preserved_bare_for_htmx(tmp_path: Path) -> 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("/cache").text)
resp = client.post(
"/vms/cleanup",
data={"csrf": csrf, "stage": "execute"},
headers={"HX-Request": "true"},
)
assert resp.status_code == 409
assert "<html" not in resp.text.lower()
def test_missing_csrf_still_403s_regardless_of_hx_header(tmp_path: Path) -> 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