"""Unit tests for the security headers (PLAN.md ยง11, A12). These assert the *values*, not just their presence: a header that silently weakened (a `unsafe-inline`, a `*`, a reporting endpoint) is a security regression dressed up as a passing test. """ from __future__ import annotations import pytest from starlette.responses import JSONResponse, PlainTextResponse, Response from undercooked.server.headers import ( CONTENT_SECURITY_POLICY, SECURITY_HEADERS, SecurityHeadersMiddleware, apply_security_headers, ) pytestmark = pytest.mark.unit EXPECTED = { "Content-Security-Policy": CONTENT_SECURITY_POLICY, "X-Content-Type-Options": "nosniff", "Referrer-Policy": "no-referrer", "X-Frame-Options": "DENY", "Permissions-Policy": "camera=(), microphone=(), geolocation=()", } def test_the_five_documented_headers_are_defined() -> None: assert dict(SECURITY_HEADERS) == EXPECTED def test_apply_adds_every_header_to_a_response() -> None: response = PlainTextResponse("hi") apply_security_headers(response) for header, value in EXPECTED.items(): assert response.headers[header] == value def test_apply_works_on_json_and_empty_bodies() -> None: payload = JSONResponse({"a": 1}) apply_security_headers(payload) assert payload.headers["content-security-policy"] == CONTENT_SECURITY_POLICY empty = Response(status_code=204) apply_security_headers(empty) assert empty.headers["x-content-type-options"] == "nosniff" def test_apply_is_idempotent_and_overrides_weaker_values() -> None: response = PlainTextResponse("hi", headers={"X-Frame-Options": "SAMEORIGIN"}) apply_security_headers(response) apply_security_headers(response) assert response.headers["x-frame-options"] == "DENY" def _csp() -> dict[str, str]: """The CSP as `directive -> whole clause`, so assertions read like the header.""" return {part.split(" ", 1)[0]: part for part in CONTENT_SECURITY_POLICY.split("; ")} def test_csp_locks_scripts_and_styles_to_self() -> None: directives = _csp() assert directives["default-src"] == "default-src 'self'" assert directives["script-src"] == "script-src 'self'" assert directives["style-src"] == "style-src 'self'" assert directives["font-src"] == "font-src 'self'" assert directives["base-uri"] == "base-uri 'none'" assert directives["frame-ancestors"] == "frame-ancestors 'none'" def test_csp_allows_only_same_origin_websockets_and_data_images() -> None: directives = _csp() assert directives["connect-src"] == "connect-src 'self' ws: wss:" assert directives["img-src"] == "img-src 'self' data:" @pytest.mark.parametrize( "forbidden", ["unsafe-inline", "unsafe-eval", " https://", "http://", "*", "report-uri"] ) def test_csp_contains_no_unsafe_source_or_remote_endpoint(forbidden: str) -> None: assert forbidden not in CONTENT_SECURITY_POLICY def test_no_header_points_at_a_remote_reporting_service() -> None: """`Report-To`/`report-uri` would phone a third party: A12 forbids it.""" assert not any(name.lower() in {"report-to", "nel"} for name in SECURITY_HEADERS) for value in SECURITY_HEADERS.values(): assert "://" not in value assert "report" not in value.lower() def test_headers_are_immutable() -> None: with pytest.raises(TypeError): SECURITY_HEADERS["X-Frame-Options"] = "ALLOWALL" # type: ignore[index] def test_middleware_is_constructible_with_an_app_stub() -> None: async def stub_app(scope: object, receive: object, send: object) -> None: # pragma: no cover return None middleware = SecurityHeadersMiddleware(stub_app) assert middleware.app is stub_app