use pmcp::types::*;
use proptest::prelude::*;
#[cfg(test)]
mod protocol_invariants {
use super::*;
proptest! {
#[test]
fn property_jsonrpc_roundtrip(
id in prop::option::of(any::<i64>().prop_map(RequestId::Number)),
method in "[a-zA-Z_][a-zA-Z0-9_/]*",
params in prop::option::of(prop::collection::hash_map(
"[a-zA-Z_][a-zA-Z0-9_]*",
any::<i32>().prop_map(|i| serde_json::Value::Number(i.into())),
0..10
))
) {
let request = JSONRPCRequest {
jsonrpc: "2.0".to_string(),
id: id.unwrap_or(RequestId::Number(1)),
method: method.clone(),
params: params.clone().map(|p| serde_json::to_value(p).unwrap()),
};
let serialized = serde_json::to_string(&request).unwrap();
let deserialized: JSONRPCRequest = serde_json::from_str(&serialized).unwrap();
prop_assert_eq!(request.jsonrpc, deserialized.jsonrpc);
prop_assert_eq!(request.id, deserialized.id);
prop_assert_eq!(request.method, deserialized.method);
prop_assert_eq!(request.params, deserialized.params);
}
#[test]
fn property_error_code_roundtrip(
code in -32999i32..-32100i32
) {
use pmcp::error::ErrorCode;
let error_code = ErrorCode::other(code);
let as_i32 = error_code.as_i32();
let from_i32 = ErrorCode::other(as_i32);
prop_assert_eq!(error_code.as_i32(), from_i32.as_i32());
}
#[test]
fn property_request_id_uniqueness(
ids in prop::collection::vec(any::<i64>(), 1..100)
) {
let request_ids: Vec<RequestId> = ids.into_iter()
.map(RequestId::Number)
.collect();
let serialized: Vec<String> = request_ids.iter()
.map(|id| serde_json::to_string(id).unwrap())
.collect();
let mut unique_serialized = serialized.clone();
unique_serialized.sort();
unique_serialized.dedup();
prop_assert_eq!(serialized.len(), unique_serialized.len());
}
}
}
#[cfg(test)]
mod uri_template_properties {
use super::*;
use pmcp::shared::uri_template::UriTemplate;
proptest! {
#[test]
fn property_uri_template_deterministic(
template_str in "[a-zA-Z0-9_/{}-]*",
params_vec in prop::collection::vec(
("[a-zA-Z_][a-zA-Z0-9_]*", "[a-zA-Z0-9_-]*"),
0..5
)
) {
if let Ok(template) = UriTemplate::new(&template_str) {
let expanded1 = template.expand(¶ms_vec);
let expanded2 = template.expand(¶ms_vec);
prop_assert_eq!(expanded1.is_ok(), expanded2.is_ok());
if let (Ok(exp1), Ok(exp2)) = (expanded1, expanded2) {
prop_assert_eq!(exp1, exp2);
}
}
}
#[test]
fn property_uri_template_match_consistency(
segments in prop::collection::vec("[a-zA-Z0-9_-]+", 1..5)
) {
let template_str = format!("/{}", segments.join("/"));
let uri_str = format!("/{}", segments.join("/"));
if let Ok(template) = UriTemplate::new(&template_str) {
let matches1 = template.match_uri(&uri_str);
let matches2 = template.match_uri(&uri_str);
prop_assert_eq!(matches1.is_some(), matches2.is_some());
if let (Some(m1), Some(m2)) = (matches1, matches2) {
prop_assert_eq!(m1, m2);
}
}
}
}
}
#[cfg(test)]
mod capability_properties {
use super::*;
proptest! {
#[test]
fn property_client_capabilities_consistency(
roots_support in any::<bool>(),
sampling_support in any::<bool>()
) {
let mut capabilities = ClientCapabilities::minimal();
if roots_support {
capabilities.roots = Some(RootsCapabilities {
list_changed: true,
});
}
if sampling_support {
capabilities.sampling = Some(SamplingCapabilities::default());
}
let serialized = serde_json::to_string(&capabilities).unwrap();
let deserialized: ClientCapabilities = serde_json::from_str(&serialized).unwrap();
prop_assert_eq!(
capabilities.sampling.is_some(),
deserialized.sampling.is_some()
);
prop_assert_eq!(
capabilities.roots.is_some(),
deserialized.roots.is_some()
);
}
#[test]
fn property_server_capabilities_consistency(
tools_count in 0usize..10,
resources_count in 0usize..10,
prompts_count in 0usize..10
) {
let mut capabilities = ServerCapabilities::minimal();
if tools_count > 0 {
capabilities.tools = Some(ToolCapabilities {
list_changed: Some(true),
});
}
if resources_count > 0 {
capabilities.resources = Some(ResourceCapabilities {
subscribe: Some(true),
list_changed: Some(true),
});
}
if prompts_count > 0 {
capabilities.prompts = Some(PromptCapabilities {
list_changed: Some(true),
});
}
prop_assert_eq!(
capabilities.tools.is_some(),
tools_count > 0
);
prop_assert_eq!(
capabilities.resources.is_some(),
resources_count > 0
);
prop_assert_eq!(
capabilities.prompts.is_some(),
prompts_count > 0
);
}
}
}
#[cfg(test)]
mod transport_properties {
use super::*;
use pmcp::shared::transport::*;
proptest! {
#[test]
fn property_message_priority_ordering(
priorities in prop::collection::vec(
prop::strategy::Union::new([
Just(MessagePriority::High).boxed(),
Just(MessagePriority::Normal).boxed(),
Just(MessagePriority::Low).boxed(),
]),
1..10
)
) {
let mut sorted_priorities = priorities.clone();
sorted_priorities.sort();
if priorities.contains(&MessagePriority::High) {
prop_assert_eq!(sorted_priorities[sorted_priorities.len() - 1], MessagePriority::High);
}
if priorities.contains(&MessagePriority::Low) {
prop_assert_eq!(sorted_priorities[0], MessagePriority::Low);
}
}
#[test]
fn property_transport_message_metadata(
priority in prop::strategy::Union::new([
Just(MessagePriority::High).boxed(),
Just(MessagePriority::Normal).boxed(),
Just(MessagePriority::Low).boxed(),
])
) {
let metadata = MessageMetadata {
content_type: None,
priority: Some(priority),
flush: false,
};
prop_assert_eq!(metadata.priority, Some(priority));
}
}
}
#[cfg(test)]
mod error_properties {
use super::*;
use pmcp::error::*;
proptest! {
#[test]
fn property_error_consistency(
message in "[a-zA-Z0-9 _.-]{1,100}"
) {
let parse_error = Error::parse(message.clone());
let invalid_request = Error::validation(message.clone());
let method_not_found = Error::method_not_found(message.clone());
let invalid_params = Error::invalid_params(message.clone());
let internal_error = Error::internal(message.clone());
prop_assert!(parse_error.error_code().is_some());
let _has_code = invalid_request.error_code();
let _has_code = method_not_found.error_code();
let _has_code = invalid_params.error_code();
let _has_code = internal_error.error_code();
if let Some(code) = parse_error.error_code() {
let code_i32 = code.as_i32();
prop_assert!((-32999..=-32000).contains(&code_i32));
}
}
}
}
#[cfg(test)]
mod json_properties {
use super::*;
proptest! {
#[test]
fn property_json_stability(
numbers in prop::collection::vec(any::<i64>(), 0..50),
strings in prop::collection::vec("[a-zA-Z0-9 _.-]*", 0..20),
booleans in prop::collection::vec(any::<bool>(), 0..10)
) {
let mut json_obj = serde_json::Map::new();
for (i, num) in numbers.iter().enumerate() {
json_obj.insert(
format!("num_{}", i),
serde_json::Value::Number((*num).into())
);
}
for (i, s) in strings.iter().enumerate() {
json_obj.insert(
format!("str_{}", i),
serde_json::Value::String(s.clone())
);
}
for (i, b) in booleans.iter().enumerate() {
json_obj.insert(
format!("bool_{}", i),
serde_json::Value::Bool(*b)
);
}
let json_value = serde_json::Value::Object(json_obj);
let serialized1 = serde_json::to_string(&json_value).unwrap();
let deserialized: serde_json::Value = serde_json::from_str(&serialized1).unwrap();
let serialized2 = serde_json::to_string(&deserialized).unwrap();
let deser2: serde_json::Value = serde_json::from_str(&serialized2).unwrap();
prop_assert_eq!(json_value, deser2);
}
}
}