Skip to main content

fastmcp_rust/testing/fixtures/
messages.rs

1//! JSON-RPC message templates for testing.
2//!
3//! Provides pre-built message fixtures for testing:
4//! - Valid request/response messages
5//! - Various error responses
6//! - Notifications
7//! - Large payloads for stress testing
8
9use serde_json::{Value, json};
10
11// ============================================================================
12// Protocol Constants
13// ============================================================================
14
15/// Default MCP protocol version.
16pub const PROTOCOL_VERSION: &str = "2024-11-05";
17
18/// Default JSON-RPC version.
19pub const JSONRPC_VERSION: &str = "2.0";
20
21// ============================================================================
22// Valid Request Messages
23// ============================================================================
24
25/// Creates a valid initialize request.
26#[must_use]
27pub fn valid_initialize_request(request_id: u64) -> Value {
28    json!({
29        "jsonrpc": JSONRPC_VERSION,
30        "id": request_id,
31        "method": "initialize",
32        "params": {
33            "protocolVersion": PROTOCOL_VERSION,
34            "capabilities": {
35                "roots": { "listChanged": true }
36            },
37            "clientInfo": {
38                "name": "test-client",
39                "version": "1.0.0"
40            }
41        }
42    })
43}
44
45/// Creates a valid initialized notification.
46#[must_use]
47pub fn valid_initialized_notification() -> Value {
48    json!({
49        "jsonrpc": JSONRPC_VERSION,
50        "method": "notifications/initialized"
51    })
52}
53
54/// Creates a valid tools/list request.
55#[must_use]
56pub fn valid_tools_list_request(request_id: u64) -> Value {
57    json!({
58        "jsonrpc": JSONRPC_VERSION,
59        "id": request_id,
60        "method": "tools/list"
61    })
62}
63
64/// Creates a valid tools/call request.
65#[must_use]
66pub fn valid_tools_call_request(request_id: u64, name: &str, arguments: Value) -> Value {
67    json!({
68        "jsonrpc": JSONRPC_VERSION,
69        "id": request_id,
70        "method": "tools/call",
71        "params": {
72            "name": name,
73            "arguments": arguments
74        }
75    })
76}
77
78/// Creates a valid resources/list request.
79#[must_use]
80pub fn valid_resources_list_request(request_id: u64) -> Value {
81    json!({
82        "jsonrpc": JSONRPC_VERSION,
83        "id": request_id,
84        "method": "resources/list"
85    })
86}
87
88/// Creates a valid resources/read request.
89#[must_use]
90pub fn valid_resources_read_request(request_id: u64, uri: &str) -> Value {
91    json!({
92        "jsonrpc": JSONRPC_VERSION,
93        "id": request_id,
94        "method": "resources/read",
95        "params": {
96            "uri": uri
97        }
98    })
99}
100
101/// Creates a valid prompts/list request.
102#[must_use]
103pub fn valid_prompts_list_request(request_id: u64) -> Value {
104    json!({
105        "jsonrpc": JSONRPC_VERSION,
106        "id": request_id,
107        "method": "prompts/list"
108    })
109}
110
111/// Creates a valid prompts/get request.
112#[must_use]
113pub fn valid_prompts_get_request(request_id: u64, name: &str, arguments: Option<Value>) -> Value {
114    let mut params = json!({ "name": name });
115    if let Some(args) = arguments {
116        params["arguments"] = args;
117    }
118    json!({
119        "jsonrpc": JSONRPC_VERSION,
120        "id": request_id,
121        "method": "prompts/get",
122        "params": params
123    })
124}
125
126/// Creates a valid ping request.
127#[must_use]
128pub fn valid_ping_request(request_id: u64) -> Value {
129    json!({
130        "jsonrpc": JSONRPC_VERSION,
131        "id": request_id,
132        "method": "ping"
133    })
134}
135
136// ============================================================================
137// Valid Response Messages
138// ============================================================================
139
140/// Creates a valid success response.
141#[must_use]
142pub fn valid_success_response(request_id: u64, result: Value) -> Value {
143    json!({
144        "jsonrpc": JSONRPC_VERSION,
145        "id": request_id,
146        "result": result
147    })
148}
149
150/// Creates a valid initialize response.
151#[must_use]
152pub fn valid_initialize_response(request_id: u64) -> Value {
153    valid_success_response(
154        request_id,
155        json!({
156            "protocolVersion": PROTOCOL_VERSION,
157            "serverInfo": {
158                "name": "test-server",
159                "version": "1.0.0"
160            },
161            "capabilities": {
162                "tools": { "listChanged": false },
163                "resources": { "subscribe": false, "listChanged": false },
164                "prompts": { "listChanged": false }
165            }
166        }),
167    )
168}
169
170/// Creates a valid tools/list response.
171#[must_use]
172pub fn valid_tools_list_response(request_id: u64, tools: Vec<Value>) -> Value {
173    valid_success_response(request_id, json!({ "tools": tools }))
174}
175
176/// Creates a valid tools/call response.
177#[must_use]
178pub fn valid_tools_call_response(request_id: u64, content: Vec<Value>, is_error: bool) -> Value {
179    valid_success_response(
180        request_id,
181        json!({
182            "content": content,
183            "isError": is_error
184        }),
185    )
186}
187
188/// Creates a valid resources/list response.
189#[must_use]
190pub fn valid_resources_list_response(request_id: u64, resources: Vec<Value>) -> Value {
191    valid_success_response(request_id, json!({ "resources": resources }))
192}
193
194/// Creates a valid resources/read response.
195#[must_use]
196pub fn valid_resources_read_response(request_id: u64, contents: Vec<Value>) -> Value {
197    valid_success_response(request_id, json!({ "contents": contents }))
198}
199
200/// Creates a valid prompts/list response.
201#[must_use]
202pub fn valid_prompts_list_response(request_id: u64, prompts: Vec<Value>) -> Value {
203    valid_success_response(request_id, json!({ "prompts": prompts }))
204}
205
206/// Creates a valid prompts/get response.
207#[must_use]
208pub fn valid_prompts_get_response(
209    request_id: u64,
210    description: Option<&str>,
211    messages: Vec<Value>,
212) -> Value {
213    let mut result = json!({ "messages": messages });
214    if let Some(desc) = description {
215        result["description"] = json!(desc);
216    }
217    valid_success_response(request_id, result)
218}
219
220/// Creates a valid pong response.
221#[must_use]
222pub fn valid_pong_response(request_id: u64) -> Value {
223    valid_success_response(request_id, json!({}))
224}
225
226// ============================================================================
227// Error Response Messages
228// ============================================================================
229
230/// Creates an error response.
231#[must_use]
232pub fn error_response(request_id: u64, code: i32, message: &str, data: Option<Value>) -> Value {
233    let mut error = json!({
234        "code": code,
235        "message": message
236    });
237    if let Some(d) = data {
238        error["data"] = d;
239    }
240    json!({
241        "jsonrpc": JSONRPC_VERSION,
242        "id": request_id,
243        "error": error
244    })
245}
246
247/// Creates a parse error response (-32700).
248#[must_use]
249pub fn parse_error_response(request_id: u64) -> Value {
250    error_response(request_id, -32700, "Parse error", None)
251}
252
253/// Creates an invalid request error response (-32600).
254#[must_use]
255pub fn invalid_request_error_response(request_id: u64) -> Value {
256    error_response(request_id, -32600, "Invalid Request", None)
257}
258
259/// Creates a method not found error response (-32601).
260#[must_use]
261pub fn method_not_found_error_response(request_id: u64, method: &str) -> Value {
262    error_response(
263        request_id,
264        -32601,
265        &format!("Method not found: {method}"),
266        None,
267    )
268}
269
270/// Creates an invalid params error response (-32602).
271#[must_use]
272pub fn invalid_params_error_response(request_id: u64, details: &str) -> Value {
273    error_response(
274        request_id,
275        -32602,
276        "Invalid params",
277        Some(json!({ "details": details })),
278    )
279}
280
281/// Creates an internal error response (-32603).
282#[must_use]
283pub fn internal_error_response(request_id: u64, details: Option<&str>) -> Value {
284    error_response(
285        request_id,
286        -32603,
287        "Internal error",
288        details.map(|d| json!({ "details": d })),
289    )
290}
291
292/// Creates a resource not found error response.
293#[must_use]
294pub fn resource_not_found_error_response(request_id: u64, uri: &str) -> Value {
295    error_response(
296        request_id,
297        -32002,
298        &format!("Resource not found: {uri}"),
299        None,
300    )
301}
302
303/// Creates a tool not found error response.
304#[must_use]
305pub fn tool_not_found_error_response(request_id: u64, name: &str) -> Value {
306    error_response(request_id, -32002, &format!("Tool not found: {name}"), None)
307}
308
309/// Creates a prompt not found error response.
310#[must_use]
311pub fn prompt_not_found_error_response(request_id: u64, name: &str) -> Value {
312    error_response(
313        request_id,
314        -32002,
315        &format!("Prompt not found: {name}"),
316        None,
317    )
318}
319
320// ============================================================================
321// Invalid Messages (for error handling tests)
322// ============================================================================
323
324/// Creates various invalid JSON-RPC messages for testing error handling.
325pub mod invalid {
326    use serde_json::{Value, json};
327
328    /// Missing jsonrpc field.
329    #[must_use]
330    pub fn missing_jsonrpc() -> Value {
331        json!({
332            "id": 1,
333            "method": "test"
334        })
335    }
336
337    /// Wrong jsonrpc version.
338    #[must_use]
339    pub fn wrong_jsonrpc_version() -> Value {
340        json!({
341            "jsonrpc": "1.0",
342            "id": 1,
343            "method": "test"
344        })
345    }
346
347    /// Missing method field.
348    #[must_use]
349    pub fn missing_method() -> Value {
350        json!({
351            "jsonrpc": "2.0",
352            "id": 1
353        })
354    }
355
356    /// Invalid method type (not string).
357    #[must_use]
358    pub fn invalid_method_type() -> Value {
359        json!({
360            "jsonrpc": "2.0",
361            "id": 1,
362            "method": 123
363        })
364    }
365
366    /// Invalid id type.
367    #[must_use]
368    pub fn invalid_id_type() -> Value {
369        json!({
370            "jsonrpc": "2.0",
371            "id": [1, 2, 3],
372            "method": "test"
373        })
374    }
375
376    /// Both result and error present.
377    #[must_use]
378    pub fn both_result_and_error() -> Value {
379        json!({
380            "jsonrpc": "2.0",
381            "id": 1,
382            "result": {},
383            "error": { "code": -32600, "message": "Error" }
384        })
385    }
386
387    /// Malformed JSON string.
388    #[must_use]
389    pub fn malformed_json_string() -> &'static str {
390        r#"{"jsonrpc": "2.0", "id": 1, "method": "test""#
391    }
392
393    /// Empty object.
394    #[must_use]
395    pub fn empty_object() -> Value {
396        json!({})
397    }
398
399    /// Null value.
400    #[must_use]
401    pub fn null_value() -> Value {
402        Value::Null
403    }
404
405    /// Array instead of object.
406    #[must_use]
407    pub fn array_instead_of_object() -> Value {
408        json!([1, 2, 3])
409    }
410}
411
412// ============================================================================
413// Notification Messages
414// ============================================================================
415
416/// Creates a notification message (no id).
417#[must_use]
418pub fn notification(method: &str, params: Option<Value>) -> Value {
419    let mut msg = json!({
420        "jsonrpc": JSONRPC_VERSION,
421        "method": method
422    });
423    if let Some(p) = params {
424        msg["params"] = p;
425    }
426    msg
427}
428
429/// Creates a progress notification.
430#[must_use]
431pub fn progress_notification(token: &str, progress: f64, message: Option<&str>) -> Value {
432    notification(
433        "notifications/progress",
434        Some(json!({
435            "progressToken": token,
436            "progress": progress,
437            "message": message
438        })),
439    )
440}
441
442/// Creates a log notification.
443#[must_use]
444pub fn log_notification(level: &str, data: Value) -> Value {
445    notification(
446        "notifications/log",
447        Some(json!({
448            "level": level,
449            "data": data
450        })),
451    )
452}
453
454/// Creates a cancelled notification.
455#[must_use]
456pub fn cancelled_notification(request_id: u64, reason: Option<&str>) -> Value {
457    notification(
458        "notifications/cancelled",
459        Some(json!({
460            "requestId": request_id,
461            "reason": reason
462        })),
463    )
464}
465
466// ============================================================================
467// Large Payload Generators
468// ============================================================================
469
470/// Generates a large string payload of approximately the given size in KB.
471#[must_use]
472pub fn large_string_payload(size_kb: usize) -> String {
473    let base = "abcdefghijklmnopqrstuvwxyz0123456789";
474    let iterations = (size_kb * 1024) / base.len() + 1;
475    base.repeat(iterations)
476}
477
478/// Generates a large JSON payload with many fields.
479#[must_use]
480pub fn large_json_payload(num_fields: usize) -> Value {
481    let mut obj = serde_json::Map::new();
482    for i in 0..num_fields {
483        obj.insert(
484            format!("field_{i:06}"),
485            json!({
486                "index": i,
487                "value": format!("value_{i}"),
488                "nested": {
489                    "a": i * 2,
490                    "b": format!("nested_{i}")
491                }
492            }),
493        );
494    }
495    Value::Object(obj)
496}
497
498/// Generates a large array payload.
499#[must_use]
500pub fn large_array_payload(num_items: usize) -> Value {
501    let items: Vec<Value> = (0..num_items)
502        .map(|i| {
503            json!({
504                "id": i,
505                "name": format!("item_{i}"),
506                "data": large_string_payload(1) // 1KB per item
507            })
508        })
509        .collect();
510    Value::Array(items)
511}
512
513/// Creates a tools/call request with a large argument payload.
514#[must_use]
515pub fn large_tools_call_request(request_id: u64, size_kb: usize) -> Value {
516    valid_tools_call_request(
517        request_id,
518        "large_input_tool",
519        json!({
520            "data": large_string_payload(size_kb)
521        }),
522    )
523}
524
525#[cfg(test)]
526mod tests {
527    use super::*;
528
529    #[test]
530    fn test_valid_initialize_request() {
531        let req = valid_initialize_request(1);
532        assert_eq!(req["jsonrpc"], "2.0");
533        assert_eq!(req["id"], 1);
534        assert_eq!(req["method"], "initialize");
535        assert!(req["params"]["protocolVersion"].is_string());
536    }
537
538    #[test]
539    fn test_valid_tools_call_request() {
540        let req = valid_tools_call_request(2, "greeting", json!({"name": "World"}));
541        assert_eq!(req["method"], "tools/call");
542        assert_eq!(req["params"]["name"], "greeting");
543        assert_eq!(req["params"]["arguments"]["name"], "World");
544    }
545
546    #[test]
547    fn test_valid_success_response() {
548        let resp = valid_success_response(1, json!({"value": 42}));
549        assert_eq!(resp["id"], 1);
550        assert!(resp.get("result").is_some());
551        assert!(resp.get("error").is_none());
552    }
553
554    #[test]
555    fn test_error_response() {
556        let resp = error_response(1, -32600, "Invalid Request", None);
557        assert_eq!(resp["id"], 1);
558        assert_eq!(resp["error"]["code"], -32600);
559        assert!(resp.get("result").is_none());
560    }
561
562    #[test]
563    fn test_parse_error_response() {
564        let resp = parse_error_response(1);
565        assert_eq!(resp["error"]["code"], -32700);
566    }
567
568    #[test]
569    fn test_method_not_found_error() {
570        let resp = method_not_found_error_response(1, "unknown_method");
571        assert_eq!(resp["error"]["code"], -32601);
572        assert!(
573            resp["error"]["message"]
574                .as_str()
575                .unwrap()
576                .contains("unknown_method")
577        );
578    }
579
580    #[test]
581    fn test_invalid_messages() {
582        let missing = invalid::missing_jsonrpc();
583        assert!(missing.get("jsonrpc").is_none());
584
585        let wrong_version = invalid::wrong_jsonrpc_version();
586        assert_eq!(wrong_version["jsonrpc"], "1.0");
587
588        let missing_method = invalid::missing_method();
589        assert!(missing_method.get("method").is_none());
590    }
591
592    #[test]
593    fn test_notification() {
594        let notif = notification("test/notification", Some(json!({"key": "value"})));
595        assert!(notif.get("id").is_none());
596        assert_eq!(notif["method"], "test/notification");
597    }
598
599    #[test]
600    fn test_progress_notification() {
601        let notif = progress_notification("token123", 0.5, Some("Halfway done"));
602        assert_eq!(notif["params"]["progressToken"], "token123");
603        assert_eq!(notif["params"]["progress"], 0.5);
604    }
605
606    #[test]
607    fn test_large_string_payload() {
608        let payload = large_string_payload(10);
609        // Should be at least 10KB
610        assert!(payload.len() >= 10 * 1024);
611    }
612
613    #[test]
614    fn test_large_json_payload() {
615        let payload = large_json_payload(100);
616        let obj = payload.as_object().unwrap();
617        assert_eq!(obj.len(), 100);
618    }
619
620    #[test]
621    fn test_large_array_payload() {
622        let payload = large_array_payload(50);
623        let arr = payload.as_array().unwrap();
624        assert_eq!(arr.len(), 50);
625    }
626
627    #[test]
628    fn test_large_tools_call_request() {
629        let req = large_tools_call_request(1, 5);
630        let data = req["params"]["arguments"]["data"].as_str().unwrap();
631        assert!(data.len() >= 5 * 1024);
632    }
633
634    // =========================================================================
635    // Additional coverage tests (bd-3qqy)
636    // =========================================================================
637
638    #[test]
639    fn valid_initialized_notification_has_no_id() {
640        let notif = valid_initialized_notification();
641        assert!(notif.get("id").is_none());
642        assert_eq!(notif["method"], "notifications/initialized");
643    }
644
645    #[test]
646    fn valid_resources_and_prompts_requests() {
647        let rl = valid_resources_list_request(10);
648        assert_eq!(rl["method"], "resources/list");
649        assert_eq!(rl["id"], 10);
650
651        let rr = valid_resources_read_request(11, "file:///test.txt");
652        assert_eq!(rr["params"]["uri"], "file:///test.txt");
653
654        let pl = valid_prompts_list_request(12);
655        assert_eq!(pl["method"], "prompts/list");
656
657        let pg = valid_prompts_get_request(13, "greet", Some(json!({"name": "Alice"})));
658        assert_eq!(pg["params"]["name"], "greet");
659        assert_eq!(pg["params"]["arguments"]["name"], "Alice");
660
661        let pg_no_args = valid_prompts_get_request(14, "simple", None);
662        assert!(pg_no_args["params"].get("arguments").is_none());
663    }
664
665    #[test]
666    fn valid_ping_pong() {
667        let ping = valid_ping_request(20);
668        assert_eq!(ping["method"], "ping");
669
670        let pong = valid_pong_response(20);
671        assert_eq!(pong["id"], 20);
672        assert!(pong["result"].is_object());
673    }
674
675    #[test]
676    fn valid_initialize_response_structure() {
677        let resp = valid_initialize_response(1);
678        assert_eq!(resp["result"]["protocolVersion"], PROTOCOL_VERSION);
679        assert!(resp["result"]["serverInfo"].is_object());
680        assert!(resp["result"]["capabilities"].is_object());
681    }
682
683    #[test]
684    fn valid_list_and_call_responses() {
685        let tl = valid_tools_list_response(1, vec![json!({"name": "t1"})]);
686        assert_eq!(tl["result"]["tools"].as_array().unwrap().len(), 1);
687
688        let tc = valid_tools_call_response(2, vec![json!({"type": "text", "text": "hi"})], false);
689        assert_eq!(tc["result"]["isError"], false);
690
691        let tc_err = valid_tools_call_response(3, vec![], true);
692        assert_eq!(tc_err["result"]["isError"], true);
693
694        let rl = valid_resources_list_response(4, vec![]);
695        assert!(rl["result"]["resources"].as_array().unwrap().is_empty());
696
697        let rr = valid_resources_read_response(5, vec![json!({"uri": "x", "text": "data"})]);
698        assert_eq!(rr["result"]["contents"].as_array().unwrap().len(), 1);
699
700        let pl = valid_prompts_list_response(6, vec![json!({"name": "p1"})]);
701        assert_eq!(pl["result"]["prompts"].as_array().unwrap().len(), 1);
702
703        let pg = valid_prompts_get_response(7, Some("desc"), vec![]);
704        assert_eq!(pg["result"]["description"], "desc");
705
706        let pg_no_desc = valid_prompts_get_response(8, None, vec![]);
707        assert!(pg_no_desc["result"].get("description").is_none());
708    }
709
710    #[test]
711    fn error_response_variants() {
712        let ir = invalid_request_error_response(1);
713        assert_eq!(ir["error"]["code"], -32600);
714
715        let ip = invalid_params_error_response(2, "bad param");
716        assert_eq!(ip["error"]["code"], -32602);
717        assert_eq!(ip["error"]["data"]["details"], "bad param");
718
719        let ie = internal_error_response(3, Some("crash"));
720        assert_eq!(ie["error"]["code"], -32603);
721        assert_eq!(ie["error"]["data"]["details"], "crash");
722
723        let ie_none = internal_error_response(4, None);
724        assert!(ie_none["error"].get("data").is_none());
725
726        let rnf = resource_not_found_error_response(5, "file:///x");
727        assert!(
728            rnf["error"]["message"]
729                .as_str()
730                .unwrap()
731                .contains("file:///x")
732        );
733
734        let tnf = tool_not_found_error_response(6, "missing_tool");
735        assert!(
736            tnf["error"]["message"]
737                .as_str()
738                .unwrap()
739                .contains("missing_tool")
740        );
741
742        let pnf = prompt_not_found_error_response(7, "missing_prompt");
743        assert!(
744            pnf["error"]["message"]
745                .as_str()
746                .unwrap()
747                .contains("missing_prompt")
748        );
749    }
750
751    #[test]
752    fn invalid_message_remaining_variants() {
753        let imt = invalid::invalid_method_type();
754        assert_eq!(imt["method"], 123);
755
756        let iid = invalid::invalid_id_type();
757        assert!(iid["id"].is_array());
758
759        let bre = invalid::both_result_and_error();
760        assert!(bre.get("result").is_some());
761        assert!(bre.get("error").is_some());
762
763        let mjs = invalid::malformed_json_string();
764        assert!(!mjs.is_empty());
765
766        let eo = invalid::empty_object();
767        assert!(eo.as_object().unwrap().is_empty());
768
769        let nv = invalid::null_value();
770        assert!(nv.is_null());
771
772        let aio = invalid::array_instead_of_object();
773        assert!(aio.is_array());
774    }
775
776    #[test]
777    fn notification_without_params() {
778        let notif = notification("test/event", None);
779        assert!(notif.get("params").is_none());
780        assert!(notif.get("id").is_none());
781    }
782
783    #[test]
784    fn log_and_cancelled_notifications() {
785        let log = log_notification("error", json!("something failed"));
786        assert_eq!(log["method"], "notifications/log");
787        assert_eq!(log["params"]["level"], "error");
788
789        let cancel = cancelled_notification(42, Some("timeout"));
790        assert_eq!(cancel["method"], "notifications/cancelled");
791        assert_eq!(cancel["params"]["requestId"], 42);
792        assert_eq!(cancel["params"]["reason"], "timeout");
793    }
794
795    #[test]
796    fn error_response_with_data() {
797        let resp = error_response(1, -32000, "Custom error", Some(json!({"extra": true})));
798        assert_eq!(resp["error"]["data"]["extra"], true);
799    }
800}