summaryrefslogtreecommitdiffstats
path: root/tests/server_core.rs
diff options
context:
space:
mode:
Diffstat (limited to 'tests/server_core.rs')
-rw-r--r--tests/server_core.rs121
1 files changed, 105 insertions, 16 deletions
diff --git a/tests/server_core.rs b/tests/server_core.rs
index e2600e4..6c9c835 100644
--- a/tests/server_core.rs
+++ b/tests/server_core.rs
@@ -101,11 +101,11 @@ fn crash_state(response: &str) -> CrashState {
101} 101}
102 102
103fn hello(core: &mut ServerCore, state: &mut ConnectionState) { 103fn hello(core: &mut ServerCore, state: &mut ConnectionState) {
104 let response = core.handle_message( 104 let response = core.handle_message(state, &format!("HELLO {}", zagan_bet::version::VERSION));
105 state, 105 assert!(
106 &format!("HELLO {}", zagan_bet::version::VERSION), 106 response.starts_with("HELLO "),
107 "handshake failed: {response}"
107 ); 108 );
108 assert!(response.starts_with("HELLO "), "handshake failed: {response}");
109} 109}
110 110
111fn register(core: &mut ServerCore, state: &mut ConnectionState, user: &str) { 111fn register(core: &mut ServerCore, state: &mut ConnectionState, user: &str) {
@@ -258,8 +258,18 @@ fn roulette_settles_split_bets_and_records_details() {
258 258
259 let mut bets = Bets::default(); 259 let mut bets = Bets::default();
260 bets.add(BetTarget::Outside { kind: Outside::Red }, 10); 260 bets.add(BetTarget::Outside { kind: Outside::Red }, 10);
261 bets.add(BetTarget::Outside { kind: Outside::Black }, 10); 261 bets.add(
262 bets.add(BetTarget::Outside { kind: Outside::Even }, 10); 262 BetTarget::Outside {
263 kind: Outside::Black,
264 },
265 10,
266 );
267 bets.add(
268 BetTarget::Outside {
269 kind: Outside::Even,
270 },
271 10,
272 );
263 bets.add(BetTarget::Outside { kind: Outside::Odd }, 10); 273 bets.add(BetTarget::Outside { kind: Outside::Odd }, 10);
264 bets.add(BetTarget::Straight { number: 17 }, 5); 274 bets.add(BetTarget::Straight { number: 17 }, 5);
265 let bets = bets.to_json(); 275 let bets = bets.to_json();
@@ -671,10 +681,14 @@ fn crash_poll_pushes_the_bust_exactly_once() {
671 let started = crash_state(&core.handle_message(&mut state, "PLAY_START crash 10")); 681 let started = crash_state(&core.handle_message(&mut state, "PLAY_START crash 10"));
672 682
673 clock.advance(1.0); 683 clock.advance(1.0);
684 let ticks = core.poll(&state);
685 assert_eq!(ticks.len(), 1, "a running round pushes coalesced ticks");
674 assert!( 686 assert!(
675 core.poll(&state).is_empty(), 687 ticks[0].starts_with("crash.tick session="),
676 "a running round must not push anything" 688 "tick: {}",
689 ticks[0]
677 ); 690 );
691 assert!(ticks[0].contains(" m="), "tick carries the multiplier");
678 692
679 clock.advance(15.0); 693 clock.advance(15.0);
680 let pushed = core.poll(&state); 694 let pushed = core.poll(&state);
@@ -920,9 +934,10 @@ fn history_lists_recent_plays_newest_first() {
920 login(&mut core, &mut state, "alice"); 934 login(&mut core, &mut state, "alice");
921 935
922 for _ in 0..3 { 936 for _ in 0..3 {
923 assert!(core 937 assert!(
924 .handle_message(&mut state, "PLAY_START slots 10") 938 core.handle_message(&mut state, "PLAY_START slots 10")
925 .starts_with("GAME_RESULT")); 939 .starts_with("GAME_RESULT")
940 );
926 } 941 }
927 942
928 let response = core.handle_message(&mut state, "HISTORY 2"); 943 let response = core.handle_message(&mut state, "HISTORY 2");
@@ -979,7 +994,10 @@ fn hello_reports_the_version_and_download_url() {
979 assert!(state.greeted()); 994 assert!(state.greeted());
980 995
981 // The gate is open now. 996 // The gate is open now.
982 assert_eq!(core.handle_message(&mut state, "PING"), "PONG"); 997 assert!(
998 core.handle_message(&mut state, "PING").starts_with("PONG "),
999 "PING must carry the server time"
1000 );
983} 1001}
984 1002
985#[test] 1003#[test]
@@ -999,7 +1017,10 @@ fn hello_refuses_a_different_major_or_minor() {
999 response.starts_with("ERROR Version mismatch"), 1017 response.starts_with("ERROR Version mismatch"),
1000 "response: {response}" 1018 "response: {response}"
1001 ); 1019 );
1002 assert!(!state.greeted(), "a refused handshake must not open the gate"); 1020 assert!(
1021 !state.greeted(),
1022 "a refused handshake must not open the gate"
1023 );
1003 assert_eq!( 1024 assert_eq!(
1004 core.handle_message(&mut state, "PING"), 1025 core.handle_message(&mut state, "PING"),
1005 "ERROR HELLO required" 1026 "ERROR HELLO required"
@@ -1014,9 +1035,10 @@ fn hello_accepts_a_patch_difference_and_rejects_bad_input() {
1014 let patch = format!("{}.{}.{}", server.major, server.minor, server.patch + 1); 1035 let patch = format!("{}.{}.{}", server.major, server.minor, server.patch + 1);
1015 1036
1016 let mut state = ConnectionState::default(); 1037 let mut state = ConnectionState::default();
1017 assert!(core 1038 assert!(
1018 .handle_message(&mut state, &format!("HELLO {patch}")) 1039 core.handle_message(&mut state, &format!("HELLO {patch}"))
1019 .starts_with("HELLO ")); 1040 .starts_with("HELLO ")
1041 );
1020 1042
1021 let mut other = ConnectionState::default(); 1043 let mut other = ConnectionState::default();
1022 assert_eq!( 1044 assert_eq!(
@@ -1028,3 +1050,70 @@ fn hello_accepts_a_patch_difference_and_rejects_bad_input() {
1028 "ERROR Invalid version" 1050 "ERROR Invalid version"
1029 ); 1051 );
1030} 1052}
1053
1054#[test]
1055fn crash_start_commits_to_a_seed_and_the_end_reveals_it() {
1056 use zagan_bet::crash::{crash_point_from_seed, seed_from_hex, verify_seed};
1057
1058 let (mut core, _path, clock) = setup_crash("crash-commit", CRASH_AT_3);
1059 let mut state = ConnectionState::default();
1060 register(&mut core, &mut state, "alice");
1061 login(&mut core, &mut state, "alice");
1062
1063 let started = crash_state(&core.handle_message(&mut state, "PLAY_START crash 10"));
1064 let commit = started.commit.clone().expect("a start reply commits");
1065 assert_eq!(commit.len(), 64, "sha256 hex");
1066 assert_eq!(started.seed, None, "the seed stays hidden while running");
1067 assert_eq!(started.crash_point, None);
1068 assert!(started.started_at > 1_600_000_000_000, "server time anchor");
1069
1070 clock.advance(5.0);
1071 let cashed = crash_state(
1072 &core.handle_message(&mut state, &format!("CRASH_CASHOUT {}", started.session_id)),
1073 );
1074 let seed = cashed.seed.clone().expect("the end reveals the seed");
1075 assert!(verify_seed(&seed, &commit), "the reveal matches the commit");
1076 let bytes = seed_from_hex(&seed).expect("32-byte hex seed");
1077 let point = crash_point_from_seed(&bytes);
1078 assert!(
1079 (point - cashed.crash_point.unwrap()).abs() < 1e-9,
1080 "the client recomputes the same point"
1081 );
1082 assert_eq!(cashed.commit.as_deref(), Some(commit.as_str()));
1083}
1084
1085#[test]
1086fn a_repeated_request_id_replays_its_reply_without_charging_twice() {
1087 use zagan_bet::protocol::Request;
1088
1089 let (mut core, _path) = setup("idempotent-spin", WIN3);
1090 let mut state = ConnectionState::default();
1091 register(&mut core, &mut state, "alice");
1092 login(&mut core, &mut state, "alice");
1093
1094 let request = Request {
1095 id: 41,
1096 idem: Some("spin-1".to_string()),
1097 payload: "PLAY_START slots 10".to_string(),
1098 };
1099 let first = core.handle_request(&mut state, &request);
1100 assert!(first.starts_with("GAME_RESULT"), "first: {first}");
1101 assert_eq!(core.balance("alice"), Some(1000));
1102 assert_eq!(core.play_count("alice"), 1);
1103
1104 let replay = core.handle_request(&mut state, &request);
1105 assert_eq!(replay, first, "the cached reply is returned verbatim");
1106 assert_eq!(core.balance("alice"), Some(1000), "no second debit");
1107 assert_eq!(core.play_count("alice"), 1, "no second audit row");
1108
1109 let different = Request {
1110 id: 41,
1111 idem: Some("spin-2".to_string()),
1112 payload: "PLAY_START slots 20".to_string(),
1113 };
1114 assert_eq!(
1115 core.handle_request(&mut state, &different),
1116 "ERROR Request id reused with a different command"
1117 );
1118 assert_eq!(core.balance("alice"), Some(1000));
1119}