use serde_json::json;
use super::{Match, compare, header_safe, header_text, mirrors, posts, sentinel_payload};
use crate::checks::draft::testkit::{META, client, events, post, trace};
use crate::context::TraceContext;
#[test]
fn header_safety_is_visible_ascii_with_no_edge_whitespace() {
for safe in ["us-west1", "a b", "tabbed\tinside", "", "~", "!"] {
assert!(header_safe(safe), "{safe:?} should be carried plainly");
}
for unsafe_value in [
" padded", "padded ", "\tpadded", "line1\nline2", "caf\u{e9}", "bell\u{7}here", "del\u{7f}", ] {
assert!(
!header_safe(unsafe_value),
"{unsafe_value:?} needs the sentinel"
);
}
}
#[test]
fn the_sentinel_is_recognised_only_when_spelled_exactly() {
assert_eq!(sentinel_payload("=?base64?YWJj?="), Some("YWJj"));
assert_eq!(sentinel_payload("=?base64??="), Some(""));
assert_eq!(sentinel_payload("=?BASE64?YWJj?="), None);
assert_eq!(sentinel_payload("=?base64?YWJj"), None);
assert_eq!(sentinel_payload("=?base64?="), None);
assert_eq!(sentinel_payload("plain"), None);
}
#[test]
fn a_value_is_rendered_into_a_header_by_its_json_type() {
assert_eq!(header_text(&json!("us-west1")), Some("us-west1".to_owned()));
assert_eq!(header_text(&json!(true)), Some("true".to_owned()));
assert_eq!(header_text(&json!(false)), Some("false".to_owned()));
assert_eq!(header_text(&json!(42)), Some("42".to_owned()));
assert_eq!(header_text(&json!(-7)), Some("-7".to_owned()));
assert_eq!(
header_text(&json!(u64::from(u32::MAX) * 8)),
Some("34359738360".to_owned())
);
assert_eq!(header_text(&json!(1.5)), None);
assert_eq!(header_text(&json!(null)), None);
assert_eq!(header_text(&json!(["a"])), None);
assert_eq!(header_text(&json!({"a": 1})), None);
}
#[test]
fn comparison_decodes_the_sentinel_before_judging() {
assert_eq!(compare("us-west1", "us-west1"), Match::Carried);
assert_eq!(
compare("=?base64?dXMtd2VzdDE=?=", "us-west1"),
Match::Carried
);
assert_eq!(
compare("=?base64?dXMtZWFzdDE=?=", "us-west1"),
Match::Mismatch
);
assert_eq!(compare("us-east1", "us-west1"), Match::Mismatch);
assert_eq!(
compare("=?base64?literal?=", "=?base64?literal?="),
Match::UnencodedSentinel
);
assert_eq!(
compare("=?base64?PT9iYXNlNjQ/bGl0ZXJhbD89?=", "=?base64?literal?="),
Match::Carried
);
}
fn mixed_transports(payload: &str) -> String {
trace(&[
r#"{"seq":0,"direction":"server-to-client","transport":"streamable-http","kind":"http","headers":{"content-type":"application/json"}}"#.to_owned(),
r#"{"seq":1,"direction":"client-to-server","transport":"stdio","kind":"http","headers":{"mcp-method":"ping"}}"#.to_owned(),
r#"{"seq":2,"direction":"client-to-server","transport":"stdio","kind":"message","payload":{"jsonrpc":"2.0","id":1,"method":"ping"}}"#.to_owned(),
post(3, r#"{"mcp-method":"ping"}"#),
client(4, payload),
])
}
#[test]
fn only_client_posts_over_streamable_http_are_posts() {
let document = mixed_transports(&format!(
r#"{{"jsonrpc":"2.0","id":1,"method":"ping","params":{{{META}}}}}"#
));
let parsed = events(&document);
let context = TraceContext::new(&parsed);
let found = posts(&context);
assert_eq!(found.len(), 1, "the server and stdio events are not POSTs");
assert_eq!(found[0].seq, 3);
assert_eq!(found[0].message_seq, 4);
assert_eq!(found[0].method(), Some("ping"));
}
#[test]
fn a_post_is_a_request_only_with_both_a_method_and_an_id() {
let cases = [
(r#"{"jsonrpc":"2.0","id":1,"method":"ping"}"#, true),
(r#"{"jsonrpc":"2.0","method":"notifications/x"}"#, false),
(r#"{"jsonrpc":"2.0","id":1,"result":{}}"#, false),
(r#"{"jsonrpc":"2.0","id":null,"method":"ping"}"#, false),
];
for (payload, expected) in cases {
let document = mixed_transports(payload);
let parsed = events(&document);
let context = TraceContext::new(&parsed);
let found = posts(&context);
assert_eq!(
found[0].is_request(),
expected,
"{payload} should{} be a request",
if expected { "" } else { " not" }
);
}
}
#[test]
fn mirrors_are_sourced_from_the_body_and_never_invented() {
let document = trace(&[
post(0, r#"{"mcp-method":"resources/read"}"#),
client(
1,
&format!(
r#"{{"jsonrpc":"2.0","id":1,"method":"resources/read","params":{{"uri":"file:///a.txt",{META}}}}}"#
),
),
]);
let parsed = events(&document);
let context = TraceContext::new(&parsed);
let found = posts(&context);
let resolved = mirrors(&found[0], &std::collections::BTreeMap::new());
let named: Vec<(&str, &str)> = resolved
.iter()
.map(|mirror| (mirror.header.as_str(), mirror.value.as_str()))
.collect();
assert_eq!(
named,
[
("mcp-method", "resources/read"),
("mcp-name", "file:///a.txt"),
],
"resources/read sources Mcp-Name from params.uri"
);
let bare = trace(&[
post(0, r#"{"mcp-method":"tools/call"}"#),
client(
1,
&format!(r#"{{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{{{META}}}}}"#),
),
]);
let parsed = events(&bare);
let context = TraceContext::new(&parsed);
let found = posts(&context);
let resolved = mirrors(&found[0], &std::collections::BTreeMap::new());
assert_eq!(resolved.len(), 1, "only Mcp-Method: params.name is absent");
assert!(
!resolved[0].encodable,
"Mcp-Method never takes the sentinel"
);
}