Files
brain-of-reese/tests/e2e/test_security_headers.py
ducoterra bef24e05e2
Build and Push Containers / build-and-push-app (push) Successful in 1m54s
Build and Push Containers / build-and-push-db (push) Failing after 13s
phase: 123_chat_image_questions
All gates green. Verification complete.

**Phase 123 — final verification pass (all 4 tasks already in `complete/`)**

- Verified the full implementation is in the working tree: `app/api/chat_images.py` (upload/serve pair), `ChatRequest.image`/`ChatMessage.image` (path-validated, omitted-when-None), toggle-off + stale-file hinted error frames, `build_user_content` multimodal build at both sites (chat.py deflected branch + `run_agent`), config-gated composer attach/preview/upload-then-send, restore + shared rendering, CSP `img-src 'self' data:` carve-out, mock-LLM capture buffer.
- `uv run pytest` → **2796 passed**, exit 0 (unit + integration).
- `uv run pytest --cov=app --cov-report=term-missing` → **TOTAL 99%** (29/4615 missed; phase-123 modules 99–100%).
- `uv run pytest tests/e2e/test_chat_image_questions.py -v --no-cov` → **5 passed** in isolation.
- `uv run ruff check . && uv run pyright` → clean (0 errors).

**Completion criteria:** (1) attach→send→multimodal text+image to the model, bubble/reload/shared all render it, saved chat stores the PATH with `"base64" not in json.dumps(stored)` — **verified** (E2E tests 1–4 + integration round-trip); (2) `BOR_IMAGES=false` — control hidden, exact hinted error frame, zero model calls / no query_log row — **verified** (E2E test 5 + integration); (3) text-only byte-identical (`content` stays a plain `str`) — **verified** (unit + integration); (4) all gates green — **verified**; (5) commit + phase move — left to the harness per pipeline rules (no `git add`/`commit` run).

No defects found; no live-infrastructure changes (repo + local dev DB only). **Next pending phase: none** — 123 is the last phase in `todo/`.
2026-09-25 05:19:18 -04:00

134 lines
5.6 KiB
Python

"""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>`` (``html { background: var(--bg) }``) and keeps ``<body>``
transparent on purpose (the z-index:-1 grid layer must show). The
stylesheet-loaded-under-the-CSP contract is therefore pinned on the
``<html>`` 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 ``<html>`` (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 <main> 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}"