from __future__ import annotations from pathlib import Path import pytest from fastapi.testclient import TestClient from pshelpers import make_settings from packetstation import security from packetstation.main import PLACEHOLDER, create_app @pytest.fixture def client(tmp_path: Path, dev_visual: None) -> TestClient: return TestClient(create_app(make_settings(tmp_path, ps_build="1.2.3-test"))) EXPECTED = { "strict-transport-security": "max-age=31536000; includeSubDomains", "x-content-type-options": "nosniff", "x-frame-options": "DENY", "referrer-policy": "no-referrer", "permissions-policy": "camera=(), microphone=(), geolocation=()", "cross-origin-opener-policy": "same-origin", } def _assert_headers(r) -> None: # type: ignore[no-untyped-def] for k, v in EXPECTED.items(): assert r.headers.get(k) == v, k csp = r.headers["content-security-policy"] for part in ( "default-src 'self'", "script-src 'self'", "style-src 'self' 'unsafe-inline'", "img-src 'self' data: blob:", "font-src 'self'", "connect-src 'self'", "worker-src 'self' blob:", "frame-ancestors 'none'", "base-uri 'self'", "form-action 'self'", ): assert part in csp assert csp == security.CSP def test_healthz(client: TestClient) -> None: r = client.get("/healthz") assert r.status_code == 200 assert r.json() == {"ok": True, "build": "1.2.3-test"} _assert_headers(r) assert r.headers.get("cache-control") != "no-store" def test_version(client: TestClient) -> None: r = client.get("/api/version") assert r.json() == {"build": "1.2.3-test", "stack": "ci", "site_tz": "America/Denver"} _assert_headers(r) assert r.headers["cache-control"] == "no-store" def test_db_health_503_without_database(client: TestClient) -> None: r = client.get("/api/db-health") assert r.status_code == 503 assert r.json()["ok"] is False assert r.headers["cache-control"] == "no-store" def test_placeholder_when_static_missing(client: TestClient) -> None: for path in ("/", "/clerical/packet/12"): r = client.get(path) assert r.status_code == 200 and r.text == PLACEHOLDER _assert_headers(r) def test_api_and_blob_never_fall_back(client: TestClient) -> None: assert client.get("/api/nope").status_code == 404 assert client.get("/_blob/abc").status_code == 404 def test_static_and_spa_fallback(tmp_path: Path, dev_visual: None) -> None: static = tmp_path / "static" (static / "assets").mkdir(parents=True) (static / "index.html").write_text("INDEX") (static / "assets" / "app.js").write_text("console.log(1)") (tmp_path / "secret.txt").write_text("outside") c = TestClient(create_app(make_settings(tmp_path, static_dir=static))) assert c.get("/").text == "INDEX" assert c.get("/assets/app.js").text == "console.log(1)" assert c.get("/warehouse/board").text == "INDEX" r = c.get("/..%2fsecret.txt") assert "outside" not in r.text def test_request_log_line(client: TestClient, capsys: pytest.CaptureFixture[str]) -> None: import json from packetstation import logs logs.setup("INFO") client.get("/api/version?token=abc") lines = [json.loads(x) for x in capsys.readouterr().out.splitlines() if '"request"' in x] rec = lines[-1] assert rec["method"] == "GET" and rec["path"] == "/api/version" and rec["status"] == 200 assert rec["build"] == "1.2.3-test" and "ms" in rec and "ip" in rec assert "token" not in json.dumps(rec) def test_lazy_module_app(dev_visual: None, monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None: import packetstation.main as m monkeypatch.setenv("STATIC_DIR", str(tmp_path / "none")) monkeypatch.setattr(m, "_app", None) app = m.app assert app is m.app assert TestClient(app).get("/healthz").json()["ok"] is True