use mcpkit_core::error::JsonRpcError;
use mcpkit_core::protocol::{
Cursor, JSONRPC_VERSION, Message, Notification, ProgressToken, Request, RequestId, Response,
};
use proptest::prelude::*;
use std::borrow::Cow;
fn arb_request_id() -> impl Strategy<Value = RequestId> {
prop_oneof![
any::<u64>().prop_map(RequestId::Number),
"[a-zA-Z0-9_-]{1,100}".prop_map(RequestId::String),
]
}
fn arb_method() -> impl Strategy<Value = Cow<'static, str>> {
prop_oneof![
"[a-z]+/[a-z_]+".prop_map(Cow::Owned),
"[a-z_]+".prop_map(Cow::Owned),
Just(Cow::Borrowed("tools/list")),
Just(Cow::Borrowed("tools/call")),
Just(Cow::Borrowed("resources/list")),
Just(Cow::Borrowed("resources/read")),
Just(Cow::Borrowed("prompts/list")),
Just(Cow::Borrowed("prompts/get")),
Just(Cow::Borrowed("initialize")),
Just(Cow::Borrowed("ping")),
]
}
fn arb_params() -> impl Strategy<Value = Option<serde_json::Value>> {
prop_oneof![
Just(None),
Just(Some(serde_json::json!({}))),
"[a-z_]{1,20}".prop_map(|k| Some(serde_json::json!({ k: "value" }))),
(any::<i32>(), any::<bool>()).prop_map(|(n, b)| {
Some(serde_json::json!({
"count": n,
"enabled": b,
"name": "test"
}))
}),
Just(Some(serde_json::json!({
"name": "test-tool",
"arguments": {
"query": "search term",
"limit": 10
}
}))),
proptest::collection::vec("[a-z]+", 0..5).prop_map(|v| Some(serde_json::Value::Array(
v.into_iter().map(serde_json::Value::String).collect()
))),
]
}
fn arb_result() -> impl Strategy<Value = serde_json::Value> {
prop_oneof![
Just(serde_json::json!({})),
Just(serde_json::json!({ "tools": [] })),
Just(serde_json::json!({
"resources": [
{ "uri": "file:///test.txt", "name": "test.txt" }
]
})),
Just(serde_json::json!({
"prompts": [
{ "name": "greeting", "description": "A friendly greeting" }
]
})),
any::<i64>().prop_map(|n| serde_json::json!(n)),
"[a-zA-Z0-9 ]{0,100}".prop_map(|s| serde_json::json!(s)),
any::<bool>().prop_map(|b| serde_json::json!(b)),
]
}
fn arb_error() -> impl Strategy<Value = JsonRpcError> {
let codes = prop_oneof![
Just(-32700), Just(-32600), Just(-32601), Just(-32602), Just(-32603), (-32099..=-32000i32), ];
let messages = prop_oneof![
Just("Parse error".to_string()),
Just("Invalid Request".to_string()),
Just("Method not found".to_string()),
Just("Invalid params".to_string()),
Just("Internal error".to_string()),
"[A-Za-z ]{5,50}".prop_map(|s| s),
];
let data = prop_oneof![
Just(None),
"[a-z ]+".prop_map(|s| Some(serde_json::json!({ "details": s }))),
];
(codes, messages, data).prop_map(|(code, message, data)| JsonRpcError {
code,
message,
data,
})
}
fn arb_request() -> impl Strategy<Value = Request> {
(arb_method(), arb_request_id(), arb_params()).prop_map(|(method, id, params)| {
let mut request = Request::new(method, id);
if let Some(p) = params {
request.params = Some(p);
}
request
})
}
fn arb_success_response() -> impl Strategy<Value = Response> {
(arb_request_id(), arb_result()).prop_map(|(id, result)| Response::success(id, result))
}
fn arb_error_response() -> impl Strategy<Value = Response> {
(arb_request_id(), arb_error()).prop_map(|(id, error)| Response::error(id, error))
}
fn arb_response() -> impl Strategy<Value = Response> {
prop_oneof![arb_success_response(), arb_error_response(),]
}
fn arb_notification() -> impl Strategy<Value = Notification> {
(arb_method(), arb_params()).prop_map(|(method, params)| {
let mut notification = Notification::new(method);
if let Some(p) = params {
notification.params = Some(p);
}
notification
})
}
fn arb_message() -> impl Strategy<Value = Message> {
prop_oneof![
arb_request().prop_map(Message::Request),
arb_response().prop_map(Message::Response),
arb_notification().prop_map(Message::Notification),
]
}
fn arb_progress_token() -> impl Strategy<Value = ProgressToken> {
prop_oneof![
any::<u64>().prop_map(ProgressToken::Number),
"[a-zA-Z0-9_-]{1,50}".prop_map(ProgressToken::String),
]
}
fn arb_cursor() -> impl Strategy<Value = Cursor> {
"[a-zA-Z0-9_=-]{1,100}".prop_map(Cursor::new)
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(256))]
#[test]
fn request_id_roundtrip(id in arb_request_id()) {
let json = serde_json::to_string(&id)?;
let parsed: RequestId = serde_json::from_str(&json)?;
prop_assert_eq!(id, parsed);
}
#[test]
fn request_id_display_matches_json(id in arb_request_id()) {
let display = id.to_string();
match &id {
RequestId::Number(n) => prop_assert_eq!(display, n.to_string()),
RequestId::String(s) => prop_assert_eq!(display, s.clone()),
RequestId::Null => prop_assert_eq!(display, "null".to_string()),
}
}
#[test]
fn request_roundtrip(request in arb_request()) {
let json = serde_json::to_string(&request)?;
let parsed: Request = serde_json::from_str(&json)?;
prop_assert_eq!(request.id, parsed.id);
prop_assert_eq!(request.method.as_ref(), parsed.method.as_ref());
prop_assert_eq!(request.params, parsed.params);
prop_assert_eq!(parsed.jsonrpc.as_ref(), JSONRPC_VERSION);
}
#[test]
fn request_has_jsonrpc_version(request in arb_request()) {
let json = serde_json::to_string(&request)?;
prop_assert!(json.contains(r#""jsonrpc":"2.0""#));
}
#[test]
fn request_has_id(request in arb_request()) {
let json = serde_json::to_string(&request)?;
prop_assert!(json.contains(r#""id":"#));
}
#[test]
fn request_has_method(request in arb_request()) {
let json = serde_json::to_string(&request)?;
prop_assert!(json.contains(r#""method":"#));
}
#[test]
fn response_roundtrip(response in arb_response()) {
let json = serde_json::to_string(&response)?;
let parsed: Response = serde_json::from_str(&json)?;
prop_assert_eq!(response.id, parsed.id);
prop_assert_eq!(response.result, parsed.result);
match (&response.error, &parsed.error) {
(Some(e1), Some(e2)) => {
prop_assert_eq!(e1.code, e2.code);
prop_assert_eq!(&e1.message, &e2.message);
prop_assert_eq!(&e1.data, &e2.data);
}
(None, None) => {}
_ => prop_assert!(false, "Error presence mismatch"),
}
}
#[test]
fn response_has_result_xor_error(response in arb_response()) {
let has_result = response.result.is_some();
let has_error = response.error.is_some();
prop_assert!(has_result ^ has_error, "Response must have result XOR error");
}
#[test]
fn success_response_is_success(response in arb_success_response()) {
prop_assert!(response.is_success());
prop_assert!(!response.is_error());
}
#[test]
fn error_response_is_error(response in arb_error_response()) {
prop_assert!(response.is_error());
prop_assert!(!response.is_success());
}
#[test]
fn notification_roundtrip(notification in arb_notification()) {
let json = serde_json::to_string(¬ification)?;
let parsed: Notification = serde_json::from_str(&json)?;
prop_assert_eq!(notification.method.as_ref(), parsed.method.as_ref());
prop_assert_eq!(notification.params, parsed.params);
prop_assert_eq!(parsed.jsonrpc.as_ref(), JSONRPC_VERSION);
}
#[test]
fn notification_has_no_id(notification in arb_notification()) {
let json = serde_json::to_string(¬ification)?;
let value: serde_json::Value = serde_json::from_str(&json)?;
prop_assert!(
value.get("id").is_none(),
"notification must not serialize a top-level id: {json}"
);
}
#[test]
fn message_roundtrip(message in arb_message()) {
let json = serde_json::to_string(&message)?;
let parsed: Message = serde_json::from_str(&json)?;
match (&message, &parsed) {
(Message::Request(m1), Message::Request(m2)) => {
prop_assert_eq!(&m1.id, &m2.id);
prop_assert_eq!(m1.method.as_ref(), m2.method.as_ref());
prop_assert_eq!(&m1.params, &m2.params);
}
(Message::Response(m1), Message::Response(m2)) => {
prop_assert_eq!(&m1.id, &m2.id);
prop_assert_eq!(&m1.result, &m2.result);
}
(Message::Notification(m1), Message::Notification(m2)) => {
prop_assert_eq!(m1.method.as_ref(), m2.method.as_ref());
prop_assert_eq!(&m1.params, &m2.params);
}
_ => prop_assert!(false, "Message variant mismatch"),
}
}
#[test]
fn message_type_detection(message in arb_message()) {
match &message {
Message::Request(_) => {
prop_assert!(message.is_request());
prop_assert!(!message.is_response());
prop_assert!(!message.is_notification());
prop_assert!(message.method().is_some());
prop_assert!(message.id().is_some());
}
Message::Response(_) => {
prop_assert!(!message.is_request());
prop_assert!(message.is_response());
prop_assert!(!message.is_notification());
prop_assert!(message.method().is_none());
prop_assert!(message.id().is_some());
}
Message::Notification(_) => {
prop_assert!(!message.is_request());
prop_assert!(!message.is_response());
prop_assert!(message.is_notification());
prop_assert!(message.method().is_some());
prop_assert!(message.id().is_none());
}
}
}
#[test]
fn progress_token_roundtrip(token in arb_progress_token()) {
let json = serde_json::to_string(&token)?;
let parsed: ProgressToken = serde_json::from_str(&json)?;
prop_assert_eq!(token, parsed);
}
#[test]
fn progress_token_display_matches_value(token in arb_progress_token()) {
let display = token.to_string();
match &token {
ProgressToken::Number(n) => prop_assert_eq!(display, n.to_string()),
ProgressToken::String(s) => prop_assert_eq!(display, s.clone()),
}
}
#[test]
fn cursor_roundtrip(cursor in arb_cursor()) {
let json = serde_json::to_string(&cursor)?;
let parsed: Cursor = serde_json::from_str(&json)?;
prop_assert_eq!(cursor.0, parsed.0);
}
#[test]
fn cursor_display_matches_value(cursor in arb_cursor()) {
prop_assert_eq!(cursor.to_string(), cursor.0);
}
#[test]
fn request_produces_valid_json(request in arb_request()) {
let json = serde_json::to_string(&request)?;
let _: serde_json::Value = serde_json::from_str(&json)?;
}
#[test]
fn response_produces_valid_json(response in arb_response()) {
let json = serde_json::to_string(&response)?;
let _: serde_json::Value = serde_json::from_str(&json)?;
}
#[test]
fn notification_produces_valid_json(notification in arb_notification()) {
let json = serde_json::to_string(¬ification)?;
let _: serde_json::Value = serde_json::from_str(&json)?;
}
#[test]
fn message_produces_valid_json(message in arb_message()) {
let json = serde_json::to_string(&message)?;
let _: serde_json::Value = serde_json::from_str(&json)?;
}
#[test]
fn error_roundtrip(error in arb_error()) {
let json = serde_json::to_string(&error)?;
let parsed: JsonRpcError = serde_json::from_str(&json)?;
prop_assert_eq!(error.code, parsed.code);
prop_assert_eq!(error.message, parsed.message);
prop_assert_eq!(error.data, parsed.data);
}
#[test]
fn error_codes_in_valid_range(error in arb_error()) {
prop_assert!(error.code < 0);
prop_assert!(error.code >= -32700);
}
}
#[cfg(test)]
mod additional_tests {
use super::*;
#[test]
fn test_invalid_json_fails() {
let invalid = "not json at all";
assert!(serde_json::from_str::<Message>(invalid).is_err());
}
#[test]
fn test_missing_jsonrpc_field() {
let json = r#"{"id": 1, "method": "test"}"#;
let result: Result<Request, _> = serde_json::from_str(json);
let _ = result;
}
#[test]
fn test_empty_method() {
let request = Request::new("", 1u64);
let json = serde_json::to_string(&request).unwrap();
let parsed: Request = serde_json::from_str(&json).unwrap();
assert_eq!(request.method.as_ref(), parsed.method.as_ref());
}
#[test]
fn test_max_u64_id() {
let request = Request::new("test", u64::MAX);
let json = serde_json::to_string(&request).unwrap();
let parsed: Request = serde_json::from_str(&json).unwrap();
assert_eq!(RequestId::Number(u64::MAX), parsed.id);
}
#[test]
fn test_response_into_result_success() {
let response = Response::success(1u64, serde_json::json!({"key": "value"}));
let result = response.into_result();
assert!(result.is_ok());
assert_eq!(result.unwrap(), serde_json::json!({"key": "value"}));
}
#[test]
fn test_response_into_result_error() {
let error = JsonRpcError {
code: -32600,
message: "Invalid Request".to_string(),
data: None,
};
let response = Response::error(1u64, error);
let result = response.into_result();
assert!(result.is_err());
assert_eq!(result.unwrap_err().code, -32600);
}
}