Files
PacketStation/tests/server/test_app.py
T

119 lines
4.0 KiB
Python
Raw Normal View History

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
# /_blob needs a session first (phase 2), so an anonymous call is refused, never the SPA page
assert client.get("/_blob/abc").status_code in (401, 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("<html>INDEX</html>")
(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 == "<html>INDEX</html>"
assert c.get("/assets/app.js").text == "console.log(1)"
assert c.get("/warehouse/board").text == "<html>INDEX</html>"
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