summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorsalmonik <salmonik@tuta.io>2026-10-28 22:46:47 +0100
committersalmonik <salmonik@tuta.io>2026-10-28 22:46:47 +0100
commit47d6c82a4943b1e804d062780b8e05f1e42f27b0 (patch)
tree11d0d557025118c7deb021eb9ad425fcc8b1e918
parentv0.3.0: add server core, bcrypt migration and plays audit (diff)
downloadzagan-bet-47d6c82a4943b1e804d062780b8e05f1e42f27b0.tar.gz
zagan-bet-47d6c82a4943b1e804d062780b8e05f1e42f27b0.zip
v0.3.1: clean up dependencies, stale tests and document verification
-rw-r--r--Cargo.toml9
-rw-r--r--docs/TESTING.md52
-rw-r--r--docs/adr/0001-testable-client-server-cores.md48
-rw-r--r--src/bin/client/connection.rs6
-rw-r--r--src/bin/client/tls.rs5
-rw-r--r--tests/slots.rs8
-rw-r--r--tests/tofu.rs4
7 files changed, 105 insertions, 27 deletions
diff --git a/Cargo.toml b/Cargo.toml
index 6fd0b3e..5e50626 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,24 +1,17 @@
1[package] 1[package]
2name = "zagan-bet" 2name = "zagan-bet"
3version = "0.3.0" 3version = "0.3.1"
4edition = "2024" 4edition = "2024"
5 5
6[dependencies] 6[dependencies]
7rusqlite = { version = "0.29", features = ["bundled"] } 7rusqlite = { version = "0.29", features = ["bundled"] }
8windows-sys = { version = "0.61.2", features = ["Win32_System_Console"] }
9crossterm = "0.27" 8crossterm = "0.27"
10bcrypt = "0.15" 9bcrypt = "0.15"
11aes-gcm = "0.10"
12pbkdf2 = "0.12"
13rand = "0.8" 10rand = "0.8"
14once_cell = "1.18"
15parking_lot = "0.12" 11parking_lot = "0.12"
16hmac = "0.12"
17sha2 = "0.10" 12sha2 = "0.10"
18base64 = "0.21"
19ctrlc = "3.4" 13ctrlc = "3.4"
20serde = { version = "1.0", features = ["derive"] } 14serde = { version = "1.0", features = ["derive"] }
21uuid = { version = "1.0", features = ["v4"] } 15uuid = { version = "1.0", features = ["v4"] }
22native-tls = "0.2" 16native-tls = "0.2"
23rustls = { version = "0.21", features = ["dangerous_configuration"] } 17rustls = { version = "0.21", features = ["dangerous_configuration"] }
24rustls-pemfile = "1.0" \ No newline at end of file
diff --git a/docs/TESTING.md b/docs/TESTING.md
new file mode 100644
index 0000000..6cac6fa
--- /dev/null
+++ b/docs/TESTING.md
@@ -0,0 +1,52 @@
1# Testing
2
3## Automated suite
4
5```sh
6cargo test
7```
8
9The suite is deterministic and headless: no network, no PTY, no sleeps, no
10shared global database. Each test owns its state.
11
12| File | Covers |
13| --- | --- |
14| `tests/client_core.rs` | Client command/game flows via a headless screen applying logical effects. |
15| `tests/client_ui.rs` | Help, history, completion, bet parsing, display width, cursor math. |
16| `tests/protocol.rs` | Newline buffering and timeout/error classification. |
17| `tests/server_core.rs` | Handler, bcrypt migration, SQL injection, transaction rollback, `plays` audit, sessions. |
18| `tests/slots.rs` | Server slots payout rules and overflow handling. |
19| `tests/storage.rs` | Client/server SQLite storage helpers. |
20| `tests/tofu.rs` | SPKI fingerprint and TOFU decisions. |
21
22`tests/tofu.rs` shells out to `openssl`, so run it on a machine where the
23`openssl` CLI is available.
24
25## Manual TUI checklist
26
27There is no CI. Run this on Linux and on a Windows VM before a release.
28Use an 80-column terminal because several output lines are width-sensitive.
29
301. Start the server with a valid `identity.p12`, then start the client.
312. First contact prints three lines and the 64-character fingerprint does not
32 wrap at 80 columns. `Connected to ...` shows a one-decimal ping.
333. `register` / `login`: backspace removes one `*` per key, Esc cancels the
34 prompt, Ctrl+C exits and restores the terminal.
354. `help` fits 80 columns. Tab completion wraps within 76 columns and the
36 cursor stays visible.
375. `play slots 50` shows `Bet: 50 PLN` and the fetched balance.
386. Space shows `Spinning...`, then the reels, win text, and new balance inside
39 the same 7-line board. The controls line is never overwritten.
407. `+` / `-` after a spin changes only the bet; no stale result lines appear.
418. `q` clears the board. Ctrl+C in a game clears the board and exits.
429. `info`, `ping`, `send hello`, and `hosts` print readable output; the host
43 fingerprint fits 80 columns.
4410. Windows: repeat 2-9 and confirm keys are not duplicated or printed twice.
45
46## Manual server checks
47
48- Register, log out, log in again (bcrypt path).
49- Log in with a pre-0.3 user row (legacy SHA-256) and confirm it still works;
50 the row is upgraded to bcrypt.
51- Spin several times and confirm `plays` grows by one row per spin while
52 `users.balance` changes by `winnings - bet`.
diff --git a/docs/adr/0001-testable-client-server-cores.md b/docs/adr/0001-testable-client-server-cores.md
new file mode 100644
index 0000000..2e3cd1e
--- /dev/null
+++ b/docs/adr/0001-testable-client-server-cores.md
@@ -0,0 +1,48 @@
1# 0001: Testable client and server cores
2
3Status: Accepted (2026-10-28)
4
5## Context
6
7The client and server started as binaries with their game logic and I/O mixed
8together. That made the TUI hard to reason about: a spin appended results below
9the board, a later bet change redrew at the wrong line, and none of it could be
10tested without a terminal. The server handler had the same problem: it owned
11static database and session state, so its security-sensitive paths could not be
12exercised from `tests/`.
13
14## Decision
15
16- The library (`src/lib.rs`) owns shared logic. `src/bin/client` and
17 `src/bin/server` are thin adapters for crossterm, TLS, environment variables,
18 and interactive prompts.
19- `client::core::ClientCore` owns input, history, commands, game state, and
20 pending replies. It returns `Effect`s (`Print`, `ReplaceRegion`, `ClearRegion`,
21 `SetPrompt`, `SendLine`, `Interactive`, ...). The binary applies them; the
22 tests apply them to a headless screen.
23- `server::core::ServerCore` owns message handling and receives an injected
24 `Connection`, `GameRegistry`, and bcrypt cost. The binary only owns the TLS
25 listener and per-connection thread.
26- Tests live in `tests/` and exercise the library. Pure UI math is tested
27 directly; terminal ANSI output is checked manually with `docs/TESTING.md`.
28 There are no PTY tests because they cannot cover Windows and `RULES.md`
29 requires identical behaviour on all platforms.
30- The client keeps sending its SHA-256 credential. The server stores
31 `bcrypt(credential)` and upgrades legacy SHA-256 rows on the first successful
32 login. This protects a stolen server database without changing the wire
33 protocol, but the client-side credential remains replayable if `client.db`
34 leaks.
35- Each settled spin writes a durable `plays` row in the same transaction as the
36 balance updates. The in-memory session is a transient guard and is removed
37 as soon as the spin settles or fails.
38
39## Consequences
40
41- Client flows and server security rules are testable without a terminal or a
42 network. A visual bug becomes an assertion about the headless screen.
43- The binaries have an extra translation layer (`Effect` application), and the
44 server opens a SQLite connection per client.
45- Terminal rendering still needs a manual Linux/Windows pass; the checklist is
46 in `docs/TESTING.md`.
47- Password storage is stronger, but not a full PAKE. A future MAJOR protocol
48 change can replace the replayable credential.
diff --git a/src/bin/client/connection.rs b/src/bin/client/connection.rs
index 49695dd..6459de5 100644
--- a/src/bin/client/connection.rs
+++ b/src/bin/client/connection.rs
@@ -131,12 +131,6 @@ impl TlsStream {
131 ) -> std::io::Result<()> { 131 ) -> std::io::Result<()> {
132 self.inner.set_read_timeout(duration) 132 self.inner.set_read_timeout(duration)
133 } 133 }
134
135 pub fn try_clone(&self) -> std::io::Result<TlsStream> {
136 Ok(TlsStream {
137 inner: self.inner.try_clone()?,
138 })
139 }
140} 134}
141 135
142impl Read for TlsStream { 136impl Read for TlsStream {
diff --git a/src/bin/client/tls.rs b/src/bin/client/tls.rs
index b118f63..8c052f7 100644
--- a/src/bin/client/tls.rs
+++ b/src/bin/client/tls.rs
@@ -204,11 +204,6 @@ impl TlsStream {
204 ) -> std::io::Result<()> { 204 ) -> std::io::Result<()> {
205 self.session.lock().sock.set_read_timeout(duration) 205 self.session.lock().sock.set_read_timeout(duration)
206 } 206 }
207
208 /// Clones share the same underlying TLS session.
209 pub fn try_clone(&self) -> std::io::Result<TlsStream> {
210 Ok(self.clone())
211 }
212} 207}
213 208
214impl Read for TlsStream { 209impl Read for TlsStream {
diff --git a/tests/slots.rs b/tests/slots.rs
index df4af28..3035800 100644
--- a/tests/slots.rs
+++ b/tests/slots.rs
@@ -11,9 +11,9 @@ fn make_game() -> SlotsGame {
11#[test] 11#[test]
12fn three_of_a_kind_pays() { 12fn three_of_a_kind_pays() {
13 let game = make_game(); 13 let game = make_game();
14 // CCC = 5x 14 // CCC = 2x
15 assert_eq!(game.calculate_payout(&['C', 'C', 'C'], 100), 200); 15 assert_eq!(game.calculate_payout(&['C', 'C', 'C'], 100), 200);
16 // SSS = 100x 16 // SSS = 28x
17 assert_eq!(game.calculate_payout(&['S', 'S', 'S'], 10), 280); 17 assert_eq!(game.calculate_payout(&['S', 'S', 'S'], 10), 280);
18} 18}
19 19
@@ -21,9 +21,9 @@ fn three_of_a_kind_pays() {
21#[test] 21#[test]
22fn two_adjacent_pays() { 22fn two_adjacent_pays() {
23 let game = make_game(); 23 let game = make_game();
24 // CC? = 2x 24 // CC? = 1x
25 assert_eq!(game.calculate_payout(&['C', 'C', 'D'], 100), 100); 25 assert_eq!(game.calculate_payout(&['C', 'C', 'D'], 100), 100);
26 // ?LL = 3x 26 // ?LL = 1x
27 assert_eq!(game.calculate_payout(&['D', 'L', 'L'], 50), 50); 27 assert_eq!(game.calculate_payout(&['D', 'L', 'L'], 50), 50);
28} 28}
29 29
diff --git a/tests/tofu.rs b/tests/tofu.rs
index 359c192..4f59d6d 100644
--- a/tests/tofu.rs
+++ b/tests/tofu.rs
@@ -1,8 +1,4 @@
1//! TOFU trust tests. 1//! TOFU trust tests.
2//!
3//! These pin the not-yet-implemented TOFU behavior: every test here is
4//! expected to FAIL until the corresponding feature lands in `src/tofu.rs`
5//! and the client storage layer.
6 2
7use std::path::PathBuf; 3use std::path::PathBuf;
8use std::process::Command; 4use std::process::Command;