stackchain-dashboard/tests/test_security_headers.py
timmy bd637d0141
All checks were successful
CI / lint (pull_request) Successful in 2m12s
CI / build-release (pull_request) Successful in 6s
CI / browser-journey (pull_request) Successful in 1m1s
CI / release-candidate (pull_request) Has been skipped
fix: enable same-origin voice capture (Closes #933)
2026-08-16 03:24:10 +00:00

75 lines
3.0 KiB
Python

import httpx
import pytest
from src import main
REQUIRED_HEADERS = {
"x-content-type-options": "nosniff",
"referrer-policy": "no-referrer",
"x-frame-options": "DENY",
}
def assert_browser_security_boundary(response: httpx.Response) -> None:
for name, value in REQUIRED_HEADERS.items():
assert response.headers[name] == value
permissions_policy = response.headers["permissions-policy"]
assert "camera=()" in permissions_policy
assert "microphone=(self)" in permissions_policy
assert "microphone=()" not in permissions_policy
policy = response.headers["content-security-policy"]
assert "default-src 'self'" in policy
assert "script-src 'self'" in policy
assert "object-src 'none'" in policy
assert "frame-ancestors 'none'" in policy
script_policy = next(part for part in policy.split(";") if "script-src" in part)
assert "'unsafe-inline'" not in script_policy
assert "'unsafe-eval'" not in script_policy
@pytest.mark.anyio
async def test_security_boundary_covers_pages_health_static_and_api_errors(monkeypatch):
monkeypatch.setenv("STACKCHAIN_DASHBOARD_ACCESS_TOKEN", "access-token-with-at-least-thirty-two-characters")
monkeypatch.setenv("STACKCHAIN_DASHBOARD_SESSION_SECRET", "session-secret-with-at-least-thirty-two-characters")
monkeypatch.setenv("STACKCHAIN_DASHBOARD_AUTH_MODE", "operator")
monkeypatch.setenv("STACKCHAIN_DASHBOARD_PUBLIC_ORIGIN", "https://test")
transport = httpx.ASGITransport(app=main.app)
async with httpx.AsyncClient(transport=transport, base_url="https://test") as client:
responses = [
await client.get("/healthz"),
await client.get("/login"),
await client.get("/static/dashboard.js"),
await client.get("/api/v1/context"),
]
assert [response.status_code for response in responses] == [200, 200, 200, 401]
for response in responses:
assert_browser_security_boundary(response)
assert response.headers["strict-transport-security"] == "max-age=31536000"
@pytest.mark.anyio
async def test_security_boundary_covers_fail_closed_authentication_response(monkeypatch):
monkeypatch.delenv("STACKCHAIN_DASHBOARD_ACCESS_TOKEN", raising=False)
monkeypatch.delenv("STACKCHAIN_DASHBOARD_SESSION_SECRET", raising=False)
monkeypatch.delenv("STACKCHAIN_DASHBOARD_AUTH_MODE", raising=False)
transport = httpx.ASGITransport(app=main.app)
async with httpx.AsyncClient(transport=transport, base_url="https://test") as client:
response = await client.get("/login")
assert response.status_code == 503
assert_browser_security_boundary(response)
@pytest.mark.anyio
async def test_insecure_local_mode_does_not_emit_hsts():
transport = httpx.ASGITransport(app=main.app, client=("127.0.0.1", 123))
async with httpx.AsyncClient(transport=transport, base_url="http://localhost") as client:
response = await client.get("/healthz")
assert response.status_code == 200
assert "strict-transport-security" not in response.headers