diff options
Diffstat (limited to 'tests/server_core.rs')
| -rw-r--r-- | tests/server_core.rs | 121 |
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 | ||
| 103 | fn hello(core: &mut ServerCore, state: &mut ConnectionState) { | 103 | fn 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 | ||
| 111 | fn register(core: &mut ServerCore, state: &mut ConnectionState, user: &str) { | 111 | fn 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] | ||
| 1055 | fn 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] | ||
| 1086 | fn 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 | } | ||
