From 2fe2e8cdbc3bb520994ff0944188f2fa657c546a Mon Sep 17 00:00:00 2001 From: salmonik Date: Sun, 1 Nov 2026 17:49:08 +0100 Subject: v4.0.0: require HELLO MAJOR.MINOR lockstep before auth --- tests/server_core.rs | 100 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) (limited to 'tests/server_core.rs') diff --git a/tests/server_core.rs b/tests/server_core.rs index 6d6cffd..e2600e4 100644 --- a/tests/server_core.rs +++ b/tests/server_core.rs @@ -100,7 +100,16 @@ fn crash_state(response: &str) -> CrashState { serde_json::from_str(raw).expect("valid crash state") } +fn hello(core: &mut ServerCore, state: &mut ConnectionState) { + let response = core.handle_message( + state, + &format!("HELLO {}", zagan_bet::version::VERSION), + ); + assert!(response.starts_with("HELLO "), "handshake failed: {response}"); +} + fn register(core: &mut ServerCore, state: &mut ConnectionState, user: &str) { + hello(core, state); assert_eq!( core.handle_message(state, &format!("REGISTER {user} credential")), format!("REGISTERED {user}") @@ -108,6 +117,7 @@ fn register(core: &mut ServerCore, state: &mut ConnectionState, user: &str) { } fn login(core: &mut ServerCore, state: &mut ConnectionState, user: &str) { + hello(core, state); assert_eq!( core.handle_message(state, &format!("AUTH {user} credential")), format!("OK {user}") @@ -155,6 +165,7 @@ fn legacy_sha256_rows_are_upgraded_on_login() { database::add_user(&conn, "legacy", "deadbeef").unwrap(); let mut state = ConnectionState::default(); + hello(&mut core, &mut state); assert_eq!( core.handle_message(&mut state, "AUTH legacy deadbeef"), "OK legacy" @@ -165,6 +176,7 @@ fn legacy_sha256_rows_are_upgraded_on_login() { assert!(bcrypt::verify("deadbeef", &stored).unwrap()); let mut other = ConnectionState::default(); + hello(&mut core, &mut other); assert_eq!( core.handle_message(&mut other, "AUTH legacy wrong"), "ERROR Invalid credentials" @@ -876,6 +888,7 @@ fn an_insufficient_bet_leaves_no_reservation() { fn history_requires_login_and_valid_counts() { let (mut core, _path) = setup("history-guards", WIN3); let mut state = ConnectionState::default(); + hello(&mut core, &mut state); assert_eq!( core.handle_message(&mut state, "HISTORY"), @@ -928,3 +941,90 @@ fn history_lists_recent_plays_newest_first() { // Newest first: the timestamps must not increase across the list. assert!(records[0].at >= records[1].at); } + +#[test] +fn a_fresh_connection_must_greet_before_anything_else() { + let (mut core, _path) = setup("hello-gate", WIN3); + let mut state = ConnectionState::default(); + + assert_eq!( + core.handle_message(&mut state, "PING"), + "ERROR HELLO required" + ); + assert_eq!( + core.handle_message(&mut state, "AUTH alice x"), + "ERROR HELLO required" + ); + assert_eq!( + core.handle_message(&mut state, "PLAY_START slots 10"), + "ERROR HELLO required" + ); + assert!(!state.greeted()); +} + +#[test] +fn hello_reports_the_version_and_download_url() { + let (mut core, _path) = setup("hello-ok", WIN3); + let mut state = ConnectionState::default(); + + let response = core.handle_message( + &mut state, + &format!("HELLO {}", zagan_bet::version::VERSION), + ); + let hello = zagan_bet::version::parse_hello(&response).expect("hello reply"); + + assert_eq!(hello.version, zagan_bet::version::VERSION); + assert_eq!(hello.latest, zagan_bet::version::VERSION); + assert!(hello.url.starts_with("https://"), "url: {}", hello.url); + assert!(state.greeted()); + + // The gate is open now. + assert_eq!(core.handle_message(&mut state, "PING"), "PONG"); +} + +#[test] +fn hello_refuses_a_different_major_or_minor() { + let (mut core, _path) = setup("hello-mismatch", WIN3); + let server = zagan_bet::version::Version::parse(zagan_bet::version::VERSION).unwrap(); + let versions = [ + format!("{}.{}.{}", server.major, server.minor + 1, server.patch), + format!("{}.{}.{}", server.major + 1, server.minor, server.patch), + ]; + + for version in versions { + let mut state = ConnectionState::default(); + let response = core.handle_message(&mut state, &format!("HELLO {version}")); + + assert!( + response.starts_with("ERROR Version mismatch"), + "response: {response}" + ); + assert!(!state.greeted(), "a refused handshake must not open the gate"); + assert_eq!( + core.handle_message(&mut state, "PING"), + "ERROR HELLO required" + ); + } +} + +#[test] +fn hello_accepts_a_patch_difference_and_rejects_bad_input() { + let (mut core, _path) = setup("hello-patch", WIN3); + let server = zagan_bet::version::Version::parse(zagan_bet::version::VERSION).unwrap(); + let patch = format!("{}.{}.{}", server.major, server.minor, server.patch + 1); + + let mut state = ConnectionState::default(); + assert!(core + .handle_message(&mut state, &format!("HELLO {patch}")) + .starts_with("HELLO ")); + + let mut other = ConnectionState::default(); + assert_eq!( + core.handle_message(&mut other, "HELLO"), + "ERROR Invalid HELLO format. Usage: HELLO " + ); + assert_eq!( + core.handle_message(&mut other, "HELLO nope"), + "ERROR Invalid version" + ); +} -- cgit v1.2.3