feat(r2): C3 server side — 204 on an explicit JSON Accept, and back=view
- wants_json: true only for an exact `application/json` entry with q > 0. Absent, empty, wildcard, application/*, near misses, q=0 and malformed headers all fall through to the 303. - The four mark routes share one exit, _mark_done: 204 with no body for the in-place client, otherwise _mark_redirect unchanged. - back=view lands on /b/<name>/view?f=<rel>#rail, only for a media item of this booth. It is built from the resolved rel and never echoed. Anything else takes the no-`back` landing. - tests/golden/r2_mark_303.json: 108 responses recorded from the PRE-R2 code (6 route cases x back absent|marks x 9 non-JSON Accepts), replayed byte for byte (INV-4). Two mutations (q>=0, substring match) turn it red. - The contract now states the q=0 rule.
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -151,3 +151,76 @@ def test_a_look_that_cannot_be_recorded_still_serves_the_page(tmp_path):
|
||||
b.chmod(0o755)
|
||||
assert r.status_code == 200
|
||||
assert not (b / ".seen").exists()
|
||||
|
||||
|
||||
# ---- C3: in-place judgment --------------------------------------------------
|
||||
|
||||
GOLDEN = pathlib.Path(__file__).parent / "golden" / "r2_mark_303.json"
|
||||
|
||||
|
||||
def _seed(root: pathlib.Path) -> TestClient:
|
||||
"""The golden's fixture, byte for byte (see golden_gen in the R2 notes)."""
|
||||
from booth.marks import declare_pick, write_note
|
||||
root.mkdir(parents=True, exist_ok=True)
|
||||
b = _booth(root, "g", {"a.png": PNG, "b b.png": PNG, "c.md": PNG})
|
||||
declare_pick(b, "q", {"prompt": "Which?", "options": ["x", "y"]}, target="a.png")
|
||||
set_flag(b, "a.png", True)
|
||||
write_note(b, "a.png", "seed")
|
||||
return TestClient(create_app(root, ttl_hours=24, start_sweeper=False),
|
||||
follow_redirects=False)
|
||||
|
||||
|
||||
def test_every_pre_r2_request_shape_gets_a_byte_identical_303(tmp_path):
|
||||
"""INV-4. The golden was recorded from the PRE-R2 code: every mark route,
|
||||
with `back` absent and `back=marks`, under nine Accept headers that must
|
||||
NOT count as asking for JSON. Status, every header, and the body must match
|
||||
exactly — the no-JS guarantee lives in these bytes."""
|
||||
import json
|
||||
cases = json.loads(GOLDEN.read_text())
|
||||
assert len(cases) == 108
|
||||
for i, case in enumerate(cases):
|
||||
c = _seed(tmp_path / str(i))
|
||||
headers = {} if case["accept"] is None else {"accept": case["accept"]}
|
||||
r = c.post(case["path"], data=case["form"], headers=headers)
|
||||
got = {"status": r.status_code,
|
||||
"headers": sorted([k.lower(), v] for k, v in r.headers.items()),
|
||||
"body": r.content.decode("latin-1")}
|
||||
want = {k: case[k] for k in ("status", "headers", "body")}
|
||||
assert got == want, (case["path"], case["form"], case["accept"])
|
||||
|
||||
|
||||
@pytest.mark.parametrize("path,form", [
|
||||
("/b/g/answer", {"ask": "q", "choice": "y"}),
|
||||
("/b/g/note", {"target": "a.png", "text": "in place"}),
|
||||
("/b/g/flag", {"target": "b b.png", "on": "1"}),
|
||||
("/b/g/unmark", {"mark": "note-1"}),
|
||||
])
|
||||
@pytest.mark.parametrize("accept", ["application/json", "text/html, application/json;q=0.5"])
|
||||
def test_an_explicit_json_accept_gets_204_and_the_write_still_lands(tmp_path, path, form, accept):
|
||||
"""The in-place path: same write as the form, no redirect, no body."""
|
||||
from booth.marks import marks_for
|
||||
c = _seed(tmp_path)
|
||||
before = [(m.id, m.shape, m.answer, m.text) for m in marks_for(tmp_path / "g")]
|
||||
r = c.post(path, data=form, headers={"accept": accept})
|
||||
assert r.status_code == 204 and r.content == b""
|
||||
assert "location" not in r.headers
|
||||
after = [(m.id, m.shape, m.answer, m.text) for m in marks_for(tmp_path / "g")]
|
||||
assert after != before, "the write must happen exactly as for the form"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("f,landing", [
|
||||
("a.png", "/b/g/view?f=a.png#rail"),
|
||||
("b b.png", "/b/g/view?f=b%20b.png#rail"),
|
||||
("c.md", "/b/g/#item-b%20b.png"), # a doc is not in the review ring
|
||||
("gone.png", "/b/g/#item-b%20b.png"), # not an item
|
||||
("", "/b/g/#item-b%20b.png"),
|
||||
("../../etc/passwd", "/b/g/#item-b%20b.png"),
|
||||
])
|
||||
def test_back_view_lands_on_the_review_only_for_a_media_item(tmp_path, f, landing):
|
||||
"""The JS-off fix for the bounce: a flag set at full size lands back at
|
||||
full size. Anything that is not a media item in this booth falls back to
|
||||
the booth page exactly as a form with no `back` does."""
|
||||
c = _seed(tmp_path)
|
||||
r = c.post("/b/g/flag", data={"target": "b b.png", "on": "1", "back": "view", "f": f})
|
||||
assert r.status_code == 303
|
||||
assert r.headers["location"] == landing
|
||||
|
||||
Reference in New Issue
Block a user