agent-market/tests/test_theme.py

45 lines
3.1 KiB
Python

#!/usr/bin/env python3
"""Unit checks for theme validation and the customCss sanitiser."""
import os, sys
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))
from market.theme import sanitize_css, sanitize_patch, build_theme_action, PRESETS
fails = 0
def check(name, cond):
global fails
fails += not cond
print(("PASS " if cond else "FAIL ") + name)
evil = ("@import url(https://evil.example/x.css); .card{color:red!important;position:fixed;z-index:99;--p-ink:transparent;"
"background:url(https://evil.example/t.png)} body{display:none} :root{--acc:red} h1{background:url(https://images.unsplash.com/a.jpg);margin:-500px}"
" <script>alert(1)</script> .x{color:expression(alert(1))} .y{behavior:url(x.htc)} .z::before{content:'x'}"
" .t{transform:scale(0)} .k{color:blue}")
out = sanitize_css(evil)
check("css: @import stripped", "@" not in out and "evil.example/x.css" not in out)
check("css: !important stripped", "important" not in out)
check("css: position/z-index/content/transform dropped", not any(k in out for k in ("position", "z-index", "content", "transform")))
check("css: custom properties dropped", "--" not in out)
check("css: non-allowlisted url() neutralised", "evil.example" not in out and "url(https://images.unsplash.com/a.jpg)" in out)
check("css: body/:root selectors dropped", "body" not in out and ":root" not in out)
check("css: script/expression/behavior gone", not any(k in out.lower() for k in ("<script", "expression", "behavior", "alert(1)")))
check("css: negative margins dropped", "-500px" not in out)
check("css: harmless rule kept", ".k{color:blue}" in out)
p = sanitize_patch({"mode": "light", "palette": {"accent": "green", "text": "red;}body{x"}, "font": "comic sans", "fontScale": 9,
"radius": -5, "columns": 12, "layout": "list", "chat": {"position": "left", "width": 5000},
"copy": {"headline": "<b>Hi</b> there", "chips": ["Bikes", {"label": "Garden", "say": "garden stuff"}]},
"sections": {"hidden": ["bullets", "nope"], "order": ["results", "hero"]}, "evil": "x",
"background": {"kind": "image", "url": "https://evil.example/bg.jpg"}})
check("patch: bad colour dropped, good kept", p["palette"] == {"accent": "green"})
check("patch: font alias resolved", p["font"] == "Comic Neue")
check("patch: numbers clamped", p["fontScale"] == 1.75 and p["radius"] == 0 and p["columns"] == 6 and p["chat"]["width"] == 600)
check("patch: html stripped from copy", "<" not in p["copy"]["headline"])
check("patch: sections validated", p["sections"]["hidden"] == ["bullets"] and p["sections"]["order"][:2] == ["results", "hero"])
check("patch: unknown keys and non-allowlisted image dropped", "evil" not in p and "background" not in p)
a = build_theme_action({"preset": "Retro"})
check("preset: retro expands", a["preset"] == "retro" and a["base"]["font"] == "VT323")
check("preset: alias", build_theme_action({"preset": "cyberpunk"})["preset"] == "neon")
check("reset action", build_theme_action({"reset": True})["reset"] is True)
check("all presets validate cleanly", all(sanitize_patch(v) for v in PRESETS.values()))
sys.exit(1 if fails else 0)