fix(booth): the reveal button was inert; add kept-lane wipe and in-booth keep
Three operator reports, one of them a real defect I had claimed was working. THE REVEAL BUTTON DID NOTHING, for a day. Its handler sat after the content block's closing tag, and a child template's out-of-block content is silently DISCARDED by Jinja. The button rendered. The handler never reached the browser. Two commits and a README paragraph said click-to-reveal worked, and the suite passed the entire time because nothing asserted against the SERVED page -- the template really did contain the code. Two guards, both confirmed to FAIL when the defect is reintroduced rather than merely added and assumed protective: * test_reveal_handler_actually_reaches_the_served_page greps the response * test_no_orphaned_markup_after_the_content_block guards the structure While moving it, caught a second instance of the same class: the explanatory comment I wrote for the fix contained a literal Jinja endblock tag, which Jinja would have parsed as a real tag and used to close the block early. KEPT-LANE ×. Wiping a kept booth required release-then-find-it-in-the-other- lane. That protected nothing and cost a hunt -- the board you just released is loose in a feed that turns over, and you have to go find it to finish a job you had already decided on. Protection now lives in the confirmation, which names the booth and says KEPT, instead of in the number of lanes you must traverse. Release stays as the reversible option. IN-BOOTH KEEP. `☆ keep` / `★ kept — release` beside "Wipe now", so promoting does not mean navigating back to the index. The booth page did not previously know its own kept state; it does now. Both post a `next` field to stay put -- and `next` is a form field, so it is attacker-controlled: only same-site absolute paths are honoured, with `//host`, schemes and backslashes refused, tested. 173 tests pass.
This commit is contained in:
@@ -1487,3 +1487,95 @@ def test_blur_toggle_posts_the_opposite_state(client):
|
||||
body = c.get("/b/bo/").text
|
||||
assert 'name="on" value="0"' in body, "a blurred item must offer un-blur"
|
||||
assert "◉ blurred" in body
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Operator-reported, 2026-09-21: "the reveal button doesn't do anything".
|
||||
# It rendered and was inert — the handler sat after {%- endblock -%} in a child
|
||||
# template, which Jinja DISCARDS. Two commits and a README claimed
|
||||
# click-to-reveal worked. The suite passed the whole time because nothing
|
||||
# asserted against the SERVED page.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_reveal_handler_actually_reaches_the_served_page(client):
|
||||
"""The regression that matters. Assert the handler is IN THE RESPONSE, not
|
||||
that the template file contains the text — the template contained it fine
|
||||
and the browser never saw it."""
|
||||
c, root = client
|
||||
d = root / "bo"
|
||||
d.mkdir()
|
||||
_png(d / "x.png")
|
||||
set_blurred(d, "x.png", True)
|
||||
|
||||
body = c.get("/b/bo/").text
|
||||
assert 'class="reveal"' in body, "the button must render"
|
||||
assert "classList.toggle('revealed')" in body, (
|
||||
"the handler must reach the page — a button with no handler is a dead "
|
||||
"control, which is exactly what shipped"
|
||||
)
|
||||
|
||||
|
||||
def test_no_orphaned_markup_after_the_content_block(client):
|
||||
"""Structural guard for the same defect class: anything a child template
|
||||
puts outside a block is silently dropped, so the only safe number of
|
||||
closing content-block tags is one, at the very end."""
|
||||
tpl = (pathlib.Path(__file__).parent.parent
|
||||
/ "booth" / "templates" / "booth.html").read_text()
|
||||
after = tpl[tpl.rindex("{% endblock %}") + len("{% endblock %}"):]
|
||||
assert after.strip() == "", (
|
||||
f"content after the final endblock is discarded by Jinja: {after[:120]!r}"
|
||||
)
|
||||
|
||||
|
||||
def test_booth_page_offers_keep_when_ephemeral_and_release_when_kept(client):
|
||||
"""Operator: 'adding a keep button inside a booth'. Both states, because a
|
||||
control that always says the same thing cannot be driving off real state."""
|
||||
c, root = client
|
||||
d = root / "bo"
|
||||
d.mkdir()
|
||||
_png(d / "x.png")
|
||||
|
||||
body = c.get("/b/bo/").text
|
||||
assert "☆ keep" in body and "release" not in body.split("boothhead")[1][:900]
|
||||
|
||||
c.post("/b/bo/keep", data={"next": "/b/bo/"}, follow_redirects=False)
|
||||
body = c.get("/b/bo/").text
|
||||
assert "★ kept — release" in body
|
||||
|
||||
|
||||
def test_keep_from_inside_a_booth_stays_on_the_booth_page(client):
|
||||
"""Without `next` the route redirects to /, which throws you out of the
|
||||
booth you were reading."""
|
||||
c, root = client
|
||||
(root / "bo").mkdir()
|
||||
r = c.post("/b/bo/keep", data={"next": "/b/bo/"}, follow_redirects=False)
|
||||
assert r.headers["location"] == "/b/bo/"
|
||||
r = c.post("/b/bo/keep", follow_redirects=False) # no next
|
||||
assert r.headers["location"] == "/"
|
||||
|
||||
|
||||
def test_next_refuses_an_open_redirect(client):
|
||||
"""`next` comes from a form field, so it is attacker-controlled input."""
|
||||
c, root = client
|
||||
(root / "bo").mkdir()
|
||||
for evil in ("//evil.example/x", "https://evil.example/x", "\\\\evil"):
|
||||
r = c.post("/b/bo/keep", data={"next": evil}, follow_redirects=False)
|
||||
assert r.headers["location"] == "/", f"{evil!r} must not be honoured"
|
||||
|
||||
|
||||
def test_kept_lane_offers_a_direct_wipe_beside_release(client):
|
||||
"""Operator: 'allow an x to delete next to release so i don't have to
|
||||
release and then find it to delete it.'"""
|
||||
c, root = client
|
||||
d = root / "bo"
|
||||
d.mkdir()
|
||||
_png(d / "x.png")
|
||||
(d / KEEP_MARKER).touch()
|
||||
|
||||
body = c.get("/").text
|
||||
assert 'class="wipe wipe-kept"' in body, "kept card must offer a direct ×"
|
||||
assert 'action="/b/bo/unkeep"' in body, "release must still be there too"
|
||||
# ...and it actually wipes.
|
||||
c.post("/b/bo/delete", follow_redirects=False)
|
||||
assert not d.exists()
|
||||
|
||||
Reference in New Issue
Block a user