#![allow(clippy::unwrap_used)]
#![allow(clippy::expect_used)]
use std::io::Cursor;
use do_memory_mcp::jsonrpc::{JsonRpcError, JsonRpcResponse, read_next_message};
use proptest::prelude::*;
use serde_json::json;
#[test]
fn empty_input_returns_none() {
let mut cursor = Cursor::new(Vec::<u8>::new());
let result = read_next_message(&mut cursor).unwrap();
assert!(result.is_none());
}
#[test]
fn truncated_json_is_returned_as_raw_line() {
let input = b"{\"jsonrpc\":\"2.0\",\"id\":1\n";
let mut cursor = Cursor::new(input.to_vec());
let result = read_next_message(&mut cursor).unwrap();
assert!(result.is_some());
let (msg, is_lsp) = result.unwrap();
assert!(!is_lsp);
assert!(msg.starts_with('{'));
}
#[test]
fn missing_jsonrpc_field_still_readable() {
let input = b"{\"id\":1,\"method\":\"test\"}\n";
let mut cursor = Cursor::new(input.to_vec());
let (msg, _) = read_next_message(&mut cursor).unwrap().unwrap();
let parsed: serde_json::Value = serde_json::from_str(&msg).unwrap();
assert!(parsed.get("jsonrpc").is_none());
}
#[test]
fn invalid_jsonrpc_version_still_readable() {
let input = b"{\"jsonrpc\":\"1.0\",\"id\":1,\"method\":\"test\"}\n";
let mut cursor = Cursor::new(input.to_vec());
let (msg, _) = read_next_message(&mut cursor).unwrap().unwrap();
let parsed: serde_json::Value = serde_json::from_str(&msg).unwrap();
assert_eq!(parsed["jsonrpc"], "1.0");
}
#[test]
fn content_length_larger_than_body_returns_error() {
let header = "Content-Length: 1000\r\n\r\n";
let body = "0123456789";
let mut data = header.as_bytes().to_vec();
data.extend_from_slice(body.as_bytes());
let mut cursor = Cursor::new(data);
let result = read_next_message(&mut cursor);
assert!(result.is_err());
}
#[test]
fn binary_non_utf8_in_content_length_body() {
let body: &[u8] = &[0xFF, 0xFE, 0xFD, 0xFC, 0xFB];
let header = format!("Content-Length: {}\r\n\r\n", body.len());
let mut data = header.into_bytes();
data.extend_from_slice(body);
let mut cursor = Cursor::new(data);
let result = read_next_message(&mut cursor).unwrap();
assert!(result.is_some());
let (msg, is_lsp) = result.unwrap();
assert!(is_lsp);
assert!(msg.contains('\u{FFFD}'));
}
#[test]
fn null_bytes_in_line_json() {
let input = b"{\"method\":\"\x00test\"}\n";
let mut cursor = Cursor::new(input.to_vec());
let result = read_next_message(&mut cursor).unwrap();
assert!(result.is_some());
let (msg, _) = result.unwrap();
assert!(msg.contains('\0'));
}
#[test]
fn deeply_nested_json_object() {
let depth = 100;
let mut json_str = String::new();
for _ in 0..depth {
json_str.push_str("{\"a\":");
}
json_str.push_str("null");
for _ in 0..depth {
json_str.push('}');
}
json_str.push('\n');
let mut cursor = Cursor::new(json_str.into_bytes());
let result = read_next_message(&mut cursor).unwrap();
assert!(result.is_some());
}
#[test]
fn content_length_zero_skips_to_next() {
let input = "Content-Length: 0\r\n\r\n{\"id\":1,\"method\":\"ping\"}\n";
let mut cursor = Cursor::new(input.as_bytes().to_vec());
let result = read_next_message(&mut cursor).unwrap();
assert!(result.is_some());
let (msg, is_lsp) = result.unwrap();
assert!(!is_lsp); assert!(msg.contains("\"method\":\"ping\""));
}
#[test]
fn content_length_negative_treated_as_zero() {
let input = "Content-Length: -5\r\n\r\n{\"id\":1,\"method\":\"ping\"}\n";
let mut cursor = Cursor::new(input.as_bytes().to_vec());
let result = read_next_message(&mut cursor).unwrap();
assert!(result.is_some());
let (msg, _) = result.unwrap();
assert!(msg.contains("\"method\":\"ping\""));
}
#[test]
fn content_length_much_larger_than_body() {
let payload = "{\"id\":1}";
let header = format!("Content-Length: {}\r\n\r\n", payload.len() + 10000);
let mut data = header.into_bytes();
data.extend_from_slice(payload.as_bytes());
let mut cursor = Cursor::new(data);
let result = read_next_message(&mut cursor);
assert!(result.is_err());
}
#[test]
fn missing_crlf_separator_after_header() {
let input = "Content-Length: 10\r\n{\"id\":1}\n";
let mut cursor = Cursor::new(input.as_bytes().to_vec());
let result = read_next_message(&mut cursor);
assert!(result.is_ok() || result.is_err());
}
#[test]
fn multiple_content_length_headers() {
let payload = "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"test\"}";
let header = format!(
"Content-Length: {}\r\nContent-Type: application/json\r\n\r\n",
payload.len()
);
let mut data = header.into_bytes();
data.extend_from_slice(payload.as_bytes());
let mut cursor = Cursor::new(data);
let result = read_next_message(&mut cursor).unwrap();
assert!(result.is_some());
let (msg, is_lsp) = result.unwrap();
assert!(is_lsp);
assert_eq!(msg, payload);
}
proptest! {
#[test]
fn response_has_result_xor_error(
id in 1i64..10000i64,
is_success in proptest::bool::ANY,
code in -32700i32..-32000i32,
message in "[a-zA-Z ]{5,50}",
) {
let response = if is_success {
JsonRpcResponse {
jsonrpc: "2.0".to_string(),
id: Some(json!(id)),
result: Some(json!({"ok": true})),
error: None,
}
} else {
JsonRpcResponse {
jsonrpc: "2.0".to_string(),
id: Some(json!(id)),
result: None,
error: Some(JsonRpcError {
code,
message,
data: None,
}),
}
};
let json_str = serde_json::to_string(&response).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&json_str).unwrap();
let has_result = parsed.get("result").is_some_and(|v| !v.is_null());
let has_error = parsed.get("error").is_some();
prop_assert!(has_result ^ has_error, "Must have result XOR error");
}
#[test]
fn id_roundtrips_for_various_types(
id_type in 0u8..3u8,
num_id in 0i64..100000i64,
str_id in "[a-zA-Z0-9-]{1,30}",
) {
let id_value = match id_type {
0 => json!(num_id),
1 => json!(str_id),
_ => json!(null),
};
let response = JsonRpcResponse {
jsonrpc: "2.0".to_string(),
id: Some(id_value.clone()),
result: Some(json!("ok")),
error: None,
};
let json_str = serde_json::to_string(&response).unwrap();
let deserialized: JsonRpcResponse = serde_json::from_str(&json_str).unwrap();
if id_type >= 2 {
prop_assert!(deserialized.id.is_none() || deserialized.id == Some(json!(null)));
} else {
prop_assert_eq!(deserialized.id, Some(id_value));
}
}
#[test]
fn error_code_preserved_exactly(
code in prop::num::i32::ANY,
message in "[a-zA-Z0-9 ]{3,40}",
) {
let error = JsonRpcError {
code,
message,
data: None,
};
let json_str = serde_json::to_string(&error).unwrap();
let deserialized: JsonRpcError = serde_json::from_str(&json_str).unwrap();
prop_assert_eq!(deserialized.code, code);
}
#[test]
fn jsonrpc_field_always_2_0(
id in 1i64..10000i64,
) {
let response = JsonRpcResponse {
jsonrpc: "2.0".to_string(),
id: Some(json!(id)),
result: Some(json!(null)),
error: None,
};
let json_str = serde_json::to_string(&response).unwrap();
let deserialized: JsonRpcResponse = serde_json::from_str(&json_str).unwrap();
prop_assert_eq!(deserialized.jsonrpc, "2.0");
}
}
#[test]
fn empty_method_string() {
let input = b"{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"\"}\n";
let mut cursor = Cursor::new(input.to_vec());
let (msg, _) = read_next_message(&mut cursor).unwrap().unwrap();
let parsed: serde_json::Value = serde_json::from_str(&msg).unwrap();
assert_eq!(parsed["method"], "");
}
#[test]
fn very_long_method_string() {
let long_method = "a".repeat(2000);
let input = format!("{{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"{long_method}\"}}\n");
let mut cursor = Cursor::new(input.into_bytes());
let (msg, _) = read_next_message(&mut cursor).unwrap().unwrap();
let parsed: serde_json::Value = serde_json::from_str(&msg).unwrap();
assert_eq!(parsed["method"].as_str().unwrap().len(), 2000);
}
#[test]
fn special_characters_in_method_name() {
let methods = [
"foo/bar",
"foo.bar.baz",
"método_ñ",
"日本語メソッド",
"method with spaces",
"method\twith\ttabs",
];
for method in methods {
let input = json!({"jsonrpc": "2.0", "id": 1, "method": method});
let line = format!("{input}\n");
let mut cursor = Cursor::new(line.into_bytes());
let (msg, _) = read_next_message(&mut cursor).unwrap().unwrap();
let parsed: serde_json::Value = serde_json::from_str(&msg).unwrap();
assert_eq!(parsed["method"].as_str().unwrap(), method);
}
}
#[test]
fn integer_overflow_in_id_field() {
let large_id = i64::MAX;
let input = format!("{{\"jsonrpc\":\"2.0\",\"id\":{large_id},\"method\":\"test\"}}\n");
let mut cursor = Cursor::new(input.into_bytes());
let (msg, _) = read_next_message(&mut cursor).unwrap().unwrap();
let parsed: serde_json::Value = serde_json::from_str(&msg).unwrap();
assert_eq!(parsed["id"].as_i64().unwrap(), i64::MAX);
}
#[test]
fn whitespace_only_lines_are_skipped() {
let input = b" \n\t\n \n{\"id\":1,\"method\":\"test\"}\n";
let mut cursor = Cursor::new(input.to_vec());
let result = read_next_message(&mut cursor).unwrap();
assert!(result.is_some());
let (msg, _) = result.unwrap();
assert!(msg.contains("\"method\":\"test\""));
}