makepad/apps/director/tests/mcp_battery.rs
Admin 90c9a7b01a tests: the three binaries over ten seconds in release come under it -- the user's law is the whole suite in about two minutes, in release, and the CI now runs every test binary of its selection several at a time with a cap on each, so one long binary is the run's floor. Grok measured every binary the debug-era table named (release harness time): fourteen were already under 10 s and are untouched; three were not. Widgets --lib (1822 tests) 17.4 s -> 8.0 s: no single slow test; every test built a fresh Cx and registered the widget library on it (45 ms each, in release), and libtest runs each test on its own thread, so nothing could be kept. Test modules that build widgets now run their cases through on_test_cx on four threads that stay up and keep one registered Cx each (checkout_test_cx); finger, key, pass, draw-list and window state is cleared on every checkout so a case that locks a sweep or draws a menu leaves nothing for the next. The style-reload tests keep their own contexts: a reload would change the next case. The isolate cost table measures batches of 1 and 2 (the four sizes measured 30.06, 29.92, 29.63 and 29.49 ms per isolate: flat), and the two sheet-contrast tests read one walk of every sheet from a OnceLock instead of building the library twelve times each. Director mcp_battery 10.2 s -> 0.4 s: the slowloris test waited out the production head deadline of 10 s; McpServer::start_with_head_deadline takes the budget, production start still passes HEAD_DEADLINE_MS (asserted), and the test proves the socket is held until an 80 ms budget and closed after it. Piano acoustic_reference 11.6 s -> 8.6 s: the promotion renders are 2.5 s instead of 4.0 s, which covers every sample measure and onset read (2.0 s after an onset found in the first 0.5 s). No test is ignored, deleted or loosened. The review dropped the lane's short-circuit in script_mod (a library change for a test's sake: the one test helper that re-registered on a pooled context no longer does) and its per-module pool thread-locals that the checkout never read.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-22 12:29:54 +02:00

912 lines
31 KiB
Rust

