diff --git a/crates/cb-render-html/src/doc.rs b/crates/cb-render-html/src/doc.rs index 7995e81..cb3f9ca 100644 --- a/crates/cb-render-html/src/doc.rs +++ b/crates/cb-render-html/src/doc.rs @@ -698,19 +698,44 @@ pub fn document( seat: Option, may_pass: bool, ) -> String { - document_with_log(view, legal, endpoint, seat, may_pass, &[], &[]) + document_with_log( + view, + legal, + Endpoints { + command: endpoint, + note: "/note", + }, + seat, + may_pass, + &[], + &[], + ) } /// The table, plus the game log (CB-WP-0018 T02). +/// Where the page posts, both channels (ADR-0014 D1). +/// +/// One struct rather than two `&str` parameters because they are one +/// concept — the guarded surface this page may talk to — and because +/// clippy was right that the signature had grown across three passes. +#[derive(Debug, Clone, Copy)] +pub struct Endpoints<'a> { + /// Pointer facts. `resolve` turns these into commands. + pub command: &'a str, + /// Free text. Nothing turns these into commands. + pub note: &'a str, +} + pub fn document_with_log( view: &GroundView, legal: &[games_ground::GroundCommand], - endpoint: &str, + to: Endpoints<'_>, seat: Option, may_pass: bool, log: &[LogLine], meta: &[String], ) -> String { + let (endpoint, note_to) = (to.command, to.note); let mut s = String::with_capacity(8192); let _ = write!( s, @@ -743,7 +768,7 @@ pub fn document_with_log( body(&mut s, view); move_section(&mut s, legal, seat, may_pass); s.push_str("
"); - meta_section(&mut s, meta); + meta_section(&mut s, meta, note_to); log_section(&mut s, log); s.push_str("
"); let _ = write!( @@ -761,7 +786,7 @@ pub fn document_with_log( /// /// Empty is a legitimate state — a first game has no tally and may have no /// notes — and renders as nothing rather than as an empty heading. -fn meta_section(s: &mut String, meta: &[String]) { +fn meta_section(s: &mut String, meta: &[String], note_to: &str) { // CB-WP-0027 T03: the comment box. Always present — the panel's // purpose is that a player can say something at any moment, and a box // that appears only sometimes trains them not to look for it. @@ -770,12 +795,14 @@ fn meta_section(s: &mut String, meta: &[String]) { // The command channel needs JavaScript because a drag is not a form // submission; a comment is, and making it depend on the script would // add a failure mode for no gain. - s.push_str( + let _ = write!( + s, "

what are you thinking?

\ -
\ + \ \
", + note_to = esc(note_to), ); if meta.is_empty() { return; diff --git a/crates/cb-render-html/src/lib.rs b/crates/cb-render-html/src/lib.rs index a460ab4..2885d15 100644 --- a/crates/cb-render-html/src/lib.rs +++ b/crates/cb-render-html/src/lib.rs @@ -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, &[], diff --git a/crates/cb-render-html/src/serve.rs b/crates/cb-render-html/src/serve.rs index dfe464d..e4f3169 100644 --- a/crates/cb-render-html/src/serve.rs +++ b/crates/cb-render-html/src/serve.rs @@ -158,6 +158,17 @@ impl Guard { format!("/command?t={}", self.token) } + /// The note channel's endpoint, token and all (CB-WP-0027). + /// + /// **A form's `action` must carry the token like every other + /// request.** The first version posted to a bare `/note` and was + /// refused with "no session token" — the endpoint had been tested + /// with a token appended by hand, so the test exercised the mechanism + /// and not the path a player takes. + pub fn note_endpoint(&self) -> String { + format!("/note?t={}", self.token) + } + pub fn page_url(&self) -> String { format!("{}/?t={}", self.origin, self.token) } diff --git a/tools/cb-play/src/hotseat.rs b/tools/cb-play/src/hotseat.rs index d853c3f..3513935 100644 --- a/tools/cb-play/src/hotseat.rs +++ b/tools/cb-play/src/hotseat.rs @@ -243,7 +243,10 @@ impl Server { let page = cb_render_html::doc::document_with_log( &view, legal, - &self.guard.endpoint(), + cb_render_html::Endpoints { + command: &self.guard.endpoint(), + note: &self.guard.note_endpoint(), + }, Some(seat), may_pass, &self.log_lines(),