"""The manager page must not build markup out of tenant text (review F-01).
The page is rendered by ``tests/page_dom_probe.js`` (Node) against a recording
DOM, so these tests drive the real inline script instead of matching its source.
The property under test is the rendered result: no markup string may carry the
entry text, and parsing every markup string the page writes must not yield an
event-handler attribute.
Pre-fix (``esc(e.question)`` concatenated into ``value="..."`` and, for the list
view, straight into element content): a question of
``" onfocus="alert(1)" data-x="`` closed the ``value`` attribute and produced an
``onfocus`` handler, and an answer containing ``
`` produced an element with an ``onerror`` handler.
"""
from __future__ import annotations
import json
import shutil
import subprocess
import tempfile
from html.parser import HTMLParser
from pathlib import Path
import pytest
PLUGIN_ROOT = Path(__file__).resolve().parents[1]
PAGE = PLUGIN_ROOT / "components/pages/manager/index.html"
PROBE = Path(__file__).resolve().parent / "page_dom_probe.js"
QUESTION = '" onfocus="alert(1)" data-x="'
ANSWER = 'line1
'
ENTRY = {"id": "e1", "question": QUESTION, "answer": ANSWER}
class _AttributeCollector(HTMLParser):
"""Collect every ``(tag, attribute, value)`` a markup string parses into."""
def __init__(self) -> None:
super().__init__(convert_charrefs=True)
self.attributes: list[tuple[str, str, str]] = []
def handle_starttag(self, tag, attrs) -> None:
for name, value in attrs:
self.attributes.append((tag, name, value if value is not None else ""))
def handle_startendtag(self, tag, attrs) -> None:
self.handle_starttag(tag, attrs)
def _render(drive: str) -> dict:
node = shutil.which("node")
if node is None:
pytest.skip("the page probe needs node to render the page against a recording DOM")
config = {"responses": {"GET /entries": {"entries": [ENTRY]}}, "drive": drive}
with tempfile.TemporaryDirectory() as tmp:
config_path = Path(tmp) / "probe_config.json"
config_path.write_text(json.dumps(config), encoding="utf-8")
completed = subprocess.run(
[node, str(PROBE), str(PAGE), str(config_path)],
capture_output=True,
text=True,
timeout=120,
)
assert completed.returncode == 0, completed.stderr
return json.loads(completed.stdout)
def _rendered_attributes(report: dict) -> list[tuple[str, str, str]]:
parser = _AttributeCollector()
for write in report["markup_writes"]:
parser.feed(write["markup"])
parser.close()
return parser.attributes
def test_entry_text_never_reaches_markup_and_never_becomes_a_handler():
report = _render("await loadEntries();")
assert report["errors"] == []
# 1. Entry text must not appear in any markup string at all: a value that
# never reaches markup cannot open an attribute or a script context.
for write in report["markup_writes"]:
assert QUESTION not in write["markup"]
assert "![]()