fix: the note form carried no session token, so every note was refused

Tier S (a fix inside a boundary; chaos d8=5, no override). Reported by the
maintainer: "I can't save notes, I get 'refused: no session token'."

The form posted to a bare `/note`. Control 1 requires the token on EVERY
request, so the guard refused all of them.

WHY THE TESTS MISSED IT IS THE PART WORTH RECORDING. I verified the note
channel over real HTTP and got 303 -- but I appended the token to the URL
by hand. I tested the ENDPOINT and not the PATH A PLAYER TAKES, so the one
thing standing between the feature and the user was the one thing not
exercised. Same family as timing the wrong span and counting the wrong
denominator: a correct measurement of the wrong subject.

Fixed with Guard::note_endpoint(), so the form's action carries the token
like every other request. The assertion now pins the token's PRESENCE
rather than the bare path, so reverting the fix turns it red.

Verified the way it should have been done first: read the form's `action`
out of the SERVED page and POST to exactly that, nothing added by hand.
303.

Clippy then flagged document_with_log at 8 arguments. It was right -- the
signature had grown across three passes -- so the two endpoints are now
one `Endpoints` struct rather than an #[allow]. They are one concept: the
guarded surface this page may talk to, one channel that becomes commands
and one that provably cannot.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
tegwick 2026-08-06 15:32:09 +02:00
parent ac57ce2011
commit 5816d334ad
4 changed files with 65 additions and 14 deletions

View file

@ -47,7 +47,14 @@ pub mod input;
pub mod jsrun;
pub mod serve;
pub use doc::{document, text_of};
pub use doc::{document, text_of, Endpoints};
/// The endpoint pair every test in this crate posts to.
#[cfg(test)]
const TEST_ENDPOINTS: Endpoints<'static> = Endpoints {
command: "/command?t=x",
note: "/note?t=x",
};
pub use input::{resolve, Note, PointerFact};
pub use serve::{Guard, Refusal, Request};
@ -186,7 +193,7 @@ mod coverage {
text_of(&document(
view,
&[],
"/command?t=x",
crate::TEST_ENDPOINTS.command,
Some(PlayerId(0)),
false,
))
@ -595,7 +602,7 @@ mod gamelog {
document_with_log(
&crate::testfix::view(Some(PlayerId(0))),
&[],
"/command?t=x",
crate::TEST_ENDPOINTS,
Some(PlayerId(0)),
false,
log,
@ -779,7 +786,7 @@ mod notes {
let html = document_with_log(
&crate::testfix::view(Some(PlayerId(0))),
&[],
"/command?t=x",
crate::TEST_ENDPOINTS,
Some(PlayerId(0)),
false,
&[],
@ -807,13 +814,16 @@ mod notes {
let html = document_with_log(
&crate::testfix::view(Some(PlayerId(0))),
&[],
"/command?t=x",
crate::TEST_ENDPOINTS,
Some(PlayerId(0)),
false,
&[],
&[],
);
assert!(html.contains("action=\"/note\""), "no comment box");
assert!(
html.contains("action=\"/note?t=x\""),
"the form must carry the session token"
);
assert!(
html.contains("method=\"post\""),
"a plain form, so it works with the script disabled"
@ -832,7 +842,7 @@ mod two_columns {
document_with_log(
&crate::testfix::view(Some(PlayerId(0))),
&[],
"/command?t=x",
crate::TEST_ENDPOINTS,
Some(PlayerId(0)),
false,
&[],