diff options
| author | salmonik <salmonik@tuta.io> | 2026-10-29 22:16:15 +0100 |
|---|---|---|
| committer | salmonik <salmonik@tuta.io> | 2026-10-29 22:16:15 +0100 |
| commit | bc0de9955c375cf27d52c9934403964205afc182 (patch) | |
| tree | 42799fb426fb0ec164f44c92a63525003c582f35 | |
| parent | initial connection timeout set to 10s (diff) | |
| download | zagan-bet-bc0de9955c375cf27d52c9934403964205afc182.tar.gz zagan-bet-bc0de9955c375cf27d52c9934403964205afc182.zip | |
formatting
31 files changed, 208 insertions, 327 deletions
diff --git a/src/bin/client/repl.rs b/src/bin/client/repl.rs index 7fa462a..9af4ea9 100644 --- a/src/bin/client/repl.rs +++ b/src/bin/client/repl.rs | |||
| @@ -1,14 +1,14 @@ | |||
| 1 | use rusqlite::Connection; | 1 | use rusqlite::Connection; |
| 2 | use std::io::{stdout, Write}; | 2 | use std::io::{Write, stdout}; |
| 3 | use std::sync::{Arc, Mutex}; | 3 | use std::sync::{Arc, Mutex}; |
| 4 | use std::time::Instant; | 4 | use std::time::Instant; |
| 5 | 5 | ||
| 6 | use crossterm::{ | 6 | use crossterm::{ |
| 7 | cursor::{MoveToColumn, MoveUp, Show}, | 7 | cursor::{MoveToColumn, MoveUp, Show}, |
| 8 | event::{read, Event, KeyCode, KeyEventKind, KeyModifiers}, | 8 | event::{Event, KeyCode, KeyEventKind, KeyModifiers, read}, |
| 9 | execute, | 9 | execute, |
| 10 | style::Stylize, | 10 | style::Stylize, |
| 11 | terminal::{disable_raw_mode, enable_raw_mode, Clear, ClearType}, | 11 | terminal::{Clear, ClearType, disable_raw_mode, enable_raw_mode}, |
| 12 | }; | 12 | }; |
| 13 | 13 | ||
| 14 | use sha2::{Digest, Sha256}; | 14 | use sha2::{Digest, Sha256}; |
| @@ -20,7 +20,7 @@ use zagan_bet::client::ui::{ | |||
| 20 | visual_line_count, | 20 | visual_line_count, |
| 21 | }; | 21 | }; |
| 22 | use zagan_bet::protocol::io_failure_message; | 22 | use zagan_bet::protocol::io_failure_message; |
| 23 | use zagan_bet::roulette::{color as roulette_color, WHEEL}; | 23 | use zagan_bet::roulette::{WHEEL, color as roulette_color}; |
| 24 | 24 | ||
| 25 | use crate::connection::TlsStream; | 25 | use crate::connection::TlsStream; |
| 26 | 26 | ||
| @@ -71,8 +71,7 @@ impl ClientState { | |||
| 71 | self.core.set_connected(true); | 71 | self.core.set_connected(true); |
| 72 | self.core.set_logged_in(authed_user); | 72 | self.core.set_logged_in(authed_user); |
| 73 | let fingerprint = database::get_fingerprint(&self.conn, addr); | 73 | let fingerprint = database::get_fingerprint(&self.conn, addr); |
| 74 | self.core | 74 | self.core.set_host_info(Some(addr.to_string()), fingerprint); |
| 75 | .set_host_info(Some(addr.to_string()), fingerprint); | ||
| 76 | database::save_host(&self.conn, addr); | 75 | database::save_host(&self.conn, addr); |
| 77 | } | 76 | } |
| 78 | Err(e) => { | 77 | Err(e) => { |
| @@ -178,12 +177,9 @@ impl ClientState { | |||
| 178 | Effect::Print(lines) => { | 177 | Effect::Print(lines) => { |
| 179 | self.commit_prompt_line(); | 178 | self.commit_prompt_line(); |
| 180 | for line in lines { | 179 | for line in lines { |
| 181 | let rendered = if line.starts_with("ERROR") | 180 | let rendered = if line.starts_with("ERROR") || line.contains("failed") { |
| 182 | || line.contains("failed") | ||
| 183 | { | ||
| 184 | line.as_str().red().to_string() | 181 | line.as_str().red().to_string() |
| 185 | } else if line.starts_with("Logged in") | 182 | } else if line.starts_with("Logged in") || line.starts_with("Successfully") |
| 186 | || line.starts_with("Successfully") | ||
| 187 | { | 183 | { |
| 188 | line.as_str().green().to_string() | 184 | line.as_str().green().to_string() |
| 189 | } else if line.starts_with("Available commands:") | 185 | } else if line.starts_with("Available commands:") |
| @@ -249,10 +245,7 @@ impl ClientState { | |||
| 249 | Effect::ForgetHost(target) => { | 245 | Effect::ForgetHost(target) => { |
| 250 | self.commit_prompt_line(); | 246 | self.commit_prompt_line(); |
| 251 | match database::forget_host(&self.conn, &target) { | 247 | match database::forget_host(&self.conn, &target) { |
| 252 | Ok(_) => println!( | 248 | Ok(_) => println!("Cleared trust and stored credentials for {}.", target), |
| 253 | "Cleared trust and stored credentials for {}.", | ||
| 254 | target | ||
| 255 | ), | ||
| 256 | Err(error) => println!("Failed to forget host: {}", error), | 249 | Err(error) => println!("Failed to forget host: {}", error), |
| 257 | } | 250 | } |
| 258 | } | 251 | } |
| @@ -297,9 +290,7 @@ impl ClientState { | |||
| 297 | let roulette = lines | 290 | let roulette = lines |
| 298 | .first() | 291 | .first() |
| 299 | .is_some_and(|first| first.contains("ROULETTE")); | 292 | .is_some_and(|first| first.contains("ROULETTE")); |
| 300 | let crash = lines | 293 | let crash = lines.first().is_some_and(|first| first.contains("CRASH")); |
| 301 | .first() | ||
| 302 | .is_some_and(|first| first.contains("CRASH")); | ||
| 303 | let rendered: Vec<String> = lines | 294 | let rendered: Vec<String> = lines |
| 304 | .iter() | 295 | .iter() |
| 305 | .map(|line| { | 296 | .map(|line| { |
diff --git a/src/bin/client/tls.rs b/src/bin/client/tls.rs index b46f52b..e14ed98 100644 --- a/src/bin/client/tls.rs +++ b/src/bin/client/tls.rs | |||
| @@ -17,10 +17,10 @@ use std::net::TcpStream; | |||
| 17 | use std::sync::Arc; | 17 | use std::sync::Arc; |
| 18 | 18 | ||
| 19 | use parking_lot::Mutex; | 19 | use parking_lot::Mutex; |
| 20 | use rustls::client::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier}; | ||
| 21 | use rustls::DigitallySignedStruct; | 20 | use rustls::DigitallySignedStruct; |
| 21 | use rustls::client::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier}; | ||
| 22 | use zagan_bet::protocol::LineBuffer; | 22 | use zagan_bet::protocol::LineBuffer; |
| 23 | use zagan_bet::tofu::{decide, spki_fingerprint, TrustDecision}; | 23 | use zagan_bet::tofu::{TrustDecision, decide, spki_fingerprint}; |
| 24 | 24 | ||
| 25 | pub struct TlsConnector { | 25 | pub struct TlsConnector { |
| 26 | config: Arc<rustls::ClientConfig>, | 26 | config: Arc<rustls::ClientConfig>, |
| @@ -53,7 +53,10 @@ impl TlsConnector { | |||
| 53 | }) | 53 | }) |
| 54 | } | 54 | } |
| 55 | 55 | ||
| 56 | pub fn connect(&self, stream: TcpStream) -> Result<TlsStream, Box<dyn std::error::Error + Send + Sync>> { | 56 | pub fn connect( |
| 57 | &self, | ||
| 58 | stream: TcpStream, | ||
| 59 | ) -> Result<TlsStream, Box<dyn std::error::Error + Send + Sync>> { | ||
| 57 | let mut conn = | 60 | let mut conn = |
| 58 | rustls::ClientConnection::new(Arc::clone(&self.config), self.server_name.clone())?; | 61 | rustls::ClientConnection::new(Arc::clone(&self.config), self.server_name.clone())?; |
| 59 | let mut sock = stream; | 62 | let mut sock = stream; |
| @@ -200,10 +203,7 @@ impl TlsStream { | |||
| 200 | } | 203 | } |
| 201 | 204 | ||
| 202 | /// Apply a read timeout to the underlying TCP socket. | 205 | /// Apply a read timeout to the underlying TCP socket. |
| 203 | pub fn set_read_timeout( | 206 | pub fn set_read_timeout(&self, duration: Option<std::time::Duration>) -> std::io::Result<()> { |
| 204 | &self, | ||
| 205 | duration: Option<std::time::Duration>, | ||
| 206 | ) -> std::io::Result<()> { | ||
| 207 | let mut s = self.session.lock(); | 207 | let mut s = self.session.lock(); |
| 208 | s.read_timeout = duration; | 208 | s.read_timeout = duration; |
| 209 | s.sock.set_read_timeout(duration) | 209 | s.sock.set_read_timeout(duration) |
| @@ -235,8 +235,7 @@ impl TlsStream { | |||
| 235 | Err(error) | 235 | Err(error) |
| 236 | if matches!( | 236 | if matches!( |
| 237 | error.kind(), | 237 | error.kind(), |
| 238 | std::io::ErrorKind::WouldBlock | 238 | std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut |
| 239 | | std::io::ErrorKind::TimedOut | ||
| 240 | ) => | 239 | ) => |
| 241 | { | 240 | { |
| 242 | return Ok(()); | 241 | return Ok(()); |
diff --git a/src/bin/server/args.rs b/src/bin/server/args.rs index 1babd1d..1db2a28 100644 --- a/src/bin/server/args.rs +++ b/src/bin/server/args.rs | |||
| @@ -31,10 +31,7 @@ pub fn parse_args() -> Args { | |||
| 31 | if i + 1 < args.len() { | 31 | if i + 1 < args.len() { |
| 32 | ip = args[i + 1].clone(); | 32 | ip = args[i + 1].clone(); |
| 33 | if ip != "127.0.0.1" && ip != "0.0.0.0" { | 33 | if ip != "127.0.0.1" && ip != "0.0.0.0" { |
| 34 | eprintln!( | 34 | eprintln!("Invalid IP address: {}. Must be 127.0.0.1 or 0.0.0.0", ip); |
| 35 | "Invalid IP address: {}. Must be 127.0.0.1 or 0.0.0.0", | ||
| 36 | ip | ||
| 37 | ); | ||
| 38 | std::process::exit(1); | 35 | std::process::exit(1); |
| 39 | } | 36 | } |
| 40 | i += 2; | 37 | i += 2; |
diff --git a/src/bin/server/main.rs b/src/bin/server/main.rs index 85e3134..f42574d 100644 --- a/src/bin/server/main.rs +++ b/src/bin/server/main.rs | |||
| @@ -40,9 +40,9 @@ fn main() { | |||
| 40 | let db_path = std::env::var("SERVER_DB").unwrap_or_else(|_| "server.db".to_string()); | 40 | let db_path = std::env::var("SERVER_DB").unwrap_or_else(|_| "server.db".to_string()); |
| 41 | match zagan_bet::server::core::recover_orphaned_reservations(&db_path) { | 41 | match zagan_bet::server::core::recover_orphaned_reservations(&db_path) { |
| 42 | Ok(0) => {} | 42 | Ok(0) => {} |
| 43 | Ok(count) => println!( | 43 | Ok(count) => { |
| 44 | "Settled {count} orphaned reservation(s) as abandoned after a restart" | 44 | println!("Settled {count} orphaned reservation(s) as abandoned after a restart") |
| 45 | ), | 45 | } |
| 46 | Err(error) => { | 46 | Err(error) => { |
| 47 | eprintln!("Failed to settle orphaned reservations in {db_path}: {error}"); | 47 | eprintln!("Failed to settle orphaned reservations in {db_path}: {error}"); |
| 48 | std::process::exit(1); | 48 | std::process::exit(1); |
diff --git a/src/bin/server/tls.rs b/src/bin/server/tls.rs index 5d84589..420b5b4 100644 --- a/src/bin/server/tls.rs +++ b/src/bin/server/tls.rs | |||
| @@ -80,10 +80,7 @@ impl TlsStream { | |||
| 80 | 80 | ||
| 81 | /// Read timeout for the socket. The connection loop uses a short one so it | 81 | /// Read timeout for the socket. The connection loop uses a short one so it |
| 82 | /// can advance crash rounds while no request is in flight. | 82 | /// can advance crash rounds while no request is in flight. |
| 83 | pub fn set_read_timeout( | 83 | pub fn set_read_timeout(&self, duration: Option<std::time::Duration>) -> std::io::Result<()> { |
| 84 | &self, | ||
| 85 | duration: Option<std::time::Duration>, | ||
| 86 | ) -> std::io::Result<()> { | ||
| 87 | self.stream.get_ref().set_read_timeout(duration) | 84 | self.stream.get_ref().set_read_timeout(duration) |
| 88 | } | 85 | } |
| 89 | } | 86 | } |
diff --git a/src/client/core.rs b/src/client/core.rs index 5f8c513..06d9c42 100644 --- a/src/client/core.rs +++ b/src/client/core.rs | |||
| @@ -3,8 +3,8 @@ use crate::client::games::response::{ | |||
| 3 | parse_blackjack_response, parse_crash_response, parse_roulette_response, parse_spin_response, | 3 | parse_blackjack_response, parse_crash_response, parse_roulette_response, parse_spin_response, |
| 4 | }; | 4 | }; |
| 5 | use crate::client::ui::{ | 5 | use crate::client::ui::{ |
| 6 | command_names, completion_lines, display_width, help_lines, input_cursor_column, | 6 | command_names, completion_lines, display_width, help_lines, input_cursor_column, parse_bet_arg, |
| 7 | parse_bet_arg, record_history, | 7 | record_history, |
| 8 | }; | 8 | }; |
| 9 | use crate::crash::{multiplier_at, CrashState}; | 9 | use crate::crash::{multiplier_at, CrashState}; |
| 10 | use crate::roulette::{Bets, NumberBet}; | 10 | use crate::roulette::{Bets, NumberBet}; |
| @@ -225,10 +225,7 @@ impl ClientCore { | |||
| 225 | "Game exited. Active hand forfeited ({} PLN lost).", | 225 | "Game exited. Active hand forfeited ({} PLN lost).", |
| 226 | state.bet | 226 | state.bet |
| 227 | ); | 227 | ); |
| 228 | vec![ | 228 | vec![Effect::ClearRegion, Effect::Print(vec![message])] |
| 229 | Effect::ClearRegion, | ||
| 230 | Effect::Print(vec![message]), | ||
| 231 | ] | ||
| 232 | } | 229 | } |
| 233 | Err(message) => { | 230 | Err(message) => { |
| 234 | self.game.set_error(message); | 231 | self.game.set_error(message); |
| @@ -258,10 +255,7 @@ impl ClientCore { | |||
| 258 | Ok(state) => { | 255 | Ok(state) => { |
| 259 | self.crash_started = None; | 256 | self.crash_started = None; |
| 260 | self.game.end_game(); | 257 | self.game.end_game(); |
| 261 | let message = format!( | 258 | let message = format!("Game exited. Round forfeited ({} PLN lost).", state.bet); |
| 262 | "Game exited. Round forfeited ({} PLN lost).", | ||
| 263 | state.bet | ||
| 264 | ); | ||
| 265 | vec![Effect::ClearRegion, Effect::Print(vec![message])] | 259 | vec![Effect::ClearRegion, Effect::Print(vec![message])] |
| 266 | } | 260 | } |
| 267 | Err(_) if self.game.crash_status() != "running" => { | 261 | Err(_) if self.game.crash_status() != "running" => { |
| @@ -286,9 +280,7 @@ impl ClientCore { | |||
| 286 | self.logged_in = None; | 280 | self.logged_in = None; |
| 287 | vec![ | 281 | vec![ |
| 288 | Effect::Disconnect, | 282 | Effect::Disconnect, |
| 289 | Effect::Print(vec![ | 283 | Effect::Print(vec!["Connection lost".to_string()]), |
| 290 | "Connection lost".to_string(), | ||
| 291 | ]), | ||
| 292 | ] | 284 | ] |
| 293 | } | 285 | } |
| 294 | } | 286 | } |
| @@ -531,27 +523,20 @@ impl ClientCore { | |||
| 531 | } | 523 | } |
| 532 | } | 524 | } |
| 533 | Key::Char(' ') => self.start_blackjack(), | 525 | Key::Char(' ') => self.start_blackjack(), |
| 534 | Key::Char('h') | 526 | Key::Char('h') if hand.as_ref().is_some_and(|hand| hand.phase == "player") => { |
| 535 | if hand.as_ref().is_some_and(|hand| hand.phase == "player") => | ||
| 536 | { | ||
| 537 | self.blackjack_action("hit") | 527 | self.blackjack_action("hit") |
| 538 | } | 528 | } |
| 539 | Key::Char('s') | 529 | Key::Char('s') if hand.as_ref().is_some_and(|hand| hand.phase == "player") => { |
| 540 | if hand.as_ref().is_some_and(|hand| hand.phase == "player") => | ||
| 541 | { | ||
| 542 | self.blackjack_action("stand") | 530 | self.blackjack_action("stand") |
| 543 | } | 531 | } |
| 544 | Key::Char('d') | 532 | Key::Char('d') |
| 545 | if hand.as_ref().is_some_and(|hand| { | 533 | if hand.as_ref().is_some_and(|hand| { |
| 546 | hand.phase == "player" | 534 | hand.phase == "player" && hand.actions.iter().any(|action| action == "double") |
| 547 | && hand.actions.iter().any(|action| action == "double") | ||
| 548 | }) => | 535 | }) => |
| 549 | { | 536 | { |
| 550 | self.blackjack_action("double") | 537 | self.blackjack_action("double") |
| 551 | } | 538 | } |
| 552 | Key::Char('+') | Key::Char('=') | 539 | Key::Char('+') | Key::Char('=') if matches!(status.as_str(), "ready" | "done") => { |
| 553 | if matches!(status.as_str(), "ready" | "done") => | ||
| 554 | { | ||
| 555 | let _ = self.game.adjust_bet(10); | 540 | let _ = self.game.adjust_bet(10); |
| 556 | self.board_effects() | 541 | self.board_effects() |
| 557 | } | 542 | } |
| @@ -602,9 +587,7 @@ impl ClientCore { | |||
| 602 | if hand.phase != "player" { | 587 | if hand.phase != "player" { |
| 603 | return Vec::new(); | 588 | return Vec::new(); |
| 604 | } | 589 | } |
| 605 | if action == "double" | 590 | if action == "double" && !hand.actions.iter().any(|available| available == "double") { |
| 606 | && !hand.actions.iter().any(|available| available == "double") | ||
| 607 | { | ||
| 608 | return Vec::new(); | 591 | return Vec::new(); |
| 609 | } | 592 | } |
| 610 | self.pending = Some(Pending::BlackjackAction); | 593 | self.pending = Some(Pending::BlackjackAction); |
| @@ -914,9 +897,9 @@ impl ClientCore { | |||
| 914 | Some(fingerprint) => { | 897 | Some(fingerprint) => { |
| 915 | effects.push(Effect::Print(vec![format!("Fingerprint: {fingerprint}")])) | 898 | effects.push(Effect::Print(vec![format!("Fingerprint: {fingerprint}")])) |
| 916 | } | 899 | } |
| 917 | None => effects.push(Effect::Print(vec![ | 900 | None => effects.push(Effect::Print( |
| 918 | "Fingerprint: (not trusted)".to_string(), | 901 | vec!["Fingerprint: (not trusted)".to_string()], |
| 919 | ])), | 902 | )), |
| 920 | } | 903 | } |
| 921 | 904 | ||
| 922 | self.pending = Some(Pending::Info); | 905 | self.pending = Some(Pending::Info); |
diff --git a/src/client/database.rs b/src/client/database.rs index e1fa42d..e1cedaf 100644 --- a/src/client/database.rs +++ b/src/client/database.rs | |||
| @@ -110,9 +110,8 @@ pub fn get_all_hosts(conn: &Connection) -> Vec<(String, Option<String>)> { | |||
| 110 | let mut stmt = conn | 110 | let mut stmt = conn |
| 111 | .prepare("SELECT host, fingerprint FROM hosts ORDER BY last_used DESC") | 111 | .prepare("SELECT host, fingerprint FROM hosts ORDER BY last_used DESC") |
| 112 | .unwrap(); | 112 | .unwrap(); |
| 113 | let rows = stmt.query_map([], |row| { | 113 | let rows = stmt |
| 114 | Ok((row.get(0)?, row.get(1)?)) | 114 | .query_map([], |row| Ok((row.get(0)?, row.get(1)?))) |
| 115 | }) | 115 | .unwrap(); |
| 116 | .unwrap(); | ||
| 117 | rows.filter_map(|r| r.ok()).collect() | 116 | rows.filter_map(|r| r.ok()).collect() |
| 118 | } | 117 | } |
diff --git a/src/client/games/blackjack.rs b/src/client/games/blackjack.rs index bad1781..d5f45b3 100644 --- a/src/client/games/blackjack.rs +++ b/src/client/games/blackjack.rs | |||
| @@ -56,15 +56,14 @@ impl Game for BlackjackGame { | |||
| 56 | .get("status") | 56 | .get("status") |
| 57 | .map(String::as_str) | 57 | .map(String::as_str) |
| 58 | .unwrap_or("ready"); | 58 | .unwrap_or("ready"); |
| 59 | let balance = state | 59 | let balance = state.data.get("balance").map(String::as_str).unwrap_or("?"); |
| 60 | .data | ||
| 61 | .get("balance") | ||
| 62 | .map(String::as_str) | ||
| 63 | .unwrap_or("?"); | ||
| 64 | let hand = Self::state(state); | 60 | let hand = Self::state(state); |
| 65 | 61 | ||
| 66 | if let Some(error) = state.data.get("error") { | 62 | if let Some(error) = state.data.get("error") { |
| 67 | let bet = hand.as_ref().map(|hand| hand.bet).unwrap_or(state.bet_amount); | 63 | let bet = hand |
| 64 | .as_ref() | ||
| 65 | .map(|hand| hand.bet) | ||
| 66 | .unwrap_or(state.bet_amount); | ||
| 68 | return format!( | 67 | return format!( |
| 69 | "=== BLACKJACK ===\r\nBalance: {balance} PLN Bet: {bet} PLN\r\n\r\n\r\nERROR: {error}\r\n[Q] Quit" | 68 | "=== BLACKJACK ===\r\nBalance: {balance} PLN Bet: {bet} PLN\r\n\r\n\r\nERROR: {error}\r\n[Q] Quit" |
| 70 | ); | 69 | ); |
diff --git a/src/client/games/manager.rs b/src/client/games/manager.rs index 4d9e6a8..b20ff44 100644 --- a/src/client/games/manager.rs +++ b/src/client/games/manager.rs | |||
| @@ -138,9 +138,7 @@ impl GameManager { | |||
| 138 | session.bet_amount = result.bet_amount; | 138 | session.bet_amount = result.bet_amount; |
| 139 | session.data.remove("error"); | 139 | session.data.remove("error"); |
| 140 | session.data.insert("reels".to_string(), result.reels); | 140 | session.data.insert("reels".to_string(), result.reels); |
| 141 | session | 141 | session.data.insert("win_type".to_string(), result.win_type); |
| 142 | .data | ||
| 143 | .insert("win_type".to_string(), result.win_type); | ||
| 144 | session.data.insert( | 142 | session.data.insert( |
| 145 | "win_lines".to_string(), | 143 | "win_lines".to_string(), |
| 146 | result | 144 | result |
| @@ -339,7 +337,9 @@ impl GameManager { | |||
| 339 | for key in ["crash_point", "payout"] { | 337 | for key in ["crash_point", "payout"] { |
| 340 | session.data.remove(key); | 338 | session.data.remove(key); |
| 341 | } | 339 | } |
| 342 | session.data.insert("status".to_string(), state.phase.clone()); | 340 | session |
| 341 | .data | ||
| 342 | .insert("status".to_string(), state.phase.clone()); | ||
| 343 | session | 343 | session |
| 344 | .data | 344 | .data |
| 345 | .insert("session_id".to_string(), state.session_id); | 345 | .insert("session_id".to_string(), state.session_id); |
diff --git a/src/client/games/response.rs b/src/client/games/response.rs index 71e1fed..88df459 100644 --- a/src/client/games/response.rs +++ b/src/client/games/response.rs | |||
| @@ -128,8 +128,8 @@ pub fn parse_roulette_response(response: &str) -> Result<RouletteResult, String> | |||
| 128 | return Err(format!("Invalid roulette color: {color}")); | 128 | return Err(format!("Invalid roulette color: {color}")); |
| 129 | } | 129 | } |
| 130 | let bets = parts[7].to_string(); | 130 | let bets = parts[7].to_string(); |
| 131 | let _: crate::roulette::Bets = serde_json::from_str(&bets) | 131 | let _: crate::roulette::Bets = |
| 132 | .map_err(|error| format!("Invalid roulette bets: {error}"))?; | 132 | serde_json::from_str(&bets).map_err(|error| format!("Invalid roulette bets: {error}"))?; |
| 133 | 133 | ||
| 134 | Ok(RouletteResult { | 134 | Ok(RouletteResult { |
| 135 | session_id: parts[1].to_string(), | 135 | session_id: parts[1].to_string(), |
diff --git a/src/client/games/roulette.rs b/src/client/games/roulette.rs index 3379317..e80eca8 100644 --- a/src/client/games/roulette.rs +++ b/src/client/games/roulette.rs | |||
| @@ -53,11 +53,7 @@ impl RouletteGame { | |||
| 53 | 53 | ||
| 54 | match state.data.get("status").map(String::as_str) { | 54 | match state.data.get("status").map(String::as_str) { |
| 55 | Some("result") => { | 55 | Some("result") => { |
| 56 | let number = state | 56 | let number = state.data.get("number").map(String::as_str).unwrap_or("?"); |
| 57 | .data | ||
| 58 | .get("number") | ||
| 59 | .map(String::as_str) | ||
| 60 | .unwrap_or("?"); | ||
| 61 | let color = state | 57 | let color = state |
| 62 | .data | 58 | .data |
| 63 | .get("color") | 59 | .get("color") |
| @@ -102,11 +98,7 @@ impl Game for RouletteGame { | |||
| 102 | fn render(&self, state: &GameState) -> String { | 98 | fn render(&self, state: &GameState) -> String { |
| 103 | let bets = Self::bets(state); | 99 | let bets = Self::bets(state); |
| 104 | let chip = state.data.get("chip").map(String::as_str).unwrap_or("10"); | 100 | let chip = state.data.get("chip").map(String::as_str).unwrap_or("10"); |
| 105 | let balance = state | 101 | let balance = state.data.get("balance").map(String::as_str).unwrap_or("?"); |
| 106 | .data | ||
| 107 | .get("balance") | ||
| 108 | .map(String::as_str) | ||
| 109 | .unwrap_or("?"); | ||
| 110 | let input = state | 102 | let input = state |
| 111 | .data | 103 | .data |
| 112 | .get("number_input") | 104 | .get("number_input") |
diff --git a/src/client/games/slots.rs b/src/client/games/slots.rs index d34bf49..2ec354e 100644 --- a/src/client/games/slots.rs +++ b/src/client/games/slots.rs | |||
| @@ -109,11 +109,7 @@ impl Game for SlotsGame { | |||
| 109 | 109 | ||
| 110 | fn render(&self, state: &GameState) -> String { | 110 | fn render(&self, state: &GameState) -> String { |
| 111 | let reels = state.data.get("reels").cloned().unwrap_or_default(); | 111 | let reels = state.data.get("reels").cloned().unwrap_or_default(); |
| 112 | let balance = state | 112 | let balance = state.data.get("balance").map(String::as_str).unwrap_or("?"); |
| 113 | .data | ||
| 114 | .get("balance") | ||
| 115 | .map(String::as_str) | ||
| 116 | .unwrap_or("?"); | ||
| 117 | let winning = Self::highlighted_cells(state); | 113 | let winning = Self::highlighted_cells(state); |
| 118 | let grid = self.render_grid(&reels, &winning); | 114 | let grid = self.render_grid(&reels, &winning); |
| 119 | let paytable = paytable_lines(); | 115 | let paytable = paytable_lines(); |
diff --git a/src/client/ui.rs b/src/client/ui.rs index 0fe78ae..4ab0759 100644 --- a/src/client/ui.rs +++ b/src/client/ui.rs | |||
| @@ -94,10 +94,7 @@ pub fn command_names() -> Vec<&'static str> { | |||
| 94 | pub fn help_lines() -> Vec<String> { | 94 | pub fn help_lines() -> Vec<String> { |
| 95 | let mut lines = vec!["Available commands:".to_string()]; | 95 | let mut lines = vec!["Available commands:".to_string()]; |
| 96 | for command in COMMANDS { | 96 | for command in COMMANDS { |
| 97 | lines.push(format!( | 97 | lines.push(format!(" {:<28} - {}", command.usage, command.description)); |
| 98 | " {:<28} - {}", | ||
| 99 | command.usage, command.description | ||
| 100 | )); | ||
| 101 | } | 98 | } |
| 102 | lines | 99 | lines |
| 103 | } | 100 | } |
| @@ -291,16 +288,12 @@ pub fn host_lines(hosts: &[(String, Option<String>)]) -> Vec<String> { | |||
| 291 | /// on the current line. `render()` has no trailing newline, so a 7-line board | 288 | /// on the current line. `render()` has no trailing newline, so a 7-line board |
| 292 | /// needs `MoveUp(6)`. | 289 | /// needs `MoveUp(6)`. |
| 293 | pub fn lines_to_move_up(previous_lines: usize) -> u16 { | 290 | pub fn lines_to_move_up(previous_lines: usize) -> u16 { |
| 294 | previous_lines | 291 | previous_lines.saturating_sub(1).min(u16::MAX as usize) as u16 |
| 295 | .saturating_sub(1) | ||
| 296 | .min(u16::MAX as usize) as u16 | ||
| 297 | } | 292 | } |
| 298 | 293 | ||
| 299 | /// Column (0-based) to move to after the given prompt and input text. | 294 | /// Column (0-based) to move to after the given prompt and input text. |
| 300 | pub fn input_cursor_column(prompt_len: usize, input_len: usize) -> u16 { | 295 | pub fn input_cursor_column(prompt_len: usize, input_len: usize) -> u16 { |
| 301 | prompt_len | 296 | prompt_len.saturating_add(input_len).min(u16::MAX as usize) as u16 |
| 302 | .saturating_add(input_len) | ||
| 303 | .min(u16::MAX as usize) as u16 | ||
| 304 | } | 297 | } |
| 305 | 298 | ||
| 306 | /// Number of terminal rows a block of logical lines occupies after wrapping. | 299 | /// Number of terminal rows a block of logical lines occupies after wrapping. |
diff --git a/src/crash.rs b/src/crash.rs index 55aab19..2936b99 100644 --- a/src/crash.rs +++ b/src/crash.rs | |||
| @@ -24,9 +24,7 @@ pub fn multiplier_at_secs(seconds: f64) -> f64 { | |||
| 24 | if !seconds.is_finite() || seconds <= 0.0 { | 24 | if !seconds.is_finite() || seconds <= 0.0 { |
| 25 | return 1.0; | 25 | return 1.0; |
| 26 | } | 26 | } |
| 27 | (GROWTH_RATE * seconds) | 27 | (GROWTH_RATE * seconds).exp().clamp(1.0, MAX_MULTIPLIER) |
| 28 | .exp() | ||
| 29 | .clamp(1.0, MAX_MULTIPLIER) | ||
| 30 | } | 28 | } |
| 31 | 29 | ||
| 32 | /// How long a round runs before reaching `multiplier`. | 30 | /// How long a round runs before reaching `multiplier`. |
diff --git a/src/roulette.rs b/src/roulette.rs index b59f92e..426ecc3 100644 --- a/src/roulette.rs +++ b/src/roulette.rs | |||
| @@ -4,8 +4,8 @@ use serde::{Deserialize, Serialize}; | |||
| 4 | 4 | ||
| 5 | /// European wheel clockwise from zero. | 5 | /// European wheel clockwise from zero. |
| 6 | pub const WHEEL: [u8; 37] = [ | 6 | pub const WHEEL: [u8; 37] = [ |
| 7 | 0, 32, 15, 19, 4, 21, 2, 25, 17, 34, 6, 27, 13, 36, 11, 30, 8, 23, 10, 5, | 7 | 0, 32, 15, 19, 4, 21, 2, 25, 17, 34, 6, 27, 13, 36, 11, 30, 8, 23, 10, 5, 24, 16, 33, 1, 20, |
| 8 | 24, 16, 33, 1, 20, 14, 31, 9, 22, 18, 29, 7, 28, 12, 35, 3, 26, | 8 | 14, 31, 9, 22, 18, 29, 7, 28, 12, 35, 3, 26, |
| 9 | ]; | 9 | ]; |
| 10 | 10 | ||
| 11 | pub const RED_NUMBERS: [u8; 18] = [ | 11 | pub const RED_NUMBERS: [u8; 18] = [ |
| @@ -92,8 +92,7 @@ impl Bets { | |||
| 92 | if is_red(number) { | 92 | if is_red(number) { |
| 93 | winnings = winnings.saturating_add(self.red.saturating_mul(EVEN_MONEY_RETURN)); | 93 | winnings = winnings.saturating_add(self.red.saturating_mul(EVEN_MONEY_RETURN)); |
| 94 | } else { | 94 | } else { |
| 95 | winnings = | 95 | winnings = winnings.saturating_add(self.black.saturating_mul(EVEN_MONEY_RETURN)); |
| 96 | winnings.saturating_add(self.black.saturating_mul(EVEN_MONEY_RETURN)); | ||
| 97 | } | 96 | } |
| 98 | if is_even(number) { | 97 | if is_even(number) { |
| 99 | winnings = winnings.saturating_add(self.even.saturating_mul(EVEN_MONEY_RETURN)); | 98 | winnings = winnings.saturating_add(self.even.saturating_mul(EVEN_MONEY_RETURN)); |
| @@ -104,8 +103,7 @@ impl Bets { | |||
| 104 | 103 | ||
| 105 | for bet in &self.numbers { | 104 | for bet in &self.numbers { |
| 106 | if bet.number == number { | 105 | if bet.number == number { |
| 107 | winnings = winnings | 106 | winnings = winnings.saturating_add(bet.amount.saturating_mul(STRAIGHT_RETURN)); |
| 108 | .saturating_add(bet.amount.saturating_mul(STRAIGHT_RETURN)); | ||
| 109 | } | 107 | } |
| 110 | } | 108 | } |
| 111 | 109 | ||
diff --git a/src/server/core.rs b/src/server/core.rs index e0f0ff2..4b68ccb 100644 --- a/src/server/core.rs +++ b/src/server/core.rs | |||
| @@ -49,11 +49,7 @@ impl ServerCore { | |||
| 49 | Self::with_registry(conn, GameRegistry::with_defaults(), DEFAULT_COST) | 49 | Self::with_registry(conn, GameRegistry::with_defaults(), DEFAULT_COST) |
| 50 | } | 50 | } |
| 51 | 51 | ||
| 52 | pub fn with_registry( | 52 | pub fn with_registry(conn: Connection, registry: GameRegistry, bcrypt_cost: u32) -> Self { |
| 53 | conn: Connection, | ||
| 54 | registry: GameRegistry, | ||
| 55 | bcrypt_cost: u32, | ||
| 56 | ) -> Self { | ||
| 57 | Self { | 53 | Self { |
| 58 | conn, | 54 | conn, |
| 59 | sessions: SessionManager::new(), | 55 | sessions: SessionManager::new(), |
| @@ -115,8 +111,7 @@ impl ServerCore { | |||
| 115 | } | 111 | } |
| 116 | "GAME_INFO" => { | 112 | "GAME_INFO" => { |
| 117 | if parts.len() != 2 { | 113 | if parts.len() != 2 { |
| 118 | "ERROR Invalid GAME_INFO format. Usage: GAME_INFO <game_type>" | 114 | "ERROR Invalid GAME_INFO format. Usage: GAME_INFO <game_type>".to_string() |
| 119 | .to_string() | ||
| 120 | } else { | 115 | } else { |
| 121 | match self.registry.get_game(parts[1]) { | 116 | match self.registry.get_game(parts[1]) { |
| 122 | Some(game) => format!( | 117 | Some(game) => format!( |
| @@ -143,16 +138,14 @@ impl ServerCore { | |||
| 143 | } | 138 | } |
| 144 | "CRASH_CASHOUT" => { | 139 | "CRASH_CASHOUT" => { |
| 145 | if parts.len() != 2 { | 140 | if parts.len() != 2 { |
| 146 | "ERROR Invalid CRASH_CASHOUT format. Usage: CRASH_CASHOUT <session>" | 141 | "ERROR Invalid CRASH_CASHOUT format. Usage: CRASH_CASHOUT <session>".to_string() |
| 147 | .to_string() | ||
| 148 | } else { | 142 | } else { |
| 149 | self.handle_crash_end(state, parts[1], false) | 143 | self.handle_crash_end(state, parts[1], false) |
| 150 | } | 144 | } |
| 151 | } | 145 | } |
| 152 | "CRASH_FORFEIT" => { | 146 | "CRASH_FORFEIT" => { |
| 153 | if parts.len() != 2 { | 147 | if parts.len() != 2 { |
| 154 | "ERROR Invalid CRASH_FORFEIT format. Usage: CRASH_FORFEIT <session>" | 148 | "ERROR Invalid CRASH_FORFEIT format. Usage: CRASH_FORFEIT <session>".to_string() |
| 155 | .to_string() | ||
| 156 | } else { | 149 | } else { |
| 157 | self.handle_crash_end(state, parts[1], true) | 150 | self.handle_crash_end(state, parts[1], true) |
| 158 | } | 151 | } |
| @@ -351,12 +344,8 @@ impl ServerCore { | |||
| 351 | if hand.player.len() != 2 { | 344 | if hand.player.len() != 2 { |
| 352 | return "ERROR Double is only allowed on the first two cards".to_string(); | 345 | return "ERROR Double is only allowed on the first two cards".to_string(); |
| 353 | } | 346 | } |
| 354 | if database::extend_reservation( | 347 | if database::extend_reservation(&mut self.conn, session_id, hand.base_bet as i64) |
| 355 | &mut self.conn, | 348 | .is_err() |
| 356 | session_id, | ||
| 357 | hand.base_bet as i64, | ||
| 358 | ) | ||
| 359 | .is_err() | ||
| 360 | { | 349 | { |
| 361 | return "ERROR Insufficient balance".to_string(); | 350 | return "ERROR Insufficient balance".to_string(); |
| 362 | } | 351 | } |
| @@ -515,7 +504,12 @@ impl ServerCore { | |||
| 515 | 504 | ||
| 516 | /// Cash out or forfeit. The multiplier comes from the server clock at the | 505 | /// Cash out or forfeit. The multiplier comes from the server clock at the |
| 517 | /// moment the request is handled; the client never supplies one. | 506 | /// moment the request is handled; the client never supplies one. |
| 518 | fn handle_crash_end(&mut self, state: &ConnectionState, session_id: &str, forfeit: bool) -> String { | 507 | fn handle_crash_end( |
| 508 | &mut self, | ||
| 509 | state: &ConnectionState, | ||
| 510 | session_id: &str, | ||
| 511 | forfeit: bool, | ||
| 512 | ) -> String { | ||
| 519 | let Some(user) = state.user.as_deref() else { | 513 | let Some(user) = state.user.as_deref() else { |
| 520 | return "ERROR Not logged in".to_string(); | 514 | return "ERROR Not logged in".to_string(); |
| 521 | }; | 515 | }; |
| @@ -698,11 +692,7 @@ impl ServerCore { | |||
| 698 | .get("number") | 692 | .get("number") |
| 699 | .cloned() | 693 | .cloned() |
| 700 | .unwrap_or_default(); | 694 | .unwrap_or_default(); |
| 701 | let color = result | 695 | let color = result.result_data.get("color").cloned().unwrap_or_default(); |
| 702 | .result_data | ||
| 703 | .get("color") | ||
| 704 | .cloned() | ||
| 705 | .unwrap_or_default(); | ||
| 706 | let bets = result | 696 | let bets = result |
| 707 | .result_data | 697 | .result_data |
| 708 | .get("bets") | 698 | .get("bets") |
| @@ -712,11 +702,7 @@ impl ServerCore { | |||
| 712 | "ROULETTE_RESULT {session_id} {winnings} {bet_amount} {new_balance} {number} {color} {bets}" | 702 | "ROULETTE_RESULT {session_id} {winnings} {bet_amount} {new_balance} {number} {color} {bets}" |
| 713 | ) | 703 | ) |
| 714 | } else { | 704 | } else { |
| 715 | let reels = result | 705 | let reels = result.result_data.get("reels").cloned().unwrap_or_default(); |
| 716 | .result_data | ||
| 717 | .get("reels") | ||
| 718 | .cloned() | ||
| 719 | .unwrap_or_default(); | ||
| 720 | let win_type = result | 706 | let win_type = result |
| 721 | .result_data | 707 | .result_data |
| 722 | .get("win_type") | 708 | .get("win_type") |
| @@ -753,8 +739,7 @@ impl ServerCore { | |||
| 753 | if let Ok(mut hand) = BlackjackHand::from_json(raw) { | 739 | if let Ok(mut hand) = BlackjackHand::from_json(raw) { |
| 754 | if !hand.is_done() { | 740 | if !hand.is_done() { |
| 755 | hand.forfeit(); | 741 | hand.forfeit(); |
| 756 | let balance = | 742 | let balance = database::get_balance(&self.conn, user).unwrap_or(0); |
| 757 | database::get_balance(&self.conn, user).unwrap_or(0); | ||
| 758 | let final_state = hand.state(&session.session_id, balance); | 743 | let final_state = hand.state(&session.session_id, balance); |
| 759 | let details = serde_json::to_string(&final_state) | 744 | let details = serde_json::to_string(&final_state) |
| 760 | .unwrap_or_else(|_| "{}".to_string()); | 745 | .unwrap_or_else(|_| "{}".to_string()); |
diff --git a/src/server/games/blackjack.rs b/src/server/games/blackjack.rs index df5a6ac..cea817a 100644 --- a/src/server/games/blackjack.rs +++ b/src/server/games/blackjack.rs | |||
| @@ -1,7 +1,5 @@ | |||
| 1 | use super::traits::{Game, GameError, GamePhase, GameResult, GameState}; | 1 | use super::traits::{Game, GameError, GamePhase, GameResult, GameState}; |
| 2 | use crate::blackjack::{ | 2 | use crate::blackjack::{card_value, fresh_shoe, hand_total, is_blackjack, BlackjackState}; |
| 3 | card_value, fresh_shoe, hand_total, is_blackjack, BlackjackState, | ||
| 4 | }; | ||
| 5 | use rand::seq::SliceRandom; | 3 | use rand::seq::SliceRandom; |
| 6 | use rand::thread_rng; | 4 | use rand::thread_rng; |
| 7 | use serde::{Deserialize, Serialize}; | 5 | use serde::{Deserialize, Serialize}; |
diff --git a/src/server/games/roulette.rs b/src/server/games/roulette.rs index 7beb8c9..0a58323 100644 --- a/src/server/games/roulette.rs +++ b/src/server/games/roulette.rs | |||
| @@ -65,17 +65,15 @@ impl Game for RouletteGame { | |||
| 65 | 65 | ||
| 66 | match action { | 66 | match action { |
| 67 | "spin" => { | 67 | "spin" => { |
| 68 | if state.phase != GamePhase::WaitingToStart | 68 | if state.phase != GamePhase::WaitingToStart && state.phase != GamePhase::Playing { |
| 69 | && state.phase != GamePhase::Playing | ||
| 70 | { | ||
| 71 | return Err(GameError::InvalidAction( | 69 | return Err(GameError::InvalidAction( |
| 72 | "Cannot spin in current phase".to_string(), | 70 | "Cannot spin in current phase".to_string(), |
| 73 | )); | 71 | )); |
| 74 | } | 72 | } |
| 75 | 73 | ||
| 76 | let payload = params | 74 | let payload = params.first().ok_or(GameError::InvalidAction( |
| 77 | .first() | 75 | "Missing roulette bets".to_string(), |
| 78 | .ok_or(GameError::InvalidAction("Missing roulette bets".to_string()))?; | 76 | ))?; |
| 79 | let bets: Bets = serde_json::from_str(payload) | 77 | let bets: Bets = serde_json::from_str(payload) |
| 80 | .map_err(|error| GameError::InvalidAction(format!("Invalid bets: {error}")))?; | 78 | .map_err(|error| GameError::InvalidAction(format!("Invalid bets: {error}")))?; |
| 81 | 79 | ||
diff --git a/src/server/games/slots.rs b/src/server/games/slots.rs index 298cd4c..caced24 100644 --- a/src/server/games/slots.rs +++ b/src/server/games/slots.rs | |||
| @@ -175,9 +175,7 @@ impl Game for SlotsGame { | |||
| 175 | 175 | ||
| 176 | match action { | 176 | match action { |
| 177 | "spin" => { | 177 | "spin" => { |
| 178 | if state.phase != GamePhase::WaitingToStart | 178 | if state.phase != GamePhase::WaitingToStart && state.phase != GamePhase::Playing { |
| 179 | && state.phase != GamePhase::Playing | ||
| 180 | { | ||
| 181 | return Err(GameError::InvalidAction( | 179 | return Err(GameError::InvalidAction( |
| 182 | "Cannot spin in current phase".to_string(), | 180 | "Cannot spin in current phase".to_string(), |
| 183 | )); | 181 | )); |
diff --git a/src/server/setup.rs b/src/server/setup.rs index 96982c5..023e6ed 100644 --- a/src/server/setup.rs +++ b/src/server/setup.rs | |||
| @@ -47,19 +47,8 @@ pub fn init_identity(dir: &Path, common_name: &str) -> Result<SetupReport, Strin | |||
| 47 | run_openssl( | 47 | run_openssl( |
| 48 | dir, | 48 | dir, |
| 49 | &[ | 49 | &[ |
| 50 | "req", | 50 | "req", "-x509", "-newkey", "rsa:2048", "-keyout", "key.pem", "-out", "cert.pem", |
| 51 | "-x509", | 51 | "-days", "3650", "-nodes", "-subj", &subject, |
| 52 | "-newkey", | ||
| 53 | "rsa:2048", | ||
| 54 | "-keyout", | ||
| 55 | "key.pem", | ||
| 56 | "-out", | ||
| 57 | "cert.pem", | ||
| 58 | "-days", | ||
| 59 | "3650", | ||
| 60 | "-nodes", | ||
| 61 | "-subj", | ||
| 62 | &subject, | ||
| 63 | ], | 52 | ], |
| 64 | "generate the certificate", | 53 | "generate the certificate", |
| 65 | &manual, | 54 | &manual, |
| @@ -98,9 +87,8 @@ pub fn init_identity(dir: &Path, common_name: &str) -> Result<SetupReport, Strin | |||
| 98 | .map_err(|error| format!("Could not inspect {}: {error}", path.display()))?; | 87 | .map_err(|error| format!("Could not inspect {}: {error}", path.display()))?; |
| 99 | let mut permissions = metadata.permissions(); | 88 | let mut permissions = metadata.permissions(); |
| 100 | permissions.set_mode(0o600); | 89 | permissions.set_mode(0o600); |
| 101 | std::fs::set_permissions(path, permissions).map_err(|error| { | 90 | std::fs::set_permissions(path, permissions) |
| 102 | format!("Could not protect {}: {error}", path.display()) | 91 | .map_err(|error| format!("Could not protect {}: {error}", path.display()))?; |
| 103 | })?; | ||
| 104 | } | 92 | } |
| 105 | } | 93 | } |
| 106 | 94 | ||
diff --git a/src/slots.rs b/src/slots.rs index 9e41f05..443444e 100644 --- a/src/slots.rs +++ b/src/slots.rs | |||
| @@ -16,14 +16,8 @@ pub const PAYLINES: [[usize; REELS]; 5] = [ | |||
| 16 | pub const LINE_NAMES: [&str; 5] = ["top", "middle", "bottom", "V", "inverted V"]; | 16 | pub const LINE_NAMES: [&str; 5] = ["top", "middle", "bottom", "V", "inverted V"]; |
| 17 | 17 | ||
| 18 | /// Symbol value multipliers. | 18 | /// Symbol value multipliers. |
| 19 | pub const SYMBOL_MULTIPLIERS: [(char, u64); 6] = [ | 19 | pub const SYMBOL_MULTIPLIERS: [(char, u64); 6] = |
| 20 | ('C', 1), | 20 | [('C', 1), ('L', 2), ('O', 3), ('G', 4), ('D', 6), ('S', 10)]; |
| 21 | ('L', 2), | ||
| 22 | ('O', 3), | ||
| 23 | ('G', 4), | ||
| 24 | ('D', 6), | ||
| 25 | ('S', 10), | ||
| 26 | ]; | ||
| 27 | 21 | ||
| 28 | /// (match length, shape multiplier): 3, 4 or 5 of a kind. | 22 | /// (match length, shape multiplier): 3, 4 or 5 of a kind. |
| 29 | pub const SHAPE_MULTIPLIERS: [(usize, u64); 3] = [(3, 1), (4, 3), (5, 14)]; | 23 | pub const SHAPE_MULTIPLIERS: [(usize, u64); 3] = [(3, 1), (4, 3), (5, 14)]; |
diff --git a/src/tofu.rs b/src/tofu.rs index 6308813..15f38d1 100644 --- a/src/tofu.rs +++ b/src/tofu.rs | |||
| @@ -17,10 +17,7 @@ pub enum TrustDecision { | |||
| 17 | /// Stored fingerprint matches: proceed. | 17 | /// Stored fingerprint matches: proceed. |
| 18 | Trusted, | 18 | Trusted, |
| 19 | /// Key changed since first use: reject with details. | 19 | /// Key changed since first use: reject with details. |
| 20 | Mismatch { | 20 | Mismatch { expected: String, actual: String }, |
| 21 | expected: String, | ||
| 22 | actual: String, | ||
| 23 | }, | ||
| 24 | } | 21 | } |
| 25 | 22 | ||
| 26 | /// Hex-encoded SHA-256 of the SPKI inside a DER-encoded X.509 certificate. | 23 | /// Hex-encoded SHA-256 of the SPKI inside a DER-encoded X.509 certificate. |
diff --git a/tests/blackjack.rs b/tests/blackjack.rs index f90ca8d..36ed7cf 100644 --- a/tests/blackjack.rs +++ b/tests/blackjack.rs | |||
| @@ -16,7 +16,12 @@ fn card_values_and_soft_totals_are_correct() { | |||
| 16 | assert_eq!(card_label("AS"), "Aâ™ "); | 16 | assert_eq!(card_label("AS"), "Aâ™ "); |
| 17 | assert_eq!(card_label("10H"), "10♥"); | 17 | assert_eq!(card_label("10H"), "10♥"); |
| 18 | 18 | ||
| 19 | let cards = |values: &[&str]| values.iter().map(|card| card.to_string()).collect::<Vec<_>>(); | 19 | let cards = |values: &[&str]| { |
| 20 | values | ||
| 21 | .iter() | ||
| 22 | .map(|card| card.to_string()) | ||
| 23 | .collect::<Vec<_>>() | ||
| 24 | }; | ||
| 20 | assert_eq!(hand_total(&cards(&["AS", "10H"])), 21); | 25 | assert_eq!(hand_total(&cards(&["AS", "10H"])), 21); |
| 21 | assert_eq!(hand_total(&cards(&["AS", "9H"])), 20); | 26 | assert_eq!(hand_total(&cards(&["AS", "9H"])), 20); |
| 22 | assert_eq!(hand_total(&cards(&["AS", "AS", "9H"])), 21); | 27 | assert_eq!(hand_total(&cards(&["AS", "AS", "9H"])), 21); |
diff --git a/tests/client_core.rs b/tests/client_core.rs index e8b5646..176791e 100644 --- a/tests/client_core.rs +++ b/tests/client_core.rs | |||
| @@ -4,6 +4,7 @@ | |||
| 4 | //! logical `Effect`s that `ClientCore` emits, so assertions are about what a | 4 | //! logical `Effect`s that `ClientCore` emits, so assertions are about what a |
| 5 | //! player would see, not about ANSI sequences. | 5 | //! player would see, not about ANSI sequences. |
| 6 | 6 | ||
| 7 | use std::time::Duration; | ||
| 7 | use zagan_bet::blackjack::BlackjackState; | 8 | use zagan_bet::blackjack::BlackjackState; |
| 8 | use zagan_bet::client::core::{ClientCore, Effect, InteractiveCommand, Key}; | 9 | use zagan_bet::client::core::{ClientCore, Effect, InteractiveCommand, Key}; |
| 9 | use zagan_bet::client::games::response::{ | 10 | use zagan_bet::client::games::response::{ |
| @@ -11,7 +12,6 @@ use zagan_bet::client::games::response::{ | |||
| 11 | }; | 12 | }; |
| 12 | use zagan_bet::client::ui::command_names; | 13 | use zagan_bet::client::ui::command_names; |
| 13 | use zagan_bet::crash::CrashState; | 14 | use zagan_bet::crash::CrashState; |
| 14 | use std::time::Duration; | ||
| 15 | 15 | ||
| 16 | const WIN3: &str = "CCCCLLCLODGSDGS"; | 16 | const WIN3: &str = "CCCCLLCLODGSDGS"; |
| 17 | const LOSS: &str = "CLOOGDSLCDCOGLS"; | 17 | const LOSS: &str = "CLOOGDSLCDCOGLS"; |
| @@ -67,10 +67,7 @@ impl Screen { | |||
| 67 | } | 67 | } |
| 68 | Effect::SendLine(line) => sent.push(line.clone()), | 68 | Effect::SendLine(line) => sent.push(line.clone()), |
| 69 | Effect::Interactive(command) => interactive.push(command.clone()), | 69 | Effect::Interactive(command) => interactive.push(command.clone()), |
| 70 | Effect::Quit | 70 | Effect::Quit | Effect::Disconnect | Effect::ListHosts | Effect::ForgetHost(_) => {} |
| 71 | | Effect::Disconnect | ||
| 72 | | Effect::ListHosts | ||
| 73 | | Effect::ForgetHost(_) => {} | ||
| 74 | } | 71 | } |
| 75 | } | 72 | } |
| 76 | } | 73 | } |
| @@ -276,7 +273,8 @@ fn a_mega_win_shows_all_winning_lines() { | |||
| 276 | 273 | ||
| 277 | assert_eq!(h.board().len(), 10); | 274 | assert_eq!(h.board().len(), 10); |
| 278 | assert!(h.board()[7].contains("MEGA WIN! 700 PLN")); | 275 | assert!(h.board()[7].contains("MEGA WIN! 700 PLN")); |
| 279 | assert!(h.board()[8].contains("Winning lines: 1 (top), 2 (middle), 3 (bottom), 4 (V), 5 (inverted V)")); | 276 | assert!(h.board()[8] |
| 277 | .contains("Winning lines: 1 (top), 2 (middle), 3 (bottom), 4 (V), 5 (inverted V)")); | ||
| 280 | assert_eq!(h.board()[2].matches("{ C }").count(), 5); | 278 | assert_eq!(h.board()[2].matches("{ C }").count(), 5); |
| 281 | } | 279 | } |
| 282 | 280 | ||
| @@ -423,10 +421,7 @@ fn help_lists_every_command() { | |||
| 423 | 421 | ||
| 424 | let transcript = h.screen.lines.join("\n"); | 422 | let transcript = h.screen.lines.join("\n"); |
| 425 | for command in command_names() { | 423 | for command in command_names() { |
| 426 | assert!( | 424 | assert!(transcript.contains(command), "help is missing '{command}'"); |
| 427 | transcript.contains(command), | ||
| 428 | "help is missing '{command}'" | ||
| 429 | ); | ||
| 430 | } | 425 | } |
| 431 | } | 426 | } |
| 432 | 427 | ||
| @@ -548,11 +543,7 @@ fn send_prints_the_echo() { | |||
| 548 | h.line("send hello"); | 543 | h.line("send hello"); |
| 549 | h.reply("hello"); | 544 | h.reply("hello"); |
| 550 | 545 | ||
| 551 | assert!(h | 546 | assert!(h.screen.lines.iter().any(|line| line == "Echo: hello")); |
| 552 | .screen | ||
| 553 | .lines | ||
| 554 | .iter() | ||
| 555 | .any(|line| line == "Echo: hello")); | ||
| 556 | } | 547 | } |
| 557 | 548 | ||
| 558 | #[test] | 549 | #[test] |
| @@ -560,11 +551,7 @@ fn disconnect_clears_connection_state() { | |||
| 560 | let mut h = Harness::new(); | 551 | let mut h = Harness::new(); |
| 561 | h.line("disconnect"); | 552 | h.line("disconnect"); |
| 562 | 553 | ||
| 563 | assert!(h | 554 | assert!(h.screen.lines.iter().any(|line| line == "Disconnected")); |
| 564 | .screen | ||
| 565 | .lines | ||
| 566 | .iter() | ||
| 567 | .any(|line| line == "Disconnected")); | ||
| 568 | 555 | ||
| 569 | h.line("play slots"); | 556 | h.line("play slots"); |
| 570 | assert!(h | 557 | assert!(h |
| @@ -636,8 +623,7 @@ fn roulette_split_bets_spin_and_show_the_result() { | |||
| 636 | "PLAY_START roulette 30 {\"red\":10,\"black\":0,\"even\":10,\"odd\":0,\"numbers\":[{\"number\":17,\"amount\":10}]}" | 623 | "PLAY_START roulette 30 {\"red\":10,\"black\":0,\"even\":10,\"odd\":0,\"numbers\":[{\"number\":17,\"amount\":10}]}" |
| 637 | ); | 624 | ); |
| 638 | 625 | ||
| 639 | let bets = | 626 | let bets = r#"{"red":10,"black":0,"even":10,"odd":0,"numbers":[{"number":17,"amount":10}]}"#; |
| 640 | r#"{"red":10,"black":0,"even":10,"odd":0,"numbers":[{"number":17,"amount":10}]}"#; | ||
| 641 | h.reply(&format!( | 627 | h.reply(&format!( |
| 642 | "ROULETTE_RESULT session-1 360 30 1330 17 black {bets}" | 628 | "ROULETTE_RESULT session-1 360 30 1330 17 black {bets}" |
| 643 | )); | 629 | )); |
| @@ -655,7 +641,10 @@ fn roulette_rejects_an_empty_spin_and_accepts_the_equals_chip_key() { | |||
| 655 | 641 | ||
| 656 | h.key(Key::Char(' ')); | 642 | h.key(Key::Char(' ')); |
| 657 | assert!(h.board()[4].contains("ERROR: Place at least one bet")); | 643 | assert!(h.board()[4].contains("ERROR: Place at least one bet")); |
| 658 | assert!(!h.sent.iter().any(|line| line.starts_with("PLAY_START roulette"))); | 644 | assert!(!h |
| 645 | .sent | ||
| 646 | .iter() | ||
| 647 | .any(|line| line.starts_with("PLAY_START roulette"))); | ||
| 659 | 648 | ||
| 660 | h.key(Key::Char('=')); | 649 | h.key(Key::Char('=')); |
| 661 | assert!(h.board()[1].contains("Chip: 20 PLN")); | 650 | assert!(h.board()[1].contains("Chip: 20 PLN")); |
| @@ -716,10 +705,7 @@ fn blackjack_deals_hits_and_settles() { | |||
| 716 | assert!(h.board()[4].contains("Press [SPACE] to deal.")); | 705 | assert!(h.board()[4].contains("Press [SPACE] to deal.")); |
| 717 | 706 | ||
| 718 | h.key(Key::Char(' ')); | 707 | h.key(Key::Char(' ')); |
| 719 | assert!(h | 708 | assert!(h.sent.iter().any(|line| line == "PLAY_START blackjack 10")); |
| 720 | .sent | ||
| 721 | .iter() | ||
| 722 | .any(|line| line == "PLAY_START blackjack 10")); | ||
| 723 | 709 | ||
| 724 | let player = bj_state( | 710 | let player = bj_state( |
| 725 | "bj1", | 711 | "bj1", |
| @@ -847,10 +833,7 @@ fn blackjack_ready_board_can_change_the_next_bet() { | |||
| 847 | assert!(h.board()[1].contains("Bet: 20 PLN")); | 833 | assert!(h.board()[1].contains("Bet: 20 PLN")); |
| 848 | 834 | ||
| 849 | h.key(Key::Char(' ')); | 835 | h.key(Key::Char(' ')); |
| 850 | assert!(h | 836 | assert!(h.sent.iter().any(|line| line == "PLAY_START blackjack 20")); |
| 851 | .sent | ||
| 852 | .iter() | ||
| 853 | .any(|line| line == "PLAY_START blackjack 20")); | ||
| 854 | } | 837 | } |
| 855 | 838 | ||
| 856 | #[test] | 839 | #[test] |
| @@ -875,7 +858,13 @@ fn parses_a_blackjack_state() { | |||
| 875 | assert!(parse_blackjack_response("ERROR Insufficient balance").is_err()); | 858 | assert!(parse_blackjack_response("ERROR Insufficient balance").is_err()); |
| 876 | } | 859 | } |
| 877 | 860 | ||
| 878 | fn crash_state(phase: &str, multiplier: f64, crash_point: Option<f64>, payout: u64, balance: i64) -> CrashState { | 861 | fn crash_state( |
| 862 | phase: &str, | ||
| 863 | multiplier: f64, | ||
| 864 | crash_point: Option<f64>, | ||
| 865 | payout: u64, | ||
| 866 | balance: i64, | ||
| 867 | ) -> CrashState { | ||
| 879 | CrashState { | 868 | CrashState { |
| 880 | session_id: "cr1".to_string(), | 869 | session_id: "cr1".to_string(), |
| 881 | phase: phase.to_string(), | 870 | phase: phase.to_string(), |
| @@ -920,10 +909,7 @@ fn crash_starts_a_round_and_draws_the_multiplier_locally() { | |||
| 920 | assert!(h.board()[3].contains("Potential: 20 PLN")); | 909 | assert!(h.board()[3].contains("Potential: 20 PLN")); |
| 921 | 910 | ||
| 922 | h.key(Key::Char('c')); | 911 | h.key(Key::Char('c')); |
| 923 | assert_eq!( | 912 | assert_eq!(h.sent.last().map(String::as_str), Some("CRASH_CASHOUT cr1")); |
| 924 | h.sent.last().map(String::as_str), | ||
| 925 | Some("CRASH_CASHOUT cr1") | ||
| 926 | ); | ||
| 927 | 913 | ||
| 928 | let cashed = crash_state("cashed", 2.0, Some(3.0), 20, 1010); | 914 | let cashed = crash_state("cashed", 2.0, Some(3.0), 20, 1010); |
| 929 | h.reply(&crash_line("CRASH_STATE", &cashed)); | 915 | h.reply(&crash_line("CRASH_STATE", &cashed)); |
| @@ -967,10 +953,7 @@ fn crash_q_forfeits_an_active_round() { | |||
| 967 | h.reply(&crash_line("CRASH_START", &running)); | 953 | h.reply(&crash_line("CRASH_START", &running)); |
| 968 | 954 | ||
| 969 | h.key(Key::Char('q')); | 955 | h.key(Key::Char('q')); |
| 970 | assert_eq!( | 956 | assert_eq!(h.sent.last().map(String::as_str), Some("CRASH_FORFEIT cr1")); |
| 971 | h.sent.last().map(String::as_str), | ||
| 972 | Some("CRASH_FORFEIT cr1") | ||
| 973 | ); | ||
| 974 | 957 | ||
| 975 | let forfeited = crash_state("forfeit", 1.2, Some(2.0), 0, 990); | 958 | let forfeited = crash_state("forfeit", 1.2, Some(2.0), 0, 990); |
| 976 | h.reply(&crash_line("CRASH_STATE", &forfeited)); | 959 | h.reply(&crash_line("CRASH_STATE", &forfeited)); |
diff --git a/tests/client_ui.rs b/tests/client_ui.rs index 7a1b675..44febde 100644 --- a/tests/client_ui.rs +++ b/tests/client_ui.rs | |||
| @@ -1,9 +1,9 @@ | |||
| 1 | use std::time::Duration; | ||
| 1 | use zagan_bet::client::ui::{ | 2 | use zagan_bet::client::ui::{ |
| 2 | command_names, completion_lines, display_width, first_contact_lines, format_pong, | 3 | command_names, completion_lines, display_width, first_contact_lines, format_pong, help_lines, |
| 3 | help_lines, host_lines, input_cursor_column, lines_to_move_up, parse_bet_arg, record_history, | 4 | host_lines, input_cursor_column, lines_to_move_up, parse_bet_arg, record_history, |
| 4 | visual_line_count, | 5 | visual_line_count, |
| 5 | }; | 6 | }; |
| 6 | use std::time::Duration; | ||
| 7 | 7 | ||
| 8 | #[test] | 8 | #[test] |
| 9 | fn help_has_no_source_indentation() { | 9 | fn help_has_no_source_indentation() { |
| @@ -27,13 +27,7 @@ fn help_documents_every_command() { | |||
| 27 | fn completion_knows_every_command() { | 27 | fn completion_knows_every_command() { |
| 28 | let commands = command_names(); | 28 | let commands = command_names(); |
| 29 | for expected in [ | 29 | for expected in [ |
| 30 | "connect", | 30 | "connect", "games", "play", "hosts", "send", "logout", "quit", |
| 31 | "games", | ||
| 32 | "play", | ||
| 33 | "hosts", | ||
| 34 | "send", | ||
| 35 | "logout", | ||
| 36 | "quit", | ||
| 37 | ] { | 31 | ] { |
| 38 | assert!( | 32 | assert!( |
| 39 | commands.contains(&expected), | 33 | commands.contains(&expected), |
| @@ -109,19 +103,12 @@ fn display_width_ignores_ansi_sequences() { | |||
| 109 | fn visual_line_count_ignores_ansi_sequences() { | 103 | fn visual_line_count_ignores_ansi_sequences() { |
| 110 | let styled = format!("\x1b[31m{}\x1b[0m", "x".repeat(75)); | 104 | let styled = format!("\x1b[31m{}\x1b[0m", "x".repeat(75)); |
| 111 | assert_eq!(visual_line_count(&[styled], 72), 2); | 105 | assert_eq!(visual_line_count(&[styled], 72), 2); |
| 112 | assert_eq!( | 106 | assert_eq!(visual_line_count(&["\x1b[31m18\x1b[0m".to_string()], 80), 1); |
| 113 | visual_line_count(&["\x1b[31m18\x1b[0m".to_string()], 80), | ||
| 114 | 1 | ||
| 115 | ); | ||
| 116 | } | 107 | } |
| 117 | 108 | ||
| 118 | #[test] | 109 | #[test] |
| 119 | fn visual_line_count_accounts_for_terminal_wrapping() { | 110 | fn visual_line_count_accounts_for_terminal_wrapping() { |
| 120 | let lines = vec![ | 111 | let lines = vec!["12345".to_string(), "123456".to_string(), String::new()]; |
| 121 | "12345".to_string(), | ||
| 122 | "123456".to_string(), | ||
| 123 | String::new(), | ||
| 124 | ]; | ||
| 125 | 112 | ||
| 126 | assert_eq!(visual_line_count(&lines, 5), 4); // 1 + 2 + 1 | 113 | assert_eq!(visual_line_count(&lines, 5), 4); // 1 + 2 + 1 |
| 127 | assert_eq!(visual_line_count(&lines, 6), 3); // 1 + 1 + 1 | 114 | assert_eq!(visual_line_count(&lines, 6), 3); // 1 + 1 + 1 |
| @@ -146,7 +133,10 @@ fn completions_wrap_within_the_requested_width() { | |||
| 146 | let commands = command_names(); | 133 | let commands = command_names(); |
| 147 | let lines = completion_lines(&commands, 40); | 134 | let lines = completion_lines(&commands, 40); |
| 148 | 135 | ||
| 149 | assert_eq!(lines.first().map(String::as_str), Some("Possible completions:")); | 136 | assert_eq!( |
| 137 | lines.first().map(String::as_str), | ||
| 138 | Some("Possible completions:") | ||
| 139 | ); | ||
| 150 | for line in &lines[1..] { | 140 | for line in &lines[1..] { |
| 151 | assert!( | 141 | assert!( |
| 152 | line.len() <= 40, | 142 | line.len() <= 40, |
| @@ -208,10 +198,7 @@ fn pong_is_formatted_to_one_decimal() { | |||
| 208 | fn host_lines_fit_an_80_column_terminal() { | 198 | fn host_lines_fit_an_80_column_terminal() { |
| 209 | let fingerprint = "a4d6f80784154c2cb739a1df61ab3e9038ed731ee2e05c7611457ac741b3834c"; | 199 | let fingerprint = "a4d6f80784154c2cb739a1df61ab3e9038ed731ee2e05c7611457ac741b3834c"; |
| 210 | let lines = host_lines(&[ | 200 | let lines = host_lines(&[ |
| 211 | ( | 201 | ("127.0.0.1:11525".to_string(), Some(fingerprint.to_string())), |
| 212 | "127.0.0.1:11525".to_string(), | ||
| 213 | Some(fingerprint.to_string()), | ||
| 214 | ), | ||
| 215 | ("unknown.example:443".to_string(), None), | 202 | ("unknown.example:443".to_string(), None), |
| 216 | ]); | 203 | ]); |
| 217 | 204 | ||
| @@ -233,10 +220,7 @@ fn hosts_sharing_a_fingerprint_are_shown_as_one_server() { | |||
| 233 | let lines = host_lines(&[ | 220 | let lines = host_lines(&[ |
| 234 | ("10.0.0.100:11525".to_string(), Some(key.to_string())), | 221 | ("10.0.0.100:11525".to_string(), Some(key.to_string())), |
| 235 | ("127.0.0.1:11525".to_string(), Some(key.to_string())), | 222 | ("127.0.0.1:11525".to_string(), Some(key.to_string())), |
| 236 | ( | 223 | ("other.example:11525".to_string(), Some("ff".repeat(32))), |
| 237 | "other.example:11525".to_string(), | ||
| 238 | Some("ff".repeat(32)), | ||
| 239 | ), | ||
| 240 | ]); | 224 | ]); |
| 241 | 225 | ||
| 242 | assert_eq!( | 226 | assert_eq!( |
| @@ -264,7 +248,10 @@ fn hosts_without_a_fingerprint_are_never_merged() { | |||
| 264 | assert!(lines.contains(&" a.example:443".to_string())); | 248 | assert!(lines.contains(&" a.example:443".to_string())); |
| 265 | assert!(lines.contains(&" b.example:443".to_string())); | 249 | assert!(lines.contains(&" b.example:443".to_string())); |
| 266 | assert_eq!( | 250 | assert_eq!( |
| 267 | lines.iter().filter(|line| line.contains("no fingerprint")).count(), | 251 | lines |
| 252 | .iter() | ||
| 253 | .filter(|line| line.contains("no fingerprint")) | ||
| 254 | .count(), | ||
| 268 | 2 | 255 | 2 |
| 269 | ); | 256 | ); |
| 270 | } | 257 | } |
diff --git a/tests/crash.rs b/tests/crash.rs index f59c52f..49eaf89 100644 --- a/tests/crash.rs +++ b/tests/crash.rs | |||
| @@ -18,7 +18,11 @@ fn the_multiplier_doubles_every_five_seconds() { | |||
| 18 | close(multiplier_at(Duration::ZERO), 1.0, 1e-9); | 18 | close(multiplier_at(Duration::ZERO), 1.0, 1e-9); |
| 19 | close(multiplier_at(Duration::from_secs(5)), 2.0, 1e-9); | 19 | close(multiplier_at(Duration::from_secs(5)), 2.0, 1e-9); |
| 20 | close(multiplier_at(Duration::from_secs(10)), 4.0, 1e-6); | 20 | close(multiplier_at(Duration::from_secs(10)), 4.0, 1e-6); |
| 21 | close(multiplier_at(Duration::from_millis(2500)), 1.414_213_562, 1e-6); | 21 | close( |
| 22 | multiplier_at(Duration::from_millis(2500)), | ||
| 23 | 1.414_213_562, | ||
| 24 | 1e-6, | ||
| 25 | ); | ||
| 22 | 26 | ||
| 23 | // Never below 1x, even for nonsense input. | 27 | // Never below 1x, even for nonsense input. |
| 24 | assert_eq!(multiplier_at_secs(-3.0), 1.0); | 28 | assert_eq!(multiplier_at_secs(-3.0), 1.0); |
diff --git a/tests/protocol.rs b/tests/protocol.rs index 2962574..bf8a4fd 100644 --- a/tests/protocol.rs +++ b/tests/protocol.rs | |||
| @@ -40,8 +40,14 @@ fn reads_multiple_lines_from_a_reader() { | |||
| 40 | let mut buffer = LineBuffer::new(); | 40 | let mut buffer = LineBuffer::new(); |
| 41 | let mut reader = Cursor::new(b"one\ntwo\n".to_vec()); | 41 | let mut reader = Cursor::new(b"one\ntwo\n".to_vec()); |
| 42 | 42 | ||
| 43 | assert_eq!(buffer.read_line(&mut reader).unwrap().as_deref(), Some("one")); | 43 | assert_eq!( |
| 44 | assert_eq!(buffer.read_line(&mut reader).unwrap().as_deref(), Some("two")); | 44 | buffer.read_line(&mut reader).unwrap().as_deref(), |
| 45 | Some("one") | ||
| 46 | ); | ||
| 47 | assert_eq!( | ||
| 48 | buffer.read_line(&mut reader).unwrap().as_deref(), | ||
| 49 | Some("two") | ||
| 50 | ); | ||
| 45 | assert_eq!(buffer.read_line(&mut reader).unwrap(), None); | 51 | assert_eq!(buffer.read_line(&mut reader).unwrap(), None); |
| 46 | } | 52 | } |
| 47 | 53 | ||
diff --git a/tests/roulette.rs b/tests/roulette.rs index 6b99d17..932bace 100644 --- a/tests/roulette.rs +++ b/tests/roulette.rs | |||
| @@ -1,6 +1,4 @@ | |||
| 1 | use zagan_bet::roulette::{ | 1 | use zagan_bet::roulette::{color, is_black, is_even, is_odd, is_red, rtp, Bets, NumberBet, WHEEL}; |
| 2 | color, is_black, is_even, is_odd, is_red, rtp, Bets, NumberBet, WHEEL, | ||
| 3 | }; | ||
| 4 | use zagan_bet::server::games::roulette::RouletteGame; | 2 | use zagan_bet::server::games::roulette::RouletteGame; |
| 5 | use zagan_bet::server::games::traits::Game; | 3 | use zagan_bet::server::games::traits::Game; |
| 6 | 4 | ||
| @@ -171,14 +169,13 @@ fn rejects_bad_roulette_results() { | |||
| 171 | use zagan_bet::client::games::response::parse_roulette_response; | 169 | use zagan_bet::client::games::response::parse_roulette_response; |
| 172 | 170 | ||
| 173 | let valid_bets = r#"{"red":10}"#; | 171 | let valid_bets = r#"{"red":10}"#; |
| 174 | assert!(parse_roulette_response(&format!( | 172 | assert!( |
| 175 | "ROULETTE_RESULT s 1 1 1 37 green {valid_bets}" | 173 | parse_roulette_response(&format!("ROULETTE_RESULT s 1 1 1 37 green {valid_bets}")).is_err() |
| 176 | )) | 174 | ); |
| 177 | .is_err()); | 175 | assert!( |
| 178 | assert!(parse_roulette_response(&format!( | 176 | parse_roulette_response(&format!("ROULETTE_RESULT s 1 1 1 17 purple {valid_bets}")) |
| 179 | "ROULETTE_RESULT s 1 1 1 17 purple {valid_bets}" | 177 | .is_err() |
| 180 | )) | 178 | ); |
| 181 | .is_err()); | ||
| 182 | assert!(parse_roulette_response("ROULETTE_RESULT s 1 1 1 17 black nope").is_err()); | 179 | assert!(parse_roulette_response("ROULETTE_RESULT s 1 1 1 17 black nope").is_err()); |
| 183 | assert!(parse_roulette_response("").is_err()); | 180 | assert!(parse_roulette_response("").is_err()); |
| 184 | } | 181 | } |
diff --git a/tests/server_core.rs b/tests/server_core.rs index 0350fb6..6b7b415 100644 --- a/tests/server_core.rs +++ b/tests/server_core.rs | |||
| @@ -4,13 +4,13 @@ use std::sync::Arc; | |||
| 4 | use rusqlite::Connection; | 4 | use rusqlite::Connection; |
| 5 | use zagan_bet::blackjack::BlackjackState; | 5 | use zagan_bet::blackjack::BlackjackState; |
| 6 | use zagan_bet::crash::CrashState; | 6 | use zagan_bet::crash::CrashState; |
| 7 | use zagan_bet::roulette::Bets; | ||
| 7 | use zagan_bet::server::core::{ConnectionState, ServerCore}; | 8 | use zagan_bet::server::core::{ConnectionState, ServerCore}; |
| 8 | use zagan_bet::server::database; | 9 | use zagan_bet::server::database; |
| 9 | use zagan_bet::server::games::crash::ManualClock; | 10 | use zagan_bet::server::games::crash::ManualClock; |
| 10 | use zagan_bet::server::games::registry::GameRegistry; | 11 | use zagan_bet::server::games::registry::GameRegistry; |
| 11 | use zagan_bet::server::games::roulette::RouletteGame; | 12 | use zagan_bet::server::games::roulette::RouletteGame; |
| 12 | use zagan_bet::server::games::slots::SlotsGame; | 13 | use zagan_bet::server::games::slots::SlotsGame; |
| 13 | use zagan_bet::roulette::Bets; | ||
| 14 | 14 | ||
| 15 | fn setup(name: &str, grid: &str) -> (ServerCore, String) { | 15 | fn setup(name: &str, grid: &str) -> (ServerCore, String) { |
| 16 | let path = PathBuf::from(env!("CARGO_TARGET_TMPDIR")) | 16 | let path = PathBuf::from(env!("CARGO_TARGET_TMPDIR")) |
| @@ -63,8 +63,7 @@ fn setup_blackjack(name: &str, deck: &[&str]) -> (ServerCore, String) { | |||
| 63 | 63 | ||
| 64 | let deck = deck.iter().map(|card| card.to_string()).collect(); | 64 | let deck = deck.iter().map(|card| card.to_string()).collect(); |
| 65 | ( | 65 | ( |
| 66 | ServerCore::with_registry(conn, GameRegistry::with_defaults(), 4) | 66 | ServerCore::with_registry(conn, GameRegistry::with_defaults(), 4).with_blackjack_deck(deck), |
| 67 | .with_blackjack_deck(deck), | ||
| 68 | path, | 67 | path, |
| 69 | ) | 68 | ) |
| 70 | } | 69 | } |
| @@ -216,7 +215,10 @@ fn plays_record_the_game_type_and_game_specific_details() { | |||
| 216 | login(&mut core, &mut state, "alice"); | 215 | login(&mut core, &mut state, "alice"); |
| 217 | 216 | ||
| 218 | let response = core.handle_message(&mut state, "PLAY_START slots 10"); | 217 | let response = core.handle_message(&mut state, "PLAY_START slots 10"); |
| 219 | assert!(response.starts_with("GAME_RESULT"), "spin failed: {response}"); | 218 | assert!( |
| 219 | response.starts_with("GAME_RESULT"), | ||
| 220 | "spin failed: {response}" | ||
| 221 | ); | ||
| 220 | 222 | ||
| 221 | let conn = Connection::open(&path).unwrap(); | 223 | let conn = Connection::open(&path).unwrap(); |
| 222 | let (game_type, details): (String, String) = conn | 224 | let (game_type, details): (String, String) = conn |
| @@ -242,8 +244,7 @@ fn roulette_settles_split_bets_and_records_details() { | |||
| 242 | register(&mut core, &mut state, "alice"); | 244 | register(&mut core, &mut state, "alice"); |
| 243 | login(&mut core, &mut state, "alice"); | 245 | login(&mut core, &mut state, "alice"); |
| 244 | 246 | ||
| 245 | let bets = | 247 | let bets = r#"{"red":10,"black":10,"even":10,"odd":10,"numbers":[{"number":17,"amount":5}]}"#; |
| 246 | r#"{"red":10,"black":10,"even":10,"odd":10,"numbers":[{"number":17,"amount":5}]}"#; | ||
| 247 | let response = core.handle_message(&mut state, &format!("PLAY_START roulette 45 {bets}")); | 248 | let response = core.handle_message(&mut state, &format!("PLAY_START roulette 45 {bets}")); |
| 248 | 249 | ||
| 249 | assert!( | 250 | assert!( |
| @@ -254,7 +255,10 @@ fn roulette_settles_split_bets_and_records_details() { | |||
| 254 | response.contains(" 220 45 1175 17 black "), | 255 | response.contains(" 220 45 1175 17 black "), |
| 255 | "response: {response}" | 256 | "response: {response}" |
| 256 | ); | 257 | ); |
| 257 | assert!(response.ends_with(&format!(" {bets}")), "response: {response}"); | 258 | assert!( |
| 259 | response.ends_with(&format!(" {bets}")), | ||
| 260 | "response: {response}" | ||
| 261 | ); | ||
| 258 | assert_eq!(core.balance("alice"), Some(1175)); | 262 | assert_eq!(core.balance("alice"), Some(1175)); |
| 259 | assert_eq!(core.play_count("alice"), 1); | 263 | assert_eq!(core.play_count("alice"), 1); |
| 260 | assert_eq!(core.session_count(), 0); | 264 | assert_eq!(core.session_count(), 0); |
| @@ -327,7 +331,10 @@ fn a_winning_spin_updates_the_balance_and_the_audit_row() { | |||
| 327 | login(&mut core, &mut state, "alice"); | 331 | login(&mut core, &mut state, "alice"); |
| 328 | 332 | ||
| 329 | let response = core.handle_message(&mut state, "PLAY_START slots 10"); | 333 | let response = core.handle_message(&mut state, "PLAY_START slots 10"); |
| 330 | assert!(response.starts_with("GAME_RESULT"), "spin failed: {response}"); | 334 | assert!( |
| 335 | response.starts_with("GAME_RESULT"), | ||
| 336 | "spin failed: {response}" | ||
| 337 | ); | ||
| 331 | assert!(response.contains(" 10 10 1000 "), "response: {response}"); | 338 | assert!(response.contains(" 10 10 1000 "), "response: {response}"); |
| 332 | assert!(response.ends_with(" win 1 0,3,6"), "response: {response}"); | 339 | assert!(response.ends_with(" win 1 0,3,6"), "response: {response}"); |
| 333 | 340 | ||
| @@ -364,11 +371,8 @@ fn a_negative_balance_is_rejected_without_wrapping() { | |||
| 364 | login(&mut core, &mut state, "alice"); | 371 | login(&mut core, &mut state, "alice"); |
| 365 | 372 | ||
| 366 | let conn = Connection::open(&path).unwrap(); | 373 | let conn = Connection::open(&path).unwrap(); |
| 367 | conn.execute( | 374 | conn.execute("UPDATE users SET balance = -5 WHERE username = 'alice'", []) |
| 368 | "UPDATE users SET balance = -5 WHERE username = 'alice'", | 375 | .unwrap(); |
| 369 | [], | ||
| 370 | ) | ||
| 371 | .unwrap(); | ||
| 372 | 376 | ||
| 373 | assert_eq!( | 377 | assert_eq!( |
| 374 | core.handle_message(&mut state, "PLAY_START slots 10"), | 378 | core.handle_message(&mut state, "PLAY_START slots 10"), |
| @@ -394,10 +398,7 @@ fn unknown_games_are_rejected_before_a_session_is_created() { | |||
| 394 | 398 | ||
| 395 | #[test] | 399 | #[test] |
| 396 | fn blackjack_reserves_the_bet_and_settles_a_hit_hand() { | 400 | fn blackjack_reserves_the_bet_and_settles_a_hit_hand() { |
| 397 | let (mut core, path) = setup_blackjack( | 401 | let (mut core, path) = setup_blackjack("blackjack-hit", &["10S", "9D", "6H", "8C", "5D"]); |
| 398 | "blackjack-hit", | ||
| 399 | &["10S", "9D", "6H", "8C", "5D"], | ||
| 400 | ); | ||
| 401 | let mut state = ConnectionState::default(); | 402 | let mut state = ConnectionState::default(); |
| 402 | register(&mut core, &mut state, "alice"); | 403 | register(&mut core, &mut state, "alice"); |
| 403 | login(&mut core, &mut state, "alice"); | 404 | login(&mut core, &mut state, "alice"); |
| @@ -442,10 +443,7 @@ fn blackjack_reserves_the_bet_and_settles_a_hit_hand() { | |||
| 442 | 443 | ||
| 443 | #[test] | 444 | #[test] |
| 444 | fn blackjack_double_reserves_an_extra_bet() { | 445 | fn blackjack_double_reserves_an_extra_bet() { |
| 445 | let (mut core, path) = setup_blackjack( | 446 | let (mut core, path) = setup_blackjack("blackjack-double", &["5S", "9D", "5H", "8C", "10D"]); |
| 446 | "blackjack-double", | ||
| 447 | &["5S", "9D", "5H", "8C", "10D"], | ||
| 448 | ); | ||
| 449 | let mut state = ConnectionState::default(); | 447 | let mut state = ConnectionState::default(); |
| 450 | register(&mut core, &mut state, "alice"); | 448 | register(&mut core, &mut state, "alice"); |
| 451 | login(&mut core, &mut state, "alice"); | 449 | login(&mut core, &mut state, "alice"); |
| @@ -483,8 +481,7 @@ fn blackjack_double_reserves_an_extra_bet() { | |||
| 483 | 481 | ||
| 484 | #[test] | 482 | #[test] |
| 485 | fn blackjack_natural_settles_immediately() { | 483 | fn blackjack_natural_settles_immediately() { |
| 486 | let (mut core, _path) = | 484 | let (mut core, _path) = setup_blackjack("blackjack-natural", &["AS", "9D", "KH", "8C"]); |
| 487 | setup_blackjack("blackjack-natural", &["AS", "9D", "KH", "8C"]); | ||
| 488 | let mut state = ConnectionState::default(); | 485 | let mut state = ConnectionState::default(); |
| 489 | register(&mut core, &mut state, "alice"); | 486 | register(&mut core, &mut state, "alice"); |
| 490 | login(&mut core, &mut state, "alice"); | 487 | login(&mut core, &mut state, "alice"); |
| @@ -501,8 +498,7 @@ fn blackjack_natural_settles_immediately() { | |||
| 501 | 498 | ||
| 502 | #[test] | 499 | #[test] |
| 503 | fn blackjack_forfeit_keeps_the_reserved_bet() { | 500 | fn blackjack_forfeit_keeps_the_reserved_bet() { |
| 504 | let (mut core, _path) = | 501 | let (mut core, _path) = setup_blackjack("blackjack-cancel", &["10S", "9D", "6H", "8C"]); |
| 505 | setup_blackjack("blackjack-cancel", &["10S", "9D", "6H", "8C"]); | ||
| 506 | let mut state = ConnectionState::default(); | 502 | let mut state = ConnectionState::default(); |
| 507 | register(&mut core, &mut state, "alice"); | 503 | register(&mut core, &mut state, "alice"); |
| 508 | login(&mut core, &mut state, "alice"); | 504 | login(&mut core, &mut state, "alice"); |
| @@ -524,8 +520,7 @@ fn blackjack_forfeit_keeps_the_reserved_bet() { | |||
| 524 | 520 | ||
| 525 | #[test] | 521 | #[test] |
| 526 | fn blackjack_disconnect_forfeits_the_reserved_bet() { | 522 | fn blackjack_disconnect_forfeits_the_reserved_bet() { |
| 527 | let (mut core, _path) = | 523 | let (mut core, _path) = setup_blackjack("blackjack-disconnect", &["10S", "9D", "6H", "8C"]); |
| 528 | setup_blackjack("blackjack-disconnect", &["10S", "9D", "6H", "8C"]); | ||
| 529 | let mut state = ConnectionState::default(); | 524 | let mut state = ConnectionState::default(); |
| 530 | register(&mut core, &mut state, "alice"); | 525 | register(&mut core, &mut state, "alice"); |
| 531 | login(&mut core, &mut state, "alice"); | 526 | login(&mut core, &mut state, "alice"); |
| @@ -578,10 +573,9 @@ fn crash_cashout_pays_the_multiplier_from_the_server_clock() { | |||
| 578 | 573 | ||
| 579 | // Five seconds of round time: 2.00x, still below the 3.00x crash point. | 574 | // Five seconds of round time: 2.00x, still below the 3.00x crash point. |
| 580 | clock.advance(5.0); | 575 | clock.advance(5.0); |
| 581 | let cashed = crash_state(&core.handle_message( | 576 | let cashed = crash_state( |
| 582 | &mut state, | 577 | &core.handle_message(&mut state, &format!("CRASH_CASHOUT {}", started.session_id)), |
| 583 | &format!("CRASH_CASHOUT {}", started.session_id), | 578 | ); |
| 584 | )); | ||
| 585 | 579 | ||
| 586 | assert_eq!(cashed.phase, "cashed"); | 580 | assert_eq!(cashed.phase, "cashed"); |
| 587 | assert_eq!(cashed.multiplier, 2.0); | 581 | assert_eq!(cashed.multiplier, 2.0); |
| @@ -600,7 +594,15 @@ fn crash_cashout_pays_the_multiplier_from_the_server_clock() { | |||
| 600 | "SELECT game_type, bet_amount, winnings, balance_after, details | 594 | "SELECT game_type, bet_amount, winnings, balance_after, details |
| 601 | FROM plays WHERE username = 'alice'", | 595 | FROM plays WHERE username = 'alice'", |
| 602 | [], | 596 | [], |
| 603 | |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?, row.get(3)?, row.get(4)?)), | 597 | |row| { |
| 598 | Ok(( | ||
| 599 | row.get(0)?, | ||
| 600 | row.get(1)?, | ||
| 601 | row.get(2)?, | ||
| 602 | row.get(3)?, | ||
| 603 | row.get(4)?, | ||
| 604 | )) | ||
| 605 | }, | ||
| 604 | ) | 606 | ) |
| 605 | .unwrap(); | 607 | .unwrap(); |
| 606 | assert_eq!(game_type, "crash"); | 608 | assert_eq!(game_type, "crash"); |
| @@ -627,10 +629,9 @@ fn a_cashout_after_the_crash_time_loses_the_reserved_bet() { | |||
| 627 | let started = crash_state(&core.handle_message(&mut state, "PLAY_START crash 10")); | 629 | let started = crash_state(&core.handle_message(&mut state, "PLAY_START crash 10")); |
| 628 | 630 | ||
| 629 | clock.advance(20.0); | 631 | clock.advance(20.0); |
| 630 | let late = crash_state(&core.handle_message( | 632 | let late = crash_state( |
| 631 | &mut state, | 633 | &core.handle_message(&mut state, &format!("CRASH_CASHOUT {}", started.session_id)), |
| 632 | &format!("CRASH_CASHOUT {}", started.session_id), | 634 | ); |
| 633 | )); | ||
| 634 | 635 | ||
| 635 | assert_eq!(late.phase, "crashed"); | 636 | assert_eq!(late.phase, "crashed"); |
| 636 | assert_eq!(late.payout, 0); | 637 | assert_eq!(late.payout, 0); |
| @@ -679,10 +680,9 @@ fn crash_forfeit_keeps_the_reserved_bet() { | |||
| 679 | login(&mut core, &mut state, "alice"); | 680 | login(&mut core, &mut state, "alice"); |
| 680 | 681 | ||
| 681 | let started = crash_state(&core.handle_message(&mut state, "PLAY_START crash 10")); | 682 | let started = crash_state(&core.handle_message(&mut state, "PLAY_START crash 10")); |
| 682 | let forfeited = crash_state(&core.handle_message( | 683 | let forfeited = crash_state( |
| 683 | &mut state, | 684 | &core.handle_message(&mut state, &format!("CRASH_FORFEIT {}", started.session_id)), |
| 684 | &format!("CRASH_FORFEIT {}", started.session_id), | 685 | ); |
| 685 | )); | ||
| 686 | 686 | ||
| 687 | assert_eq!(forfeited.phase, "forfeit"); | 687 | assert_eq!(forfeited.phase, "forfeit"); |
| 688 | assert_eq!(forfeited.payout, 0); | 688 | assert_eq!(forfeited.payout, 0); |
| @@ -747,10 +747,7 @@ fn crash_rounds_are_owned_by_the_player_who_started_them() { | |||
| 747 | 747 | ||
| 748 | clock.advance(5.0); | 748 | clock.advance(5.0); |
| 749 | assert_eq!( | 749 | assert_eq!( |
| 750 | core.handle_message( | 750 | core.handle_message(&mut bob, &format!("CRASH_CASHOUT {}", started.session_id)), |
| 751 | &mut bob, | ||
| 752 | &format!("CRASH_CASHOUT {}", started.session_id) | ||
| 753 | ), | ||
| 754 | "ERROR Invalid session" | 751 | "ERROR Invalid session" |
| 755 | ); | 752 | ); |
| 756 | assert_eq!(core.balance("bob"), Some(1000)); | 753 | assert_eq!(core.balance("bob"), Some(1000)); |
| @@ -775,10 +772,9 @@ fn a_crash_reservation_is_durable_until_the_round_settles() { | |||
| 775 | drop(conn); | 772 | drop(conn); |
| 776 | 773 | ||
| 777 | clock.advance(5.0); | 774 | clock.advance(5.0); |
| 778 | let cashed = crash_state(&core.handle_message( | 775 | let cashed = crash_state( |
| 779 | &mut state, | 776 | &core.handle_message(&mut state, &format!("CRASH_CASHOUT {}", started.session_id)), |
| 780 | &format!("CRASH_CASHOUT {}", started.session_id), | 777 | ); |
| 781 | )); | ||
| 782 | assert_eq!(cashed.phase, "cashed"); | 778 | assert_eq!(cashed.phase, "cashed"); |
| 783 | 779 | ||
| 784 | let conn = Connection::open(&path).unwrap(); | 780 | let conn = Connection::open(&path).unwrap(); |
diff --git a/tests/slots.rs b/tests/slots.rs index 8bdd9af..7f0b6b5 100644 --- a/tests/slots.rs +++ b/tests/slots.rs | |||
| @@ -1,8 +1,8 @@ | |||
| 1 | //! Regression tests for the server-side 5x3 slots game. | 1 | //! Regression tests for the server-side 5x3 slots game. |
| 2 | 2 | ||
| 3 | use zagan_bet::slots::{paytable_lines, LINE_NAMES, SHAPE_MULTIPLIERS, SYMBOL_MULTIPLIERS}; | ||
| 4 | use zagan_bet::server::games::slots::{LineWin, SlotsGame, PAYLINES, REELS, ROWS}; | 3 | use zagan_bet::server::games::slots::{LineWin, SlotsGame, PAYLINES, REELS, ROWS}; |
| 5 | use zagan_bet::server::games::traits::{Game, GamePhase}; | 4 | use zagan_bet::server::games::traits::{Game, GamePhase}; |
| 5 | use zagan_bet::slots::{paytable_lines, LINE_NAMES, SHAPE_MULTIPLIERS, SYMBOL_MULTIPLIERS}; | ||
| 6 | 6 | ||
| 7 | /// Convert row-major test rows into the column-major wire format. | 7 | /// Convert row-major test rows into the column-major wire format. |
| 8 | fn grid(rows: [&str; 3]) -> String { | 8 | fn grid(rows: [&str; 3]) -> String { |
| @@ -62,7 +62,10 @@ fn four_of_a_kind_pays_more_than_three() { | |||
| 62 | fn five_of_a_kind_pays_all_lines() { | 62 | fn five_of_a_kind_pays_all_lines() { |
| 63 | let (winnings, wins) = payout(["SSSSS", "SSSSS", "SSSSS"], 10); | 63 | let (winnings, wins) = payout(["SSSSS", "SSSSS", "SSSSS"], 10); |
| 64 | 64 | ||
| 65 | assert_eq!(winnings, 7000, "five SSSSS lines: 5 * 10 * S(10) * shape5(14)"); | 65 | assert_eq!( |
| 66 | winnings, 7000, | ||
| 67 | "five SSSSS lines: 5 * 10 * S(10) * shape5(14)" | ||
| 68 | ); | ||
| 66 | assert_eq!(lines(&wins), vec![1, 2, 3, 4, 5]); | 69 | assert_eq!(lines(&wins), vec![1, 2, 3, 4, 5]); |
| 67 | assert_eq!(wins[0].cells, vec![0, 3, 6, 9, 12]); | 70 | assert_eq!(wins[0].cells, vec![0, 3, 6, 9, 12]); |
| 68 | assert_eq!(wins[3].cells, vec![0, 4, 8, 10, 12]); | 71 | assert_eq!(wins[3].cells, vec![0, 4, 8, 10, 12]); |
diff --git a/tests/tofu.rs b/tests/tofu.rs index 4f59d6d..b644cc4 100644 --- a/tests/tofu.rs +++ b/tests/tofu.rs | |||
| @@ -74,7 +74,10 @@ fn fingerprint_is_sha256_of_spki() { | |||
| 74 | let expected = openssl_spki_sha256(dir.join("primary.pem").to_str().unwrap()); | 74 | let expected = openssl_spki_sha256(dir.join("primary.pem").to_str().unwrap()); |
| 75 | 75 | ||
| 76 | let fp = spki_fingerprint(&der).expect("must parse the test certificate"); | 76 | let fp = spki_fingerprint(&der).expect("must parse the test certificate"); |
| 77 | assert_eq!(fp, expected, "fingerprint must equal openssl's SPKI SHA-256"); | 77 | assert_eq!( |
| 78 | fp, expected, | ||
| 79 | "fingerprint must equal openssl's SPKI SHA-256" | ||
| 80 | ); | ||
| 78 | } | 81 | } |
| 79 | 82 | ||
| 80 | #[test] | 83 | #[test] |
| @@ -90,10 +93,7 @@ fn first_contact_trusts_and_records() { | |||
| 90 | 93 | ||
| 91 | #[test] | 94 | #[test] |
| 92 | fn matching_key_is_trusted() { | 95 | fn matching_key_is_trusted() { |
| 93 | assert_eq!( | 96 | assert_eq!(decide(Some("ab12cd34"), "ab12cd34"), TrustDecision::Trusted); |
| 94 | decide(Some("ab12cd34"), "ab12cd34"), | ||
| 95 | TrustDecision::Trusted | ||
| 96 | ); | ||
| 97 | } | 97 | } |
| 98 | 98 | ||
| 99 | #[test] | 99 | #[test] |