//! Adversarial integration battery for Studio's streamable-HTTP MCP endpoint.
//!
//! Real loopback sockets, `std::net::TcpStream` clients, no extra crates.
use makepad_director::mcp::*;
use std::io::{self, Read, Write};
use std::net::{Ipv4Addr, Shutdown, TcpStream};
use std::path::PathBuf;
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
struct Fake {
huge: bool,
calls: Mutex<Vec<String>>,
}
impl ToolDispatcher for Fake {
fn list(&self, caller: &LaneCaller) -> Vec<ToolDef> {
vec![ToolDef::new(
"code_outline",
format!("outline for {}", caller.lane),
r#"{"type":"object","properties":{"scope":{"type":"string"}},"additionalProperties":false}"#,
Risk::Read,
)]
}
fn call(&self, caller: &LaneCaller, name: &str, args: &Value, request_id: &str) -> Outcome {
self.calls.lock().expect("fake dispatcher poisoned").push(name.to_string());
if self.huge {
return Outcome { text: "x".repeat(20 * 1024), is_error: false };
}
if name != "code_outline" {
return Outcome { text: format!("unknown tool {name}"), is_error: true };
}
let scope = args.get("scope").and_then(Value::as_str).unwrap_or("");
Outcome {
text: format!(
r#"{{"lane":"{}","tool":"{name}","scope":"{scope}","request_id":"{request_id}"}}"#,
caller.lane
),
is_error: false,
}
}
}
fn temp_dir(tag: &str) -> PathBuf {
use std::sync::atomic::{AtomicU64, Ordering};
static N: AtomicU64 = AtomicU64::new(0);
let dir = std::env::temp_dir().join(format!(
"studio-mcp-battery-{tag}-{}-{}",
std::process::id(),
N.fetch_add(1, Ordering::Relaxed)
));
std::fs::create_dir_all(&dir).unwrap();
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(&dir, std::fs::Permissions::from_mode(0o700)).unwrap();
}
dir
}
struct Reply {
status: u16,
headers: Vec<(String, String)>,
body: Vec<u8>,
}
impl Reply {
fn header(&self, name: &str) -> Option<&str> {
self.headers.iter().find(|(k, _)| k == name).map(|(_, v)| v.as_str())
}
fn json(&self) -> Value {
parse_json(&self.body).unwrap_or_else(|e| {
panic!("body is not JSON ({e}): {:?}", String::from_utf8_lossy(&self.body))
})
}
fn rpc_error_code(&self) -> i64 {
self.json().get("error").and_then(|e| e.get("code")).and_then(Value::as_i64).unwrap_or_else(|| {
panic!("missing error.code in {:?}", String::from_utf8_lossy(&self.body))
})
}
fn rpc_id_i64(&self) -> Option<i64> {
self.json().get("id").and_then(Value::as_i64)
}
}
fn port_from_url(url: &str) -> u16 {
let rest = url.strip_prefix("http://127.0.0.1:").expect("url() is loopback http");
let port = rest.split('/').next().expect("url() has a path");
port.parse().expect("url() port")
}
fn connect(port: u16, read_ms: u64) -> TcpStream {
let stream = TcpStream::connect((Ipv4Addr::LOCALHOST, port)).unwrap();
stream.set_nodelay(true).unwrap();
stream.set_read_timeout(Some(Duration::from_millis(read_ms.max(1)))).unwrap();
stream.set_write_timeout(Some(Duration::from_millis(read_ms.max(1)))).unwrap();
stream
}
fn request_bytes(
method: &str,
path: &str,
host: &str,
extra: &[(&str, &str)],
body: &[u8],
close: bool,
) -> Vec<u8> {
let mut req = format!("{method} {path} HTTP/1.1\r\nHost: {host}\r\n");
req.push_str(if close { "Connection: close\r\n" } else { "Connection: keep-alive\r\n" });
for (k, v) in extra {
req.push_str(&format!("{k}: {v}\r\n"));
}
if !body.is_empty() || method.eq_ignore_ascii_case("POST") {
req.push_str(&format!("Content-Length: {}\r\n", body.len()));
}
req.push_str("\r\n");
let mut out = req.into_bytes();
out.extend_from_slice(body);
out
}
fn read_reply(stream: &mut TcpStream, leftover: &mut Vec<u8>) -> io::Result<Reply> {
let mut data = std::mem::take(leftover);
let mut tmp = [0u8; 4096];
let header_end = loop {
if let Some(i) = data.windows(4).position(|w| w == b"\r\n\r\n") {
break i;
}
let n = stream.read(&mut tmp)?;
if n == 0 {
return Err(io::Error::new(
io::ErrorKind::UnexpectedEof,
format!("eof with {} header bytes", data.len()),
));
}
data.extend_from_slice(&tmp[..n]);
if data.len() > 128 * 1024 {
return Err(io::Error::new(io::ErrorKind::InvalidData, "headers too large"));
}
};
let header_text = String::from_utf8_lossy(&data[..header_end]).into_owned();
let rest = data[header_end + 4..].to_vec();
let mut lines = header_text.split("\r\n");
let status_line = lines.next().unwrap_or("");
let status: u16 = status_line
.split(' ')
.nth(1)
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, status_line.to_string()))?
.parse()
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, format!("{e}: {status_line}")))?;
let mut headers = Vec::new();
let mut length = 0usize;
for line in lines {
if line.is_empty() {
continue;
}
let (k, v) = line.split_once(':').ok_or_else(|| {
io::Error::new(io::ErrorKind::InvalidData, format!("malformed header {line:?}"))
})?;
if k.eq_ignore_ascii_case("content-length") {
length = v.trim().parse().unwrap_or(0);
}
headers.push((k.to_ascii_lowercase(), v.trim().to_string()));
}
let mut body = rest;
while body.len() < length {
let n = stream.read(&mut tmp)?;
if n == 0 {
break;
}
body.extend_from_slice(&tmp[..n]);
}
if body.len() < length {
return Err(io::Error::new(io::ErrorKind::UnexpectedEof, "short body"));
}
leftover.extend_from_slice(&body[length..]);
body.truncate(length);
Ok(Reply { status, headers, body })
}
fn http_host(
port: u16,
method: &str,
path: &str,
host: &str,
extra: &[(&str, &str)],
body: &[u8],
) -> Reply {
let mut stream = connect(port, 4000);
let bytes = request_bytes(method, path, host, extra, body, true);
stream.write_all(&bytes).unwrap();
let _ = stream.shutdown(Shutdown::Write);
read_reply(&mut stream, &mut Vec::new()).unwrap()
}
fn http(port: u16, method: &str, path: &str, extra: &[(&str, &str)], body: &[u8]) -> Reply {
http_host(port, method, path, &format!("127.0.0.1:{port}"), extra, body)
}
fn rpc(port: u16, token: &str, body: &str) -> Reply {
let auth = format!("Bearer {token}");
http(
port,
"POST",
"/mcp",
&[("Content-Type", "application/json"), ("Authorization", &auth)],
body.as_bytes(),
)
}
fn rpc_on(stream: &mut TcpStream, leftover: &mut Vec<u8>, port: u16, token: &str, body: &str, close: bool) -> Reply {
let host = format!("127.0.0.1:{port}");
let auth = format!("Bearer {token}");
let bytes = request_bytes(
"POST",
"/mcp",
&host,
&[("Content-Type", "application/json"), ("Authorization", &auth)],
body.as_bytes(),
close,
);
stream.write_all(&bytes).unwrap();
read_reply(stream, leftover).unwrap()
}
fn start(huge: bool) -> (McpServer, Arc<TokenStore>, Arc<Fake>, PathBuf) {
let dir = temp_dir("srv");
let tokens = Arc::new(TokenStore::open(&dir).unwrap());
let fake = Arc::new(Fake { huge, calls: Mutex::new(Vec::new()) });
let server = McpServer::start(tokens.clone(), fake.clone()).unwrap();
(server, tokens, fake, dir)
}
fn ping_body() -> &'static str {
r#"{"jsonrpc":"2.0","id":1,"method":"ping"}"#
}
#[test]
fn handshake_then_call_roundtrip_with_request_ids() {
let (server, tokens, fake, dir) = start(false);
let port = port_from_url(&server.url());
let token = tokens.mint("lane-a", "owner-1").unwrap();
let t = token.as_str();
let init = rpc(
port,
t,
r#"{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"t","version":"0"}}}"#,
);
assert_eq!(init.status, 200);
let v = init.json();
assert_eq!(v.get("id").and_then(Value::as_i64), Some(1));
assert_eq!(
v.get("result").and_then(|r| r.get("protocolVersion")).and_then(Value::as_str),
Some("2025-06-18")
);
let initialized = rpc(port, t, r#"{"jsonrpc":"2.0","method":"notifications/initialized"}"#);
assert_eq!(initialized.status, 202);
assert!(initialized.body.is_empty());
let list = rpc(port, t, r#"{"jsonrpc":"2.0","id":"list-1","method":"tools/list"}"#);
assert_eq!(list.status, 200);
let v = list.json();
assert_eq!(v.get("id").and_then(Value::as_str), Some("list-1"));
let tools = v.get("result").and_then(|r| r.get("tools")).and_then(Value::as_arr).unwrap();
assert_eq!(tools.len(), 1);
assert_eq!(tools[0].get("name").and_then(Value::as_str), Some("code_outline"));
let call = rpc(
port,
t,
r#"{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"code_outline","arguments":{"scope":"widgets::dock"}}}"#,
);
assert_eq!(call.status, 200);
let v = call.json();
assert_eq!(v.get("id").and_then(Value::as_i64), Some(3));
let text = v
.get("result")
.and_then(|r| r.get("content"))
.and_then(Value::as_arr)
.and_then(|c| c.first())
.and_then(|c| c.get("text"))
.and_then(Value::as_str)
.unwrap();
let inner = parse_json(text.as_bytes()).unwrap();
assert_eq!(inner.get("tool").and_then(Value::as_str), Some("code_outline"));
assert_eq!(inner.get("request_id").and_then(Value::as_str), Some("mcp-lane-a-3"));
let calls = fake.calls.lock().unwrap();
assert_eq!(calls.as_slice(), ["code_outline"]);
drop(server);
let _ = std::fs::remove_dir_all(dir);
}
#[test]
fn wrong_bearer_401_with_www_authenticate() {
let (server, _tokens, _fake, dir) = start(false);
let port = port_from_url(&server.url());
let reply = http(
port,
"POST",
"/mcp",
&[("Content-Type", "application/json"), ("Authorization", "Bearer 00")],
b"{}",
);
assert_eq!(reply.status, 401);
let www = reply.header("www-authenticate").unwrap_or("");
assert!(www.contains("Bearer"), "WWW-Authenticate: {www:?}");
drop(server);
let _ = std::fs::remove_dir_all(dir);
}
#[test]
fn missing_bearer_401() {
let (server, _tokens, _fake, dir) = start(false);
let port = port_from_url(&server.url());
let reply = http(port, "POST", "/mcp", &[("Content-Type", "application/json")], b"{}");
assert_eq!(reply.status, 401);
let www = reply.header("www-authenticate").unwrap_or("");
assert!(www.contains("Bearer"), "WWW-Authenticate: {www:?}");
drop(server);
let _ = std::fs::remove_dir_all(dir);
}
#[test]
fn revoked_token_401_after_success() {
let (server, tokens, _fake, dir) = start(false);
let port = port_from_url(&server.url());
let token = tokens.mint("lane-d", "owner-1").unwrap();
assert_eq!(rpc(port, token.as_str(), ping_body()).status, 200);
assert!(tokens.revoke("lane-d").unwrap());
assert_eq!(rpc(port, token.as_str(), ping_body()).status, 401);
drop(server);
let _ = std::fs::remove_dir_all(dir);
}
#[test]
fn reminted_token_invalidates_old() {
let (server, tokens, _fake, dir) = start(false);
let port = port_from_url(&server.url());
let first = tokens.mint("lane-d", "owner-1").unwrap();
assert_eq!(rpc(port, first.as_str(), ping_body()).status, 200);
let second = tokens.mint("lane-d", "owner-1").unwrap();
assert_ne!(first.as_str(), second.as_str());
assert_eq!(rpc(port, first.as_str(), ping_body()).status, 401);
assert_eq!(rpc(port, second.as_str(), ping_body()).status, 200);
drop(server);
let _ = std::fs::remove_dir_all(dir);
}
#[test]
fn origin_foreign_403() {
let (server, tokens, _fake, dir) = start(false);
let port = port_from_url(&server.url());
let token = tokens.mint("lane-b", "owner-1").unwrap();
let auth = format!("Bearer {}", token.as_str());
let reply = http(
port,
"POST",
"/mcp",
&[
("Content-Type", "application/json"),
("Authorization", &auth),
("Origin", "http://evil.example"),
],
b"{}",
);
assert_eq!(reply.status, 403);
drop(server);
let _ = std::fs::remove_dir_all(dir);
}
#[test]
fn host_mismatch_403() {
let (server, tokens, _fake, dir) = start(false);
let port = port_from_url(&server.url());
let token = tokens.mint("lane-b", "owner-1").unwrap();
let auth = format!("Bearer {}", token.as_str());
let extra = [("Content-Type", "application/json"), ("Authorization", auth.as_str())];
let foreign = http_host(port, "POST", "/mcp", "evil.example", &extra, ping_body().as_bytes());
assert_eq!(foreign.status, 403, "foreign Host must be refused");
let wrong_port = http_host(
port,
"POST",
"/mcp",
&format!("127.0.0.1:{}", port.wrapping_add(1).max(1)),
&extra,
ping_body().as_bytes(),
);
assert_eq!(wrong_port.status, 403, "loopback Host with the wrong port must be refused");
let ipv4 = http_host(port, "POST", "/mcp", "127.0.0.1", &extra, ping_body().as_bytes());
assert_eq!(ipv4.status, 200, "Host 127.0.0.1 without a port is loopback");
let localhost = http_host(port, "POST", "/mcp", "localhost", &extra, ping_body().as_bytes());
assert_eq!(localhost.status, 200, "Host localhost without a port is loopback");
let bracket = http_host(
port,
"POST",
"/mcp",
&format!("[::1]:{port}"),
&extra,
ping_body().as_bytes(),
);
assert_eq!(bracket.status, 200, "Host [::1]:<port> is listed as loopback");
drop(server);
let _ = std::fs::remove_dir_all(dir);
}
#[test]
fn get_405_with_allow_post() {
let (server, tokens, _fake, dir) = start(false);
let port = port_from_url(&server.url());
let token = tokens.mint("lane-b", "owner-1").unwrap();
let auth = format!("Bearer {}", token.as_str());
let reply = http(port, "GET", "/mcp", &[("Authorization", &auth)], b"");
assert_eq!(reply.status, 405);
assert_eq!(reply.header("allow"), Some("POST"));
drop(server);
let _ = std::fs::remove_dir_all(dir);
}
#[test]
fn delete_405() {
let (server, tokens, _fake, dir) = start(false);
let port = port_from_url(&server.url());
let token = tokens.mint("lane-b", "owner-1").unwrap();
let auth = format!("Bearer {}", token.as_str());
let reply = http(port, "DELETE", "/mcp", &[("Authorization", &auth)], b"");
assert_eq!(reply.status, 405);
assert_eq!(reply.header("allow"), Some("POST"));
drop(server);
let _ = std::fs::remove_dir_all(dir);
}
#[test]
fn unknown_path_404() {
let (server, tokens, _fake, dir) = start(false);
let port = port_from_url(&server.url());
let token = tokens.mint("lane-b", "owner-1").unwrap();
let auth = format!("Bearer {}", token.as_str());
let reply = http(
port,
"POST",
"/other",
&[("Content-Type", "application/json"), ("Authorization", &auth)],
b"{}",
);
assert_eq!(reply.status, 404);
drop(server);
let _ = std::fs::remove_dir_all(dir);
}
#[test]
fn wrong_content_type_415() {
let (server, tokens, _fake, dir) = start(false);
let port = port_from_url(&server.url());
let token = tokens.mint("lane-b", "owner-1").unwrap();
let auth = format!("Bearer {}", token.as_str());
let reply = http(
port,
"POST",
"/mcp",
&[("Content-Type", "text/plain"), ("Authorization", &auth)],
b"{}",
);
assert_eq!(reply.status, 415);
drop(server);
let _ = std::fs::remove_dir_all(dir);
}
#[test]
fn oversized_body_413_exactly_at_boundary() {
let (server, tokens, _fake, dir) = start(false);
let port = port_from_url(&server.url());
let token = tokens.mint("lane-b", "owner-1").unwrap();
let auth = format!("Bearer {}", token.as_str());
let extra = [("Content-Type", "application/json"), ("Authorization", auth.as_str())];
let ping = ping_body().as_bytes();
let mut at_cap = vec![b' '; MAX_BODY_BYTES as usize];
at_cap[..ping.len()].copy_from_slice(ping);
let ok = http(port, "POST", "/mcp", &extra, &at_cap);
assert_ne!(ok.status, 413, "64 KiB body must be accepted");
assert_eq!(ok.status, 200);
assert_eq!(ok.rpc_id_i64(), Some(1));
let mut over = vec![b'x'; MAX_BODY_BYTES as usize + 1];
over[..ping.len()].copy_from_slice(ping);
let too_large = http(port, "POST", "/mcp", &extra, &over);
assert_eq!(too_large.status, 413);
drop(server);
let _ = std::fs::remove_dir_all(dir);
}
#[test]
fn jsonrpc_parse_error_minus_32700() {
let (server, tokens, _fake, dir) = start(false);
let port = port_from_url(&server.url());
let token = tokens.mint("lane-b", "owner-1").unwrap();
let reply = rpc(port, token.as_str(), "{not json");
assert_eq!(reply.status, 400);
assert_eq!(reply.rpc_error_code(), PARSE_ERROR);
drop(server);
let _ = std::fs::remove_dir_all(dir);
}
#[test]
fn invalid_request_minus_32600() {
let (server, tokens, _fake, dir) = start(false);
let port = port_from_url(&server.url());
let token = tokens.mint("lane-b", "owner-1").unwrap();
let t = token.as_str();
let missing = rpc(port, t, r#"{"id":1,"method":"ping"}"#);
assert_eq!(missing.status, 400);
assert_eq!(missing.rpc_error_code(), INVALID_REQUEST);
let batch = rpc(port, t, r#"[{"jsonrpc":"2.0","id":1,"method":"ping"}]"#);
assert_eq!(batch.status, 400);
assert_eq!(batch.rpc_error_code(), INVALID_REQUEST);
drop(server);
let _ = std::fs::remove_dir_all(dir);
}
#[test]
fn method_not_found_minus_32601() {
let (server, tokens, _fake, dir) = start(false);
let port = port_from_url(&server.url());
let token = tokens.mint("lane-b", "owner-1").unwrap();
let reply = rpc(port, token.as_str(), r#"{"jsonrpc":"2.0","id":4,"method":"resources/list"}"#);
assert_eq!(reply.status, 200);
assert_eq!(reply.rpc_error_code(), METHOD_NOT_FOUND);
assert_eq!(reply.json().get("id").and_then(Value::as_i64), Some(4));
drop(server);
let _ = std::fs::remove_dir_all(dir);
}
#[test]
fn invalid_params_minus_32602() {
let (server, tokens, _fake, dir) = start(false);
let port = port_from_url(&server.url());
let token = tokens.mint("lane-b", "owner-1").unwrap();
let reply =
rpc(port, token.as_str(), r#"{"jsonrpc":"2.0","id":9,"method":"tools/call","params":{"arguments":{}}}"#);
assert_eq!(reply.status, 200);
assert_eq!(reply.rpc_error_code(), INVALID_PARAMS);
drop(server);
let _ = std::fs::remove_dir_all(dir);
}
#[test]
fn unknown_tool_error_result() {
let (server, tokens, fake, dir) = start(false);
let port = port_from_url(&server.url());
let token = tokens.mint("lane-b", "owner-1").unwrap();
let reply = rpc(
port,
token.as_str(),
r#"{"jsonrpc":"2.0","id":5,"method":"tools/call","params":{"name":"missing_tool","arguments":{}}}"#,
);
assert_eq!(reply.status, 200);
let v = reply.json();
assert!(v.get("error").is_none(), "unknown tool is a result, not a JSON-RPC error");
assert_eq!(v.get("result").and_then(|r| r.get("isError")).and_then(Value::as_bool), Some(true));
assert_eq!(fake.calls.lock().unwrap().as_slice(), ["missing_tool"]);
drop(server);
let _ = std::fs::remove_dir_all(dir);
}
#[test]
fn result_over_16kib_is_refused_not_truncated() {
let (server, tokens, _fake, dir) = start(true);
let port = port_from_url(&server.url());
let token = tokens.mint("lane-c", "owner-1").unwrap();
let reply = rpc(
port,
token.as_str(),
r#"{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"code_outline","arguments":{}}}"#,
);
assert_eq!(reply.status, 200);
assert!(
parse_json(&reply.body).is_ok(),
"oversized-result refusal must still be valid JSON, got {:?}",
String::from_utf8_lossy(&reply.body)
);
assert!(
reply.body.len() < 16 * 1024,
"must refuse rather than emit the 20 KiB payload, body is {} bytes",
reply.body.len()
);
assert!(!reply.body.windows(32).any(|w| w.iter().all(|&b| b == b'x')));
let err = reply.json().get("error").cloned().expect("json-rpc error");
assert_eq!(err.get("code").and_then(Value::as_i64), Some(INTERNAL_ERROR));
let msg = err.get("message").and_then(Value::as_str).unwrap_or("");
assert!(msg.contains("page"), "error should tell the caller to page: {msg:?}");
assert!(msg.contains("cursor"), "error should mention the cursor: {msg:?}");
drop(server);
let _ = std::fs::remove_dir_all(dir);
}
#[test]
fn notification_returns_202_empty() {
let (server, tokens, _fake, dir) = start(false);
let port = port_from_url(&server.url());
let token = tokens.mint("lane-b", "owner-1").unwrap();
let t = token.as_str();
let initialized = rpc(port, t, r#"{"jsonrpc":"2.0","method":"notifications/initialized"}"#);
assert_eq!(initialized.status, 202);
assert!(initialized.body.is_empty());
let unknown = rpc(port, t, r#"{"jsonrpc":"2.0","method":"notifications/not-a-real-note"}"#);
assert_eq!(unknown.status, 202);
assert!(unknown.body.is_empty());
drop(server);
let _ = std::fs::remove_dir_all(dir);
}
#[test]
fn concurrency_bounded_503_with_retry_after() {
let (server, tokens, _fake, dir) = start(false);
let port = port_from_url(&server.url());
let token = tokens.mint("lane-g", "owner-1").unwrap();
let t = token.as_str();
// Workers take from a 16-slot channel. Connecting all 20 before any worker
// has called recv overflows at 17 and 503s a connection we wanted to keep.
// Park the four workers first, then fill the queue.
std::thread::sleep(Duration::from_millis(50));
let mut held = Vec::new();
for _ in 0..WORKERS {
held.push(connect(port, 4000));
}
std::thread::sleep(Duration::from_millis(200));
for _ in 0..QUEUE {
held.push(connect(port, 4000));
}
std::thread::sleep(Duration::from_millis(50));
let deadline = Instant::now() + Duration::from_secs(4);
let mut refused = None;
let mut extras = Vec::new();
while Instant::now() < deadline {
let mut overflow = connect(port, 500);
match read_reply(&mut overflow, &mut Vec::new()) {
Ok(reply) if reply.status == 503 => {
refused = Some(reply);
break;
}
Ok(reply) => panic!("overflow connection got {} instead of 503", reply.status),
Err(_) => {
extras.push(overflow);
std::thread::sleep(Duration::from_millis(50));
}
}
}
let refused = refused.expect("overflow connection got no 503 within 4 s");
assert_eq!(refused.status, 503);
assert_eq!(refused.header("retry-after"), Some("1"));
let host = format!("127.0.0.1:{port}");
let auth = format!("Bearer {t}");
let ping = request_bytes(
"POST",
"/mcp",
&host,
&[("Content-Type", "application/json"), ("Authorization", &auth)],
ping_body().as_bytes(),
true,
);
for stream in held.iter_mut() {
stream.write_all(&ping).unwrap();
}
for (i, stream) in held.iter_mut().enumerate() {
let reply = read_reply(stream, &mut Vec::new())
.unwrap_or_else(|e| panic!("held connection {i} did not answer: {e}"));
assert_eq!(reply.status, 200, "held connection {i} status");
}
let _ = extras;
drop(held);
drop(server);
let _ = std::fs::remove_dir_all(dir);
}
#[test]
fn keep_alive_serves_256_then_closes() {
let (server, tokens, _fake, dir) = start(false);
let port = port_from_url(&server.url());
let token = tokens.mint("lane-k", "owner-1").unwrap();
let mut stream = connect(port, 4000);
let mut leftover = Vec::new();
for i in 0..MAX_REQUESTS_PER_CONN {
let reply = rpc_on(&mut stream, &mut leftover, port, token.as_str(), ping_body(), false);
assert_eq!(reply.status, 200, "keep-alive request {} of {}", i + 1, MAX_REQUESTS_PER_CONN);
if i + 1 < MAX_REQUESTS_PER_CONN {
let conn = reply.header("connection").unwrap_or("");
assert!(!conn.eq_ignore_ascii_case("close"), "request {} closed early ({conn})", i + 1);
}
}
let host = format!("127.0.0.1:{port}");
let auth = format!("Bearer {}", token.as_str());
let extra = request_bytes(
"POST",
"/mcp",
&host,
&[("Content-Type", "application/json"), ("Authorization", &auth)],
ping_body().as_bytes(),
false,
);
let write_err = stream.write_all(&extra).err();
match read_reply(&mut stream, &mut leftover) {
Err(_) => {}
Ok(reply) => panic!("257th request got HTTP {} instead of close", reply.status),
}
let _ = write_err;
drop(server);
let _ = std::fs::remove_dir_all(dir);
}
#[test]
fn slowloris_partial_header_times_out() {
// Production `McpServer::start` still arms HEAD_DEADLINE_MS (10 s). The
// close path is the same `next_request` deadline; 80 ms is long enough
// to show the socket is held until that budget and short enough that the
// binary is not a 10 s sleep. Half the budget is the floor: an immediate
// reject would be a different failure.
assert_eq!(HEAD_DEADLINE_MS, 10_000);
const DEADLINE_MS: u64 = 80;
let dir = temp_dir("srv");
let tokens = Arc::new(TokenStore::open(&dir).unwrap());
let fake = Arc::new(Fake { huge: false, calls: Mutex::new(Vec::new()) });
let server = McpServer::start_with_head_deadline(tokens, fake, DEADLINE_MS).unwrap();
let port = port_from_url(&server.url());
let budget = Duration::from_millis(DEADLINE_MS + 1000);
let mut stream = connect(port, DEADLINE_MS + 1000);
stream.write_all(b"POST /mc").unwrap();
let start = Instant::now();
let mut buf = [0u8; 256];
let result = stream.read(&mut buf);
let elapsed = start.elapsed();
assert!(
elapsed >= Duration::from_millis(DEADLINE_MS / 2),
"partial head closed in {elapsed:?}, before the {DEADLINE_MS} ms slowloris budget"
);
assert!(
elapsed <= budget,
"slowloris hang {elapsed:?} exceeded deadline {DEADLINE_MS} ms + 1 s"
);
match result {
Ok(0) => {}
Ok(n) => {
let text = String::from_utf8_lossy(&buf[..n]);
assert!(
text.contains("408") || text.contains("HTTP/1.1"),
"partial-head close produced {text:?}"
);
}
Err(e) => {
assert!(
matches!(
e.kind(),
io::ErrorKind::UnexpectedEof
| io::ErrorKind::ConnectionReset
| io::ErrorKind::BrokenPipe
| io::ErrorKind::TimedOut
| io::ErrorKind::WouldBlock
),
"unexpected slowloris error: {e:?}"
);
}
}
drop(server);
let _ = std::fs::remove_dir_all(dir);
}
#[test]
fn token_store_persistence_roundtrip_0600() {
let dir = temp_dir("persist");
let token = {
let store = TokenStore::open(&dir).unwrap();
store.mint("lane-e", "owner-9").unwrap()
};
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let meta = std::fs::metadata(dir.join("mcp").join("tokens.txt")).unwrap();
assert_eq!(meta.permissions().mode() & 0o777, 0o600);
let dmeta = std::fs::metadata(dir.join("mcp")).unwrap();
assert_eq!(dmeta.permissions().mode() & 0o777, 0o700);
}
let store = TokenStore::open(&dir).unwrap();
let caller = store.authorize(token.as_str()).unwrap();
assert_eq!(caller, LaneCaller { lane: "lane-e".into(), owner: "owner-9".into() });
let _ = std::fs::remove_dir_all(dir);
}
#[test]
fn token_store_refuses_tampered_file() {
let dir = temp_dir("tamper");
{
let store = TokenStore::open(&dir).unwrap();
let _ = store.mint("lane-e", "owner-10").unwrap();
}
std::fs::write(dir.join("mcp").join("tokens.txt"), "lane-e owner-10 zz\n").unwrap();
assert!(TokenStore::open(&dir).is_err());
let _ = std::fs::remove_dir_all(dir);
}
#[test]
fn token_store_refuses_world_readable_file() {
let dir = temp_dir("world");
{
let store = TokenStore::open(&dir).unwrap();
let _ = store.mint("lane-e", "owner-10").unwrap();
}
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let path = dir.join("mcp").join("tokens.txt");
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o644)).unwrap();
let err = match TokenStore::open(&dir) {
Ok(_) => panic!("world-readable token file must be refused"),
Err(e) => e,
};
assert!(
err.contains("0600") || err.contains("owned"),
"unexpected refusal: {err}"
);
}
let _ = std::fs::remove_dir_all(dir);
}
#[test]
fn malformed_utf8_body_400() {
let (server, tokens, _fake, dir) = start(false);
let port = port_from_url(&server.url());
let token = tokens.mint("lane-b", "owner-1").unwrap();
let auth = format!("Bearer {}", token.as_str());
let body = b"{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"ping\"}\xff";
let reply = http(
port,
"POST",
"/mcp",
&[("Content-Type", "application/json"), ("Authorization", &auth)],
body,
);
assert_eq!(reply.status, 400);
drop(server);
let _ = std::fs::remove_dir_all(dir);
}
#[test]
fn duplicate_authorization_header_400() {
let (server, tokens, _fake, dir) = start(false);
let port = port_from_url(&server.url());
let token = tokens.mint("lane-b", "owner-1").unwrap();
let mut stream = connect(port, 4000);
let req = format!(
"POST /mcp HTTP/1.1\r\nHost: 127.0.0.1:{port}\r\nConnection: close\r\n\
Authorization: Bearer {t}\r\nAuthorization: Bearer {t}\r\n\
Content-Type: application/json\r\nContent-Length: 2\r\n\r\n{{}}",
t = token.as_str()
);
stream.write_all(req.as_bytes()).unwrap();
let reply = read_reply(&mut stream, &mut Vec::new()).unwrap();
assert_eq!(reply.status, 400);
drop(server);
let _ = std::fs::remove_dir_all(dir);
}
#[test]
fn header_over_512_bytes_431() {
let (server, tokens, _fake, dir) = start(false);
let port = port_from_url(&server.url());
let token = tokens.mint("lane-b", "owner-1").unwrap();
let origin = "x".repeat(513);
let auth = format!("Bearer {}", token.as_str());
let reply = http(
port,
"POST",
"/mcp",
&[
("Content-Type", "application/json"),
("Authorization", &auth),
("Origin", &origin),
],
ping_body().as_bytes(),
);
assert_eq!(reply.status, 431);
drop(server);
let _ = std::fs::remove_dir_all(dir);
}