Files
FabledSteward/tests/test_inline_handlers.py
T
bvandeusenandClaude Opus 5.5 531a193bd1
CI / lint (push) Successful in 3s
CI / integration (push) Failing after 54s
CI / unit (push) Successful in 1m34s
CI / publish (push) Skipped
feat(alerts): history reads as episodes; tests follow the confirm dialog (#5316, #5314)
History (#5316):
- Transitions fold into episodes (steward/alerts/episodes.py): each stretch
  out of quiet, the worst state it reached, how long it lasted, its worst
  reading against the rule ('3m 21s (fires above 3m)'), who was told across
  its changes, and its Ansible runs. The changes themselves fold under it.
- An episode cut by paging says so ('cleared later (above)', 'earlier').
- For one rule: a verdict ('Fired 4 times in the last 7d, 1h 2m in all') and
  no Rule column. Notified and Action show only when the range has any.

Tests:
- The confirm guard looks for a confirm( call, not the word in the comment
  that says it is gone (lesson #4365).
- The Hosts viewer test looks for the add-host dialog by id, since every page
  now carries the confirm dialog.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 23:02:01 -04:00

52 lines
2.6 KiB
Python

"""No template puts a rendered value into inline JavaScript (#5099).
An onclick/onsubmit attribute is HTML-decoded before its JavaScript is
parsed, so autoescaping does not protect a name placed inside it: a monitor
called x');fetch(…);// ran as script when an admin clicked Delete. A
prompt goes in data-confirm="…" (base.html asks with it); any other value a
handler needs is an integer, rendered with |int.
"""
import re
from tests._templates import REPO as _REPO, template_files as _templates
_HANDLER = re.compile(r"""\bon[a-z]+\s*=\s*("[^"]*"|'[^']*')""", re.I)
_VALUE = re.compile(r"\{\{(.*?)\}\}", re.S)
def unsafe_handler_values(text: str) -> list[str]:
"""Each {{ … }} inside an inline handler that is not an |int."""
return [m.group(0) for h in _HANDLER.finditer(text) for m in _VALUE.finditer(h.group(1))
if not re.search(r"\|\s*int\s*$", m.group(1).strip())]
def test_the_guard_can_fail():
page = """
<button onclick="return confirm('Delete rule {{ rule.name }}?')">x</button>
<form onsubmit='return confirm({{ msg|tojson }})'>
<button onclick="toggle({{ idx|int }})">ok</button>
<button data-confirm="Delete {{ rule.name }}?" data-range="{{ key }}">ok</button>
"""
assert unsafe_handler_values(page) == ["{{ rule.name }}", "{{ msg|tojson }}"]
def test_no_template_renders_a_value_into_an_inline_handler():
files = _templates()
assert len(files) > 100
found = {str(p.relative_to(_REPO)): hits for p in files if (hits := unsafe_handler_values(p.read_text()))}
assert not found, f"use data-confirm, or render an integer with |int: {found}"
def test_base_asks_before_a_data_confirm_form_submits():
"""#5314: in Steward's own dialog, filled as text, never the browser's confirm()."""
base = (_REPO / "steward" / "templates" / "base.html").read_text()
js = (_REPO / "steward" / "static" / "confirm.js").read_text()
assert "filename='confirm.js'" in base and '<dialog id="confirm-dialog"' in base
assert '<form method="dialog">' in base
assert re.search(r'<button type="submit"[^>]*value="back"[^>]*autofocus>', base) # Cancel has the focus
assert re.search(r'<button type="submit"[^>]*btn-danger[^>]*value="go"', base)
assert "dataset.confirm" in js and "getElementById('confirm-dialog')" in js and "requestSubmit" in js
assert "textContent = title" in js and "innerHTML" not in js # text, never markup
assert not re.search(r"\bconfirm\s*\(", js + base) # a call, not the comment saying it is gone
assert any("data-confirm=" in p.read_text() for p in _templates())