use proptest::prelude::*;
use serde_json::{json, Value};
#[derive(Debug, Clone)]
struct FuzzedRequest {
method: String,
params: Value,
}
impl Arbitrary for FuzzedRequest {
type Parameters = ();
type Strategy = BoxedStrategy<Self>;
fn arbitrary_with(_: Self::Parameters) -> Self::Strategy {
(
prop::string::string_regex("[a-zA-Z][a-zA-Z0-9_/]*").unwrap(),
any::<bool>().prop_flat_map(|has_params| {
if has_params {
prop::collection::vec(any::<String>(), 0..5)
.prop_map(|v| json!(v))
.boxed()
} else {
Just(json!(null)).boxed()
}
}),
)
.prop_map(|(method, params)| FuzzedRequest { method, params })
.boxed()
}
}
proptest! {
#[test]
fn test_server_handles_invalid_methods(request in any::<FuzzedRequest>()) {
let request_json = json!({
"jsonrpc": "2.0",
"id": 1,
"method": request.method,
"params": request.params
});
let serialized = serde_json::to_string(&request_json);
prop_assert!(serialized.is_ok());
}
#[test]
fn test_malformed_json_handling(
json_str in prop::string::string_regex(r#"\{"[a-z]+": [a-z0-9", ]*\}"#).unwrap()
) {
let result: Result<Value, _> = serde_json::from_str(&json_str);
let _ = result;
}
#[test]
fn test_tool_parameter_fuzzing(
file_path in prop::string::string_regex("[a-zA-Z0-9_/.]+").unwrap(),
line in 0u32..10000,
character in 0u32..1000,
) {
let tools = vec![
json!({
"name": "rust_analyzer_symbols",
"arguments": {"file_path": file_path.clone()}
}),
json!({
"name": "rust_analyzer_definition",
"arguments": {
"file_path": file_path.clone(),
"line": line,
"character": character
}
}),
json!({
"name": "rust_analyzer_hover",
"arguments": {
"file_path": file_path.clone(),
"line": line,
"character": character
}
}),
];
for tool in tools {
let request = json!({
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": tool
});
let serialized = serde_json::to_string(&request);
prop_assert!(serialized.is_ok());
}
}
#[test]
fn test_concurrent_request_ids(
ids in prop::collection::vec(0u64..1000000, 1..100)
) {
for id in ids {
let request = json!({
"jsonrpc": "2.0",
"id": id,
"method": "initialize",
"params": {}
});
let serialized = serde_json::to_string(&request);
prop_assert!(serialized.is_ok());
let deserialized: Value = serde_json::from_str(&serialized.unwrap()).unwrap();
prop_assert_eq!(deserialized["id"].as_u64(), Some(id));
}
}
#[test]
fn test_unicode_in_parameters(
unicode_str in "\\PC*", ) {
let request = json!({
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "rust_analyzer_symbols",
"arguments": {
"file_path": unicode_str
}
}
});
let serialized = serde_json::to_string(&request);
prop_assert!(serialized.is_ok());
let deserialized: Value = serde_json::from_str(&serialized.unwrap()).unwrap();
prop_assert_eq!(
deserialized["params"]["arguments"]["file_path"].as_str(),
Some(unicode_str.as_str())
);
}
#[test]
fn test_deeply_nested_params(
depth in 1usize..10,
) {
let mut params = json!({"value": "test"});
for _ in 0..depth {
params = json!({"nested": params});
}
let request = json!({
"jsonrpc": "2.0",
"id": 1,
"method": "test",
"params": params
});
let serialized = serde_json::to_string(&request);
prop_assert!(serialized.is_ok());
}
#[test]
fn test_notification_vs_request(
method in prop::string::string_regex("[a-z_/]+").unwrap(),
has_id in any::<bool>(),
) {
let mut message = json!({
"jsonrpc": "2.0",
"method": method
});
if has_id {
message["id"] = json!(1);
}
let serialized = serde_json::to_string(&message);
prop_assert!(serialized.is_ok());
let deserialized: Value = serde_json::from_str(&serialized.unwrap()).unwrap();
prop_assert_eq!(deserialized.get("id").is_some(), has_id);
}
}