Skip to main content

syncpond_protocol/
lib.rs

1pub mod protocol;
2
3pub use protocol::{
4    Command,
5    Response,
6    format_command,
7    format_response,
8    parse_command,
9    parse_response,
10    connect_with_auth,
11    connect_with_retry,
12    start_long_lived_tcp,
13    Client,
14    send_command,
15};
16
17#[cfg(test)]
18mod tests {
19    use super::*;
20    use serde_json::json;
21
22    #[test]
23    fn parse_and_format_room_create() {
24        let cmd = parse_command("ROOM.CREATE").unwrap();
25        assert_eq!(cmd, Command::RoomCreate);
26        let line = format_command(&cmd);
27        assert_eq!(line, "ROOM.CREATE");
28    }
29
30    #[test]
31    fn parse_set_with_json() {
32        let line = "SET 1 public foo {\"a\": 123, \"s\": \"x\"}";
33        let cmd = parse_command(line).unwrap();
34        match cmd {
35            Command::Set { room_id, container, key, value } => {
36                assert_eq!(room_id, 1);
37                assert_eq!(container, "public");
38                assert_eq!(key, "foo");
39                assert_eq!(value, json!({"a": 123, "s": "x"}));
40            }
41            _ => panic!("unexpected command variant"),
42        }
43    }
44
45    #[test]
46    fn parse_and_format_persist_commands() {
47        let s = "PERSIST.SET 1 public foo";
48        let cmd = parse_command(s).unwrap();
49        assert_eq!(cmd, Command::PersistSet { room_id: 1, container: "public".into(), key: "foo".into() });
50        assert_eq!(format_command(&cmd), "PERSIST.SET 1 public foo");
51
52        let s2 = "PERSIST.UNSET 1 public foo";
53        let cmd2 = parse_command(s2).unwrap();
54        assert_eq!(cmd2, Command::PersistUnset { room_id: 1, container: "public".into(), key: "foo".into() });
55        assert_eq!(format_command(&cmd2), "PERSIST.UNSET 1 public foo");
56
57        let s3 = "PERSIST.GET 1 public foo";
58        let cmd3 = parse_command(s3).unwrap();
59        assert_eq!(cmd3, Command::PersistGet { room_id: 1, container: "public".into(), key: "foo".into() });
60        assert_eq!(format_command(&cmd3), "PERSIST.GET 1 public foo");
61    }
62
63    #[test]
64    fn parse_and_format_server_commands() {
65        use serde_json::json;
66
67        let s = "SERVER.SET 1 mykey {\"a\":1}";
68        let cmd = parse_command(s).unwrap();
69        assert_eq!(cmd, Command::ServerSet { room_id: 1, key: "mykey".into(), value: json!({"a":1}) });
70        assert_eq!(format_command(&cmd), "SERVER.SET 1 mykey {\"a\":1}");
71
72        let s2 = "SERVER.DEL 1 mykey";
73        let cmd2 = parse_command(s2).unwrap();
74        assert_eq!(cmd2, Command::ServerDel { room_id: 1, key: "mykey".into() });
75        assert_eq!(format_command(&cmd2), "SERVER.DEL 1 mykey");
76
77        let s3 = "SERVER.GET 1 mykey";
78        let cmd3 = parse_command(s3).unwrap();
79        assert_eq!(cmd3, Command::ServerGet { room_id: 1, key: "mykey".into() });
80        assert_eq!(format_command(&cmd3), "SERVER.GET 1 mykey");
81    }
82
83    #[test]
84    fn parse_and_format_save_load_commands() {
85        let s = "SAVE 1";
86        let cmd = parse_command(s).unwrap();
87        assert_eq!(cmd, Command::Save { room_id: 1 });
88        assert_eq!(format_command(&cmd), "SAVE 1");
89
90        let s2 = "LOAD 1";
91        let cmd2 = parse_command(s2).unwrap();
92        assert_eq!(cmd2, Command::Load { room_id: 1 });
93        assert_eq!(format_command(&cmd2), "LOAD 1");
94    }
95
96    #[test]
97    fn response_roundtrip() {
98        let r = Response::Ok(Some("{\"a\":1}".to_string()));
99        let s = format_response(&r);
100        assert_eq!(s, "OK {\"a\":1}");
101        let parsed = parse_response(&s).unwrap();
102        assert_eq!(parsed, r);
103    }
104
105    #[tokio::test]
106    async fn connect_and_auth_and_receive() {
107        use tokio::net::TcpListener;
108        use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
109        use tokio::time::Duration;
110
111        let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
112        let addr = listener.local_addr().unwrap().to_string();
113        let api_key = "test-api-key-xyz";
114
115        let (mut rx, _tx) = start_long_lived_tcp(addr.clone(), api_key.to_string());
116
117        // Accept the incoming connection from our background client and validate auth.
118        let accept = tokio::spawn(async move {
119            let (mut socket, _) = listener.accept().await.unwrap();
120            let mut reader = BufReader::new(&mut socket);
121            let mut line = String::new();
122            reader.read_line(&mut line).await.unwrap();
123            assert_eq!(line.trim_end(), api_key);
124            socket.write_all(b"HELLO\n").await.unwrap();
125            socket.flush().await.unwrap();
126        });
127
128        let got = tokio::time::timeout(Duration::from_secs(2), rx.recv())
129            .await
130            .expect("timeout waiting for line")
131            .expect("channel closed");
132        assert_eq!(got, "HELLO");
133
134        accept.await.unwrap();
135    }
136
137    #[tokio::test]
138    async fn client_wrapper_send_and_shutdown() {
139        use tokio::net::TcpListener;
140        use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
141        use tokio::time::Duration;
142
143        let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
144        let addr = listener.local_addr().unwrap().to_string();
145        let api_key = "test-api-key-xyz".to_string();
146
147        // Start the client wrapper which spawns a background task.
148        let client = Client::start(addr.clone(), api_key.clone());
149
150        // Accept and validate auth + command from client in a background task.
151        let accept = tokio::spawn(async move {
152            let (mut socket, _) = listener.accept().await.unwrap();
153            let mut reader = BufReader::new(&mut socket);
154            let mut line = String::new();
155            // auth
156            reader.read_line(&mut line).await.unwrap();
157            assert_eq!(line.trim_end(), api_key);
158
159            // read command sent by client
160            let mut cmd_line = String::new();
161            reader.read_line(&mut cmd_line).await.unwrap();
162            assert_eq!(cmd_line.trim_end(), "ROOM.CREATE");
163        });
164
165        // Send a command from the client wrapper.
166        client.send(Command::RoomCreate).await.unwrap();
167
168        // Wait for the server task to accept the connection and receive the command,
169        // then shut down the client.
170        accept.await.unwrap();
171        client.shutdown().await.unwrap();
172    }
173}