"""Phase 82 E2E (Playwright): the security headers reach a real browser and the strict CSP does not break the page. Story: n/a — security hardening, audit SEC-04 (``.agents/remediation_plan.md``). Run in isolation (DB must be up: ``podman compose up -d db``): uv run pytest tests/e2e/test_security_headers.py -v --no-cov No LLM interaction: the page load IS the contract. The suite never touches the chat stream and never depends on the aipi endpoint — the shared ``app_server`` fixture's deterministic LOCAL mock LLM is simply unused. Test → task mapping (phase 82, task 03): 1. ``test_chat_page_navigation_headers_and_csp_clean`` — a real ``page.goto`` of the chat shell (``/``): the navigation response carries all three headers (CSP exactly the A1 string), the page painted under the CSP (the dark-tech canvas applied → the stylesheet loaded), the shell's ``#main`` booted, and zero console CSP violations. 2. ``test_sources_page_navigation_headers_and_csp_clean`` — the same header + no-violation contract for the second page, ``/sources.html``. Paint-check note (vs. the task text): the task suggested asserting the ``body`` background-color is not transparent, but the pinned design contract (phase 08, ``test_dark_tech_theme.py``) puts the visible canvas on ```` (``html { background: var(--bg) }``) and keeps ```` transparent on purpose (the z-index:-1 grid layer must show). The stylesheet-loaded-under-the-CSP contract is therefore pinned on the ```` canvas color — the same value phase 08 pins. """ from __future__ import annotations import re from playwright.sync_api import ConsoleMessage, Page, expect #: The exact owner-approved policy (phase 82, decision A1), with the #: phase-123 img-src carve-out (the question-image composer's data-URL #: preview + live bubble — ``data:`` is allowed for images ONLY). CSP = ( "default-src 'self'; base-uri 'none'; frame-ancestors 'none'; " "img-src 'self' data:" ) #: Chromium reports CSP denials to the console with this phrasing #: ("Refused to … because it violates the following Content Security #: Policy directive …" / "The Content Security Policy directive … #: prevents execution …"). _CSP_CONSOLE = re.compile(r"Content Security Policy", re.IGNORECASE) #: The dark-tech canvas — ``frontend/assets/styles.css`` ``--bg: #0f0a0a``, #: pinned by phase 08 (``test_dark_tech_theme.py``). CANVAS_BG = "rgb(15, 10, 10)" def _attach_csp_watch(page: Page) -> list[str]: """Record every console message that mentions a CSP denial. Attached BEFORE navigation (the contract — denials of the main document's resources are logged while it loads). Returns the live list; assert it is empty after the load. """ violations: list[str] = [] def _on_console(msg: ConsoleMessage) -> None: if _CSP_CONSOLE.search(msg.text): violations.append(msg.text) page.on("console", _on_console) return violations def _assert_navigation_headers(page: Page, url: str) -> None: """A real navigation: the response carries all three headers, CSP exact.""" response = page.goto(url) assert response is not None, f"no navigation response for {url}" assert response.status == 200, f"{url} → {response.status}" headers = response.headers # lowercase keys assert headers.get("content-security-policy") == CSP, ( f"{url}: CSP is {headers.get('content-security-policy')!r}, " f"expected the exact A1 string {CSP!r}" ) assert headers.get("x-frame-options") == "DENY", ( f"{url}: X-Frame-Options is {headers.get('x-frame-options')!r}" ) assert headers.get("x-content-type-options") == "nosniff", ( f"{url}: X-Content-Type-Options is {headers.get('x-content-type-options')!r}" ) def _assert_chat_page_painted(page: Page) -> None: """The page painted under the CSP — styles loaded, shell booted. The visible canvas is ```` (see the module docstring): its computed color can only be the dark-tech palette if ``/assets/*.css`` was fetched and applied under ``default-src 'self'`` — a CSP-broken document would be a white/default canvas without ``#main`` styled. """ bg = page.evaluate( "() => getComputedStyle(document.documentElement).backgroundColor" ) assert bg == CANVAS_BG, ( f"dark-tech canvas not applied under the CSP (stylesheet not loaded?): {bg}" ) expect(page.locator("#main")).to_be_attached() box = page.locator("#main").bounding_box() assert box is not None and box["width"] > 0 and box["height"] > 0, ( "shell
is present but invisible/empty — the page did not boot" ) def test_chat_page_navigation_headers_and_csp_clean( page: Page, app_url: str, db_ready: None ) -> None: """AC1: real navigation of the chat shell → three headers (CSP exact), painted page, zero CSP violations on the console.""" violations = _attach_csp_watch(page) _assert_navigation_headers(page, app_url + "/") _assert_chat_page_painted(page) # Console delivery is async — let pending messages settle before the # zero-violation gate (the load event already fired). page.wait_for_timeout(250) assert violations == [], f"CSP violations on the chat page: {violations}" def test_sources_page_navigation_headers_and_csp_clean( page: Page, app_url: str, db_ready: None ) -> None: """AC2: the second page (Sources) — same three headers, no CSP violations.""" violations = _attach_csp_watch(page) _assert_navigation_headers(page, app_url + "/sources.html") page.wait_for_timeout(250) assert violations == [], f"CSP violations on the sources page: {violations}"