Files
FabledGrimoire/tests/test_frontend_no_remote_assets.py
bvandeusenandClaude Opus 5.5 101ab7bc8d fix(design): the app serves its own fonts and names no other host
theme.css loaded Fraunces, Inter and JetBrains Mono from Google Fonts. That sent
every visitor's address to Google on every page load, and an instance with no
internet drew in fallback type. All three families are now vendored under
frontend/src/assets/fonts with every subset Google publishes for them, plus their
OFL licences. The Content-Security-Policy no longer lists fonts.googleapis.com or
fonts.gstatic.com.

A new guard test fails if any stylesheet, page or component links a remote asset
or preconnects to one, or if the CSP names another host.

The theme.css header now describes the sheet as it is instead of how it used to
read.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 11:27:40 -04:00

66 lines
2.9 KiB
Python

"""The app draws itself only from its own origin.
A font, stylesheet or script linked from another host hands that host every
visitor's address on every page load, sign-in page included, and when the host
is slow or blocked the page quietly renders in fallback type with nothing in the
console. Everything the frontend needs ships in the image instead, and the
Content-Security-Policy names no other host, so a remote link that slips back in
is refused by the browser as well as by this check.
"""
from __future__ import annotations
import pathlib
import re
ROOT = pathlib.Path(__file__).resolve().parents[1]
FRONTEND = ROOT / "frontend"
CSS_COMMENT = re.compile(r"/\*.*?\*/", re.S)
HTML_COMMENT = re.compile(r"<!--.*?-->", re.S)
REMOTE = r"""(?:https?:)?//"""
REMOTE_REFS = [
re.compile(rf"""@import\s+(?:url\(\s*)?['"]?{REMOTE}""", re.I),
re.compile(rf"""url\(\s*['"]?{REMOTE}""", re.I),
re.compile(rf"""<link\b[^>]*\bhref\s*=\s*['"]{REMOTE}""", re.I),
re.compile(rf"""<script\b[^>]*\bsrc\s*=\s*['"]{REMOTE}""", re.I),
re.compile(r"""\brel\s*=\s*['"](?:preconnect|dns-prefetch)""", re.I),
]
def remote_refs(text: str) -> list[str]:
"""The remote asset references in a stylesheet, page or component."""
text = HTML_COMMENT.sub("", CSS_COMMENT.sub("", text))
return [m.group(0) for pattern in REMOTE_REFS for m in pattern.finditer(text)]
def frontend_sources() -> dict[str, str]:
paths = [FRONTEND / "index.html"]
for suffix in ("*.css", "*.vue", "*.html"):
paths += (FRONTEND / "src").rglob(suffix)
return {str(p.relative_to(ROOT)): p.read_text() for p in paths}
def test_no_frontend_source_links_a_remote_asset():
sources = frontend_sources()
assert len(sources) > 1, f"no frontend sources under {FRONTEND}"
found = {name: refs for name, text in sources.items() if (refs := remote_refs(text))}
assert found == {}, "vendor the asset and serve it from the app's own origin"
def test_the_content_security_policy_names_no_other_host():
app = (ROOT / "src" / "grimoire" / "app.py").read_text()
policy = re.search(r'"Content-Security-Policy",\s*((?:"[^"]*"\s*)+)\)', app)
assert policy, "the Content-Security-Policy header is no longer set in app.py"
assert "//" not in policy.group(1)
def test_the_guard_can_fail():
assert remote_refs("@import url('https://fonts.example.com/css2?family=X');")
assert remote_refs("@font-face { src: url(//cdn.example.com/x.woff2); }")
assert remote_refs('<link rel="stylesheet" href="https://cdn.example.com/x.css">')
assert remote_refs('<script src="https://cdn.example.com/x.js"></script>')
assert remote_refs('<link rel="preconnect" href="/">')
assert not remote_refs("/* no @import url('https://x.example.com') here */")
assert not remote_refs("@font-face { src: url(./inter-normal-latin.woff2); }")
assert not remote_refs('<a href="https://example.com">docs</a>')