119 lines
4.0 KiB
Python
119 lines
4.0 KiB
Python
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": "same-origin",
|
|
"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
|