summaryrefslogtreecommitdiffstats
path: root/tests/protocol.rs
diff options
context:
space:
mode:
authorsalmonik <salmonik@tuta.io>2026-11-08 03:29:35 +0100
committersalmonik <salmonik@tuta.io>2026-11-08 03:29:35 +0100
commit058432130628d00764663ecf819a9adc6793768d (patch)
treed3f49f111fbfaa48647cca2600adcf9017af2d30 /tests/protocol.rs
parentweb: serve latest.php through PHP and refuse stray PHP files (diff)
downloadzagan-bet-058432130628d00764663ecf819a9adc6793768d.tar.gz
zagan-bet-058432130628d00764663ecf819a9adc6793768d.zip
v5.0.0: protocol v5 and the browser frontend
Protocol v5 replaces newline lines with bounded [u32 len][text] frames and REQ/RES/EVT envelopes. Requests carry correlation ids and the server replays the last 256 replies per connection, so a gateway retry cannot double-debit and a late reply cannot clobber a newer request. HELLO/PONG carry server time; crash rounds announce started_at and push coalesced 10 Hz ticks. Crash now commits sha256(seed) before a round and reveals the seed when it ends, so clients recompute the crash point. Adds the webd/ gateway: one blocking thread, one ClientCore and one pinned upstream TLS connection per session, an HttpOnly session cookie, takeover, and a 60-second grace window. The browser sends Command JSON and renders View JSON; it never speaks the game protocol or stores credentials. Adds the vanilla TypeScript browser app at /play/ (login, lobby, all four games, history, notices, crash verification), the nginx /play and /ws routes, and the headless DOM smoke test. webd and the app stay out of the TUI/GUI/server build graph. The client/server version moves to 5.0.0 in lockstep; v4 peers are refused at HELLO.
Diffstat (limited to 'tests/protocol.rs')
-rw-r--r--tests/protocol.rs150
1 files changed, 101 insertions, 49 deletions
diff --git a/tests/protocol.rs b/tests/protocol.rs
index bf8a4fd..d829ba9 100644
--- a/tests/protocol.rs
+++ b/tests/protocol.rs
@@ -1,84 +1,136 @@
1use std::io::{Cursor, Error, ErrorKind}; 1use std::io::Cursor;
2use zagan_bet::protocol::{
3 FrameBuffer, FrameError, MAX_FRAME_LEN, Packet, encode_frame, event_line, io_failure_message,
4 parse_packet, parse_request, reply_line, request_line,
5};
2 6
3use zagan_bet::protocol::{io_failure_message, LineBuffer}; 7fn framed(payload: &str) -> Vec<u8> {
8 encode_frame(payload)
9}
4 10
5#[test] 11#[test]
6fn keeps_data_after_the_first_newline() { 12fn decodes_every_complete_frame_and_keeps_the_remainder() {
7 let mut buffer = LineBuffer::new(); 13 let mut buffer = FrameBuffer::new();
8 buffer.push(b"PONG\nGAME_RESULT session 60 10 990 OSS two\n"); 14 let mut bytes = framed("RES 1 PONG 1700000000000");
15 bytes.extend(framed("EVT 2 crash.tick session=cr1 t=1700000000100 m=1.5"));
16 buffer.push(&bytes);
9 17
10 assert_eq!(buffer.next_line().as_deref(), Some("PONG"));
11 assert_eq!( 18 assert_eq!(
12 buffer.next_line().as_deref(), 19 buffer.next_frame().unwrap().unwrap(),
13 Some("GAME_RESULT session 60 10 990 OSS two") 20 "RES 1 PONG 1700000000000"
21 );
22 assert_eq!(
23 buffer.next_frame().unwrap().unwrap(),
24 "EVT 2 crash.tick session=cr1 t=1700000000100 m=1.5"
14 ); 25 );
15 assert_eq!(buffer.next_line(), None); 26 assert!(buffer.next_frame().is_none());
27 assert!(buffer.is_empty());
16} 28}
17 29
18#[test] 30#[test]
19fn handles_crlf_and_empty_lines() { 31fn waits_for_a_partial_frame() {
20 let mut buffer = LineBuffer::new(); 32 let mut buffer = FrameBuffer::new();
21 buffer.push(b"PONG\r\n\r\n"); 33 let bytes = framed("RES 7 OK");
34 buffer.push(&bytes[..3]);
35 assert!(buffer.next_frame().is_none());
22 36
23 assert_eq!(buffer.next_line().as_deref(), Some("PONG")); 37 buffer.push(&bytes[3..7]);
24 assert_eq!(buffer.next_line().as_deref(), Some("")); 38 assert!(buffer.next_frame().is_none());
25 assert_eq!(buffer.next_line(), None); 39
40 buffer.push(&bytes[7..]);
41 assert_eq!(buffer.next_frame().unwrap().unwrap(), "RES 7 OK");
26} 42}
27 43
28#[test] 44#[test]
29fn waits_for_a_split_line() { 45fn an_oversized_length_is_rejected_without_waiting_for_bytes() {
30 let mut buffer = LineBuffer::new(); 46 let mut buffer = FrameBuffer::new();
31 buffer.push(b"PO"); 47 buffer.push(&((MAX_FRAME_LEN as u32 + 1).to_be_bytes()));
32 assert_eq!(buffer.next_line(), None); 48 match buffer.next_frame() {
49 Some(Err(FrameError::TooLarge(length))) => {
50 assert_eq!(length, MAX_FRAME_LEN + 1);
51 }
52 other => panic!("expected an oversize error, got {other:?}"),
53 }
54 assert!(buffer.is_empty(), "the buffer must not keep the bad frame");
55}
33 56
34 buffer.push(b"NG\n"); 57#[test]
35 assert_eq!(buffer.next_line().as_deref(), Some("PONG")); 58fn invalid_utf8_is_a_protocol_error() {
59 let mut buffer = FrameBuffer::new();
60 buffer.push(&1u32.to_be_bytes());
61 buffer.push(&[0xff]);
62 assert_eq!(buffer.next_frame(), Some(Err(FrameError::InvalidUtf8)));
36} 63}
37 64
38#[test] 65#[test]
39fn reads_multiple_lines_from_a_reader() { 66fn reads_frames_from_a_blocking_reader() {
40 let mut buffer = LineBuffer::new(); 67 let mut bytes = framed("RES 1 OK");
41 let mut reader = Cursor::new(b"one\ntwo\n".to_vec()); 68 bytes.extend(framed("EVT 1 crash.tick session=cr1 t=1 m=1.0"));
69 let mut reader = Cursor::new(bytes);
70 let mut buffer = FrameBuffer::new();
42 71
43 assert_eq!( 72 assert_eq!(
44 buffer.read_line(&mut reader).unwrap().as_deref(), 73 buffer.read_frame(&mut reader).unwrap().as_deref(),
45 Some("one") 74 Some("RES 1 OK")
46 ); 75 );
47 assert_eq!( 76 assert_eq!(
48 buffer.read_line(&mut reader).unwrap().as_deref(), 77 buffer.read_frame(&mut reader).unwrap().as_deref(),
49 Some("two") 78 Some("EVT 1 crash.tick session=cr1 t=1 m=1.0")
50 ); 79 );
51 assert_eq!(buffer.read_line(&mut reader).unwrap(), None); 80 assert_eq!(buffer.read_frame(&mut reader).unwrap(), None);
52} 81}
53 82
54#[test] 83#[test]
55fn returns_a_partial_line_at_eof() { 84fn builds_and_parses_the_envelope() {
56 let mut buffer = LineBuffer::new();
57 let mut reader = Cursor::new(b"partial".to_vec());
58
59 assert_eq!( 85 assert_eq!(
60 buffer.read_line(&mut reader).unwrap().as_deref(), 86 request_line(42, None, "PLAY_START crash 50"),
61 Some("partial") 87 "REQ 42 PLAY_START crash 50"
62 ); 88 );
63 assert_eq!(buffer.read_line(&mut reader).unwrap(), None); 89 assert_eq!(
90 request_line(42, Some("abc123"), "PLAY_START crash 50"),
91 "REQ 42 idem=abc123 PLAY_START crash 50"
92 );
93
94 let request = parse_request("REQ 42 idem=abc123 PLAY_START crash 50").unwrap();
95 assert_eq!(request.id, 42);
96 assert_eq!(request.idem.as_deref(), Some("abc123"));
97 assert_eq!(request.payload, "PLAY_START crash 50");
98
99 let request = parse_request("REQ 7 PING").unwrap();
100 assert_eq!(request.id, 7);
101 assert_eq!(request.idem, None);
102 assert_eq!(request.payload, "PING");
103
104 assert!(parse_request("PLAY_START crash 50").is_err());
105 assert!(parse_request("REQ nope PING").is_err());
106 assert!(parse_request("REQ 1 idem= PING").is_err());
64} 107}
65 108
66#[test] 109#[test]
67fn timeout_errors_get_a_timeout_message() { 110fn parses_replies_and_events() {
68 assert_eq!(
69 io_failure_message(&Error::from(ErrorKind::TimedOut)),
70 "ERROR Request timed out"
71 );
72 assert_eq!(
73 io_failure_message(&Error::from(ErrorKind::WouldBlock)),
74 "ERROR Request timed out"
75 );
76 assert_eq!( 111 assert_eq!(
77 io_failure_message(&Error::from(ErrorKind::UnexpectedEof)), 112 parse_packet(&reply_line(9, "ERROR Insufficient balance")).unwrap(),
78 "ERROR Connection lost" 113 Packet::Reply {
114 id: 9,
115 body: "ERROR Insufficient balance".to_string()
116 }
79 ); 117 );
80 assert_eq!( 118 assert_eq!(
81 io_failure_message(&Error::from(ErrorKind::ConnectionReset)), 119 parse_packet(&event_line(3, "CRASH_BUSTED {}")).unwrap(),
82 "ERROR Connection lost" 120 Packet::Event {
121 seq: 3,
122 body: "CRASH_BUSTED {}".to_string()
123 }
83 ); 124 );
125 assert!(parse_packet("REQ 1 PING").is_err());
126 assert!(parse_packet("RES nope OK").is_err());
127}
128
129#[test]
130fn io_failures_are_reported_as_short_messages() {
131 let timeout = std::io::Error::new(std::io::ErrorKind::TimedOut, "slow");
132 assert_eq!(io_failure_message(&timeout), "ERROR Request timed out");
133
134 let closed = std::io::Error::new(std::io::ErrorKind::UnexpectedEof, "gone");
135 assert_eq!(io_failure_message(&closed), "ERROR Connection lost");
84} 136}