From 772727b0ef7d63144bf629d774f8b6c1c996bf78 Mon Sep 17 00:00:00 2001 From: salmonik Date: Thu, 29 Oct 2026 21:36:26 +0100 Subject: v2.3.1: settle orphaned reservations on server restart --- tests/server_core.rs | 130 ++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 129 insertions(+), 1 deletion(-) (limited to 'tests/server_core.rs') diff --git a/tests/server_core.rs b/tests/server_core.rs index 695cfe4..0350fb6 100644 --- a/tests/server_core.rs +++ b/tests/server_core.rs @@ -442,7 +442,7 @@ fn blackjack_reserves_the_bet_and_settles_a_hit_hand() { #[test] fn blackjack_double_reserves_an_extra_bet() { - let (mut core, _path) = setup_blackjack( + let (mut core, path) = setup_blackjack( "blackjack-double", &["5S", "9D", "5H", "8C", "10D"], ); @@ -464,6 +464,21 @@ fn blackjack_double_reserves_an_extra_bet() { assert_eq!(done.balance, 1020); assert_eq!(core.balance("alice"), Some(1020)); assert_eq!(core.play_count("alice"), 1); + + // A double extends the reservation, and settling clears it. + let conn = Connection::open(&path).unwrap(); + let pending: i64 = conn + .query_row("SELECT COUNT(*) FROM pending_plays", [], |row| row.get(0)) + .unwrap(); + assert_eq!(pending, 0); + let bet: i64 = conn + .query_row( + "SELECT bet_amount FROM plays WHERE username = 'alice'", + [], + |row| row.get(0), + ) + .unwrap(); + assert_eq!(bet, 20); } #[test] @@ -741,3 +756,116 @@ fn crash_rounds_are_owned_by_the_player_who_started_them() { assert_eq!(core.balance("bob"), Some(1000)); assert_eq!(core.session_count(), 1, "alice's round must survive"); } + +#[test] +fn a_crash_reservation_is_durable_until_the_round_settles() { + let (mut core, path, clock) = setup_crash("pending-crash", CRASH_AT_3); + let mut state = ConnectionState::default(); + register(&mut core, &mut state, "alice"); + login(&mut core, &mut state, "alice"); + + let started = crash_state(&core.handle_message(&mut state, "PLAY_START crash 10")); + assert_eq!(started.balance, 990); + + let conn = Connection::open(&path).unwrap(); + let pending: i64 = conn + .query_row("SELECT COUNT(*) FROM pending_plays", [], |row| row.get(0)) + .unwrap(); + assert_eq!(pending, 1, "the reserve must be written with the debit"); + drop(conn); + + clock.advance(5.0); + let cashed = crash_state(&core.handle_message( + &mut state, + &format!("CRASH_CASHOUT {}", started.session_id), + )); + assert_eq!(cashed.phase, "cashed"); + + let conn = Connection::open(&path).unwrap(); + let pending: i64 = conn + .query_row("SELECT COUNT(*) FROM pending_plays", [], |row| row.get(0)) + .unwrap(); + assert_eq!(pending, 0, "settlement must clear the reservation"); +} + +#[test] +fn a_restart_settles_an_orphaned_reservation_as_abandoned() { + let (mut core, path) = setup_blackjack("pending-restart", &["10S", "9D", "6H", "8C"]); + let mut state = ConnectionState::default(); + register(&mut core, &mut state, "alice"); + login(&mut core, &mut state, "alice"); + + let current = blackjack_state(&core.handle_message(&mut state, "PLAY_START blackjack 10")); + assert_eq!(current.phase, "player"); + assert_eq!(current.balance, 990); + + // The process dies with the hand still running. + drop(core); + + let conn = Connection::open(&path).unwrap(); + let pending: i64 = conn + .query_row("SELECT COUNT(*) FROM pending_plays", [], |row| row.get(0)) + .unwrap(); + assert_eq!(pending, 1, "the reservation must survive the process"); + drop(conn); + + // Boot does this once, before accepting connections. + let recovered = zagan_bet::server::core::recover_orphaned_reservations(&path).unwrap(); + assert_eq!(recovered, 1); + + let core = ServerCore::open(&path).unwrap(); + assert_eq!( + core.balance("alice"), + Some(990), + "an abandoned hand is a forfeit, not a refund" + ); + assert_eq!(core.play_count("alice"), 1); + + let conn = Connection::open(&path).unwrap(); + let (game_type, bet, winnings, details): (String, i64, i64, String) = conn + .query_row( + "SELECT game_type, bet_amount, winnings, details + FROM plays WHERE username = 'alice'", + [], + |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?, row.get(3)?)), + ) + .unwrap(); + assert_eq!(game_type, "blackjack"); + assert_eq!(bet, 10); + assert_eq!(winnings, 0); + let details: serde_json::Value = serde_json::from_str(&details).unwrap(); + assert_eq!(details["result"], "abandoned"); + assert_eq!(details["reason"], "server_restart"); + assert_eq!(details["player"][0], "10S", "the hand payload is preserved"); + + let pending: i64 = conn + .query_row("SELECT COUNT(*) FROM pending_plays", [], |row| row.get(0)) + .unwrap(); + assert_eq!(pending, 0, "the sweep must clear the reservation"); +} + +#[test] +fn an_insufficient_bet_leaves_no_reservation() { + let (mut core, path) = setup_blackjack("pending-broke", &["10S", "9D", "6H", "8C"]); + let mut state = ConnectionState::default(); + register(&mut core, &mut state, "alice"); + login(&mut core, &mut state, "alice"); + + let conn = Connection::open(&path).unwrap(); + conn.execute("UPDATE users SET balance = 5 WHERE username = 'alice'", []) + .unwrap(); + drop(conn); + + assert_eq!( + core.handle_message(&mut state, "PLAY_START blackjack 10"), + "ERROR Insufficient balance" + ); + assert_eq!(core.session_count(), 0); + assert_eq!(core.balance("alice"), Some(5)); + + let conn = Connection::open(&path).unwrap(); + let pending: i64 = conn + .query_row("SELECT COUNT(*) FROM pending_plays", [], |row| row.get(0)) + .unwrap(); + assert_eq!(pending, 0); +} -- cgit v1.2.3