"""Frontend-Security Tests: XSS-mitigation, JWT-handling, CSP-middleware. These tests enforce the rules from architecture Section 13 + task graph R-5: - R-5: No `x-html` attribute anywhere in any HTML template (only x-text). - R-1: JWT must be read from / written to localStorage (not cookies, not sessionStorage). - 13.3: CSP-Header is configured in app/main.py (security_headers_middleware), not in a reverse-proxy config file. """ from __future__ import annotations import re from pathlib import Path import pytest ROOT = Path(__file__).resolve().parent.parent WEBUI = ROOT / "app" / "webui" MAIN_PY = ROOT / "app" / "main.py" # --------------------------------------------------------------------------- # R-5: no x-html in any HTML template # --------------------------------------------------------------------------- # Match `x-html` as a whole word (attribute, not part of `x-htmlSomething`). # We deliberately allow `x-html="..."` to fail loudly, and we also catch # any camel-cased variant like `xHtml` for paranoia. XHTML_PATTERN = re.compile(r"\bx[-_]?html\b", re.IGNORECASE) def _all_html_files() -> list[Path]: return sorted(WEBUI.glob("*.html")) def test_no_x_html_in_alpine_templates(): """R-5: no `x-html` attribute anywhere in any HTML file.""" offenders: list[tuple[str, int, str]] = [] for path in _all_html_files(): for lineno, line in enumerate(path.read_text(encoding="utf-8").splitlines(), 1): # Strip HTML comments to avoid false positives in documentation stripped = re.sub(r"", "", line) if XHTML_PATTERN.search(stripped): offenders.append((path.name, lineno, line.strip()[:120])) assert not offenders, ( "x-html usage is forbidden (R-5); offenders:\n" + "\n".join(f" {n}:{ln}: {snippet}" for n, ln, snippet in offenders) ) def test_no_innerHTML_in_alpine_pages(): """Defence in depth: also flag direct `innerHTML` assignments.""" for path in _all_html_files(): content = path.read_text(encoding="utf-8") # allow within