Skip to main content

cdp_server/
protocol.rs

1// @trace REQ-CDS-001 [entity:CdpServer] [api:GET /json/version]
2// @trace REQ-CDS-004 [entity:DomainRegistry]
3// JSON-RPC 2.0 message types and serialization for CDP.
4
5use serde::{Deserialize, Serialize};
6use serde_json::Value;
7
8/// Incoming CDP/JSON-RPC 2.0 request.
9#[derive(Debug, Clone, Deserialize)]
10pub struct CdpMessage {
11    pub id: Option<i64>,
12    pub method: String,
13    #[serde(default)]
14    pub params: Option<Value>,
15    /// Flattened-session routing tag (CDP wire field is camelCase).
16    #[serde(default, rename = "sessionId")]
17    pub session_id: Option<String>,
18}
19
20/// Outgoing CDP/JSON-RPC 2.0 response.
21#[derive(Debug, Serialize)]
22pub struct CdpResponse {
23    pub id: Option<i64>,
24    #[serde(skip_serializing_if = "Option::is_none")]
25    pub result: Option<Value>,
26    #[serde(skip_serializing_if = "Option::is_none")]
27    pub error: Option<CdpError>,
28}
29
30/// CDP error object.
31#[derive(Debug, Clone, Serialize)]
32pub struct CdpError {
33    pub code: i64,
34    pub message: String,
35}
36
37/// Session-level I/O error (replaces unit `()` error types).
38#[derive(Debug)]
39pub enum SessionError {
40    /// WebSocket connection closed by peer.
41    Closed,
42    /// Underlying I/O failure (read/write error, unexpected frame, etc.).
43    Io,
44}
45
46/// CDP event notification (no id field).
47#[derive(Debug, Clone, Serialize)]
48pub struct CdpEvent {
49    pub method: String,
50    #[serde(skip_serializing_if = "Option::is_none")]
51    pub params: Option<Value>,
52}
53
54// JSON-RPC 2.0 error codes.
55pub const ERR_INVALID_REQUEST: i64 = -32600;
56pub const ERR_METHOD_NOT_FOUND: i64 = -32601;
57#[allow(dead_code)]
58pub const ERR_INVALID_PARAMS: i64 = -32602;
59#[allow(dead_code)]
60pub const ERR_INTERNAL: i64 = -32603;
61#[allow(dead_code)]
62pub const ERR_PARSE_ERROR: i64 = -32700;
63
64pub fn parse_message(raw: &str) -> Option<CdpMessage> {
65    serde_json::from_str(raw).ok()
66}
67
68pub fn serialize_response(resp: &CdpResponse) -> String {
69    serde_json::to_string(resp).unwrap_or_else(|_| {
70        r#"{"id":null,"error":{"code":-32700,"message":"serialize error"}}"#.into()
71    })
72}
73
74pub fn serialize_event(ev: &CdpEvent) -> String {
75    serde_json::to_string(ev).unwrap_or_else(|_| "{}".into())
76}
77
78pub fn ok_response(id: Option<i64>, result: Value) -> CdpResponse {
79    CdpResponse {
80        id,
81        result: Some(result),
82        error: None,
83    }
84}
85
86pub fn error_response(id: Option<i64>, code: i64, message: impl Into<String>) -> CdpResponse {
87    CdpResponse {
88        id,
89        result: None,
90        error: Some(CdpError {
91            code,
92            message: message.into(),
93        }),
94    }
95}
96
97pub fn ok_empty(id: Option<i64>) -> CdpResponse {
98    ok_response(id, serde_json::json!({}))
99}
100
101#[cfg(test)]
102mod tests {
103    use super::*;
104    use serde_json::json;
105
106    // 1. parse_message valid JSON → Some
107    #[test]
108    fn parse_valid_json_returns_some() {
109        let msg = parse_message(r#"{"id":1,"method":"Page.navigate"}"#);
110        assert!(msg.is_some());
111        let m = msg.unwrap();
112        assert_eq!(m.id, Some(1));
113        assert_eq!(m.method, "Page.navigate");
114    }
115
116    // 2. parse_message invalid JSON → None
117    #[test]
118    fn parse_invalid_json_returns_none() {
119        assert!(parse_message("{not json}").is_none());
120        assert!(parse_message("").is_none());
121    }
122
123    // 3. parse_message with null id → id is None
124    #[test]
125    fn parse_null_id_is_none() {
126        let msg = parse_message(r#"{"id":null,"method":"Page.reload"}"#).unwrap();
127        assert_eq!(msg.id, None);
128    }
129
130    // 4. parse_message with id 0 → id is Some(0)
131    #[test]
132    fn parse_zero_id_is_some_zero() {
133        let msg = parse_message(r#"{"id":0,"method":"Page.reload"}"#).unwrap();
134        assert_eq!(msg.id, Some(0));
135    }
136
137    // 5. parse_message with sessionId (flattened-session routing tag — the
138    //    CDP wire field is camelCase; snake_case never parsed, silently
139    //    breaking all flat-session routing before the rename)
140    #[test]
141    fn parse_session_id() {
142        let msg = parse_message(r#"{"id":5,"method":"Runtime.evaluate","sessionId":"sess-abc"}"#)
143            .unwrap();
144        assert_eq!(msg.session_id, Some("sess-abc".into()));
145        // snake_case is NOT a CDP wire field — it must not parse.
146        let snake = parse_message(r#"{"id":5,"method":"Runtime.evaluate","session_id":"x"}"#)
147            .unwrap();
148        assert_eq!(snake.session_id, None);
149    }
150
151    // 6. parse_message with nested params object
152    #[test]
153    fn parse_nested_params() {
154        let msg = parse_message(
155            r#"{"id":2,"method":"Page.navigate","params":{"url":"https://example.com","transitionType":"link"}}"#,
156        )
157        .unwrap();
158        let params = msg.params.unwrap();
159        assert_eq!(params["url"], "https://example.com");
160        assert_eq!(params["transitionType"], "link");
161    }
162
163    // 7. parse_message with array params
164    #[test]
165    fn parse_array_params() {
166        let msg =
167            parse_message(r#"{"id":3,"method":"DOM.querySelectorAll","params":["div","span"]}"#)
168                .unwrap();
169        let params = msg.params.unwrap();
170        assert!(params.is_array());
171        assert_eq!(params.as_array().unwrap().len(), 2);
172    }
173
174    // 8. serialize_response with result only
175    #[test]
176    fn serialize_response_result_only() {
177        let resp = ok_response(Some(1), json!({"frameId": "f1"}));
178        let s = serialize_response(&resp);
179        let v: Value = serde_json::from_str(&s).unwrap();
180        assert!(v.get("result").is_some());
181        assert!(v.get("error").is_none());
182        assert_eq!(v["id"], 1);
183    }
184
185    // 9. serialize_response with error only
186    #[test]
187    fn serialize_response_error_only() {
188        let resp = error_response(Some(2), ERR_METHOD_NOT_FOUND, "not found");
189        let s = serialize_response(&resp);
190        let v: Value = serde_json::from_str(&s).unwrap();
191        assert!(v.get("error").is_some());
192        assert!(v.get("result").is_none());
193    }
194
195    // 10. serialize_response is valid JSON
196    #[test]
197    fn serialize_response_produces_valid_json() {
198        let resp = ok_response(Some(42), json!({"data": true}));
199        let s = serialize_response(&resp);
200        assert!(serde_json::from_str::<Value>(&s).is_ok());
201    }
202
203    // 11. serialize_event with params
204    #[test]
205    fn serialize_event_with_params() {
206        let ev = CdpEvent {
207            method: "Page.frameNavigated".into(),
208            params: Some(json!({"frameId": "f1"})),
209        };
210        let s = serialize_event(&ev);
211        let v: Value = serde_json::from_str(&s).unwrap();
212        assert_eq!(v["method"], "Page.frameNavigated");
213        assert!(v.get("params").is_some());
214    }
215
216    // 12. serialize_event without params (method only)
217    #[test]
218    fn serialize_event_method_only() {
219        let ev = CdpEvent {
220            method: "Page.domContentEventFired".into(),
221            params: None,
222        };
223        let s = serialize_event(&ev);
224        let v: Value = serde_json::from_str(&s).unwrap();
225        assert_eq!(v["method"], "Page.domContentEventFired");
226        assert!(v.get("params").is_none());
227    }
228
229    // 13. ok_response has result, no error
230    #[test]
231    fn ok_response_has_result_no_error() {
232        let resp = ok_response(Some(1), json!({"ok": true}));
233        assert!(resp.result.is_some());
234        assert!(resp.error.is_none());
235    }
236
237    // 14. error_response has error, no result
238    #[test]
239    fn error_response_has_error_no_result() {
240        let resp = error_response(Some(1), -32600, "invalid");
241        assert!(resp.error.is_some());
242        assert!(resp.result.is_none());
243    }
244
245    // 15. ok_empty returns empty object result
246    #[test]
247    fn ok_empty_returns_empty_object() {
248        let resp = ok_empty(Some(1));
249        assert_eq!(resp.result, Some(json!({})));
250        assert!(resp.error.is_none());
251    }
252
253    // 16. CdpError debug format
254    #[test]
255    fn cdp_error_debug_format() {
256        let err = CdpError {
257            code: -32601,
258            message: "Method not found".into(),
259        };
260        let dbg = format!("{:?}", err);
261        assert!(dbg.contains("-32601"));
262        assert!(dbg.contains("Method not found"));
263    }
264
265    // 17. CdpResponse with large id (i64::MAX)
266    #[test]
267    fn cdp_response_large_id() {
268        let resp = ok_response(Some(i64::MAX), json!({}));
269        let s = serialize_response(&resp);
270        let v: Value = serde_json::from_str(&s).unwrap();
271        assert_eq!(v["id"].as_i64(), Some(i64::MAX));
272    }
273
274    // 18. CdpResponse with negative id
275    #[test]
276    fn cdp_response_negative_id() {
277        let resp = ok_response(Some(-100), json!({}));
278        let s = serialize_response(&resp);
279        let v: Value = serde_json::from_str(&s).unwrap();
280        assert_eq!(v["id"].as_i64(), Some(-100));
281    }
282
283    // 19. parse_message with unicode in method
284    #[test]
285    fn parse_unicode_method() {
286        let msg = parse_message(r#"{"id":1,"method":"Page.日本語テスト"}"#).unwrap();
287        assert_eq!(msg.method, "Page.日本語テスト");
288    }
289
290    // 20. parse_message extra fields ignored
291    #[test]
292    fn parse_extra_fields_ignored() {
293        let msg =
294            parse_message(r#"{"id":1,"method":"Page.reload","extra":"ignored","another":123}"#)
295                .unwrap();
296        assert_eq!(msg.id, Some(1));
297        assert_eq!(msg.method, "Page.reload");
298    }
299
300    // 21. SessionError variants (Closed, Io) debug format
301    #[test]
302    fn session_error_debug_format() {
303        let closed_dbg = format!("{:?}", SessionError::Closed);
304        assert!(closed_dbg.contains("Closed"));
305        let io_dbg = format!("{:?}", SessionError::Io);
306        assert!(io_dbg.contains("Io"));
307    }
308
309    // 22. ERR_INVALID_REQUEST / ERR_METHOD_NOT_FOUND constants
310    #[test]
311    fn error_code_constants() {
312        assert_eq!(ERR_INVALID_REQUEST, -32600);
313        assert_eq!(ERR_METHOD_NOT_FOUND, -32601);
314    }
315
316    // 23. ok_response serialize → contains 'result' key
317    #[test]
318    fn ok_response_serialize_contains_result() {
319        let resp = ok_response(Some(1), json!({"value": 42}));
320        let s = serialize_response(&resp);
321        assert!(s.contains("\"result\""));
322    }
323
324    // 24. error_response serialize → contains 'error' key
325    #[test]
326    fn error_response_serialize_contains_error() {
327        let resp = error_response(Some(1), ERR_INVALID_REQUEST, "bad");
328        let s = serialize_response(&resp);
329        assert!(s.contains("\"error\""));
330    }
331}