#![cfg(all(
feature = "streamable-http",
feature = "http-client",
not(target_arch = "wasm32")
))]
use std::collections::HashMap;
use std::net::{Ipv4Addr, SocketAddr};
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use async_trait::async_trait;
use pmcp::types::protocol::error_codes::{HEADER_MISMATCH, UNSUPPORTED_PROTOCOL_VERSION};
use pmcp::server::auth::{AuthContext, AuthProvider};
use pmcp::server::http_middleware::{
ServerHttpContext, ServerHttpMiddleware, ServerHttpMiddlewareChain, ServerHttpResponse,
};
use pmcp::server::streamable_http_server::{StreamableHttpServer, StreamableHttpServerConfig};
use pmcp::server::{PromptHandler, ResourceHandler, Server};
use pmcp::testing::META_SERVER_INFO;
use pmcp::types::prompts::GetPromptRequest;
use pmcp::types::protocol::{ProtocolVersion, PROTOCOL_VERSION_2026_07_28 as V2};
use pmcp::types::resources::ReadResourceRequest;
use pmcp::types::{
CallToolRequest, Content, GetPromptResult, ListResourcesResult, ReadResourceResult, RequestMeta,
};
use pmcp::ServerCapabilities;
use pmcp::{RequestHandlerExtra, ToolHandler};
use tokio::sync::Mutex;
struct SearchTool;
#[async_trait]
impl ToolHandler for SearchTool {
async fn handle(
&self,
_args: serde_json::Value,
_extra: RequestHandlerExtra,
) -> pmcp::Result<serde_json::Value> {
Ok(serde_json::json!({ "answer": "ok" }))
}
}
struct GreetingPrompt;
#[async_trait]
impl PromptHandler for GreetingPrompt {
async fn handle(
&self,
_args: HashMap<String, String>,
_extra: RequestHandlerExtra,
) -> pmcp::Result<GetPromptResult> {
Ok(GetPromptResult::new(vec![], Some("greeting".to_string())))
}
}
struct GreetingResource;
#[async_trait]
impl ResourceHandler for GreetingResource {
async fn read(
&self,
uri: &str,
_extra: RequestHandlerExtra,
) -> pmcp::Result<ReadResourceResult> {
Ok(ReadResourceResult::new(vec![Content::resource_with_text(
uri.to_string(),
"hello".to_string(),
"text/plain".to_string(),
)]))
}
async fn list(
&self,
_cursor: Option<String>,
_extra: RequestHandlerExtra,
) -> pmcp::Result<ListResourcesResult> {
Ok(ListResourcesResult::new(vec![]))
}
}
const DISCOVER_EXTENSION_KEY: &str = "io.example/experimental";
fn extensions_capabilities() -> ServerCapabilities {
let mut caps = ServerCapabilities::default();
let mut ext = HashMap::new();
ext.insert(
DISCOVER_EXTENSION_KEY.to_string(),
serde_json::json!({ "enabled": true }),
);
caps.extensions = Some(ext);
caps
}
fn build_server(opt_in_v2: bool) -> Server {
let mut builder = Server::builder()
.name("v2-required-headers")
.version("1.0.0");
if opt_in_v2 {
builder = builder
.capabilities(extensions_capabilities())
.with_supported_protocol_versions([
ProtocolVersion("2025-11-25".to_string()),
ProtocolVersion(V2.to_string()),
]);
}
builder
.tool("search", SearchTool)
.prompt("greeting", GreetingPrompt)
.resources(GreetingResource)
.build()
.expect("server builds")
}
fn discover_body(meta_version: Option<&str>) -> String {
let mut params = serde_json::Map::new();
if let Some(v) = meta_version {
params.insert(
"_meta".to_string(),
serde_json::json!({ "io.modelcontextprotocol/protocolVersion": v }),
);
}
serde_json::json!({
"jsonrpc": "2.0",
"id": 1,
"method": "server/discover",
"params": params,
})
.to_string()
}
struct RejectingAuth;
#[async_trait]
impl AuthProvider for RejectingAuth {
async fn validate_request(
&self,
authorization_header: Option<&str>,
) -> pmcp::Result<Option<AuthContext>> {
match authorization_header {
Some("Bearer good-token") => Ok(None),
_ => Err(pmcp::Error::authentication("missing or invalid token")),
}
}
}
struct RecordingMiddleware {
saw_response: Arc<AtomicBool>,
}
#[async_trait]
impl ServerHttpMiddleware for RecordingMiddleware {
async fn on_response(
&self,
_response: &mut ServerHttpResponse,
_context: &ServerHttpContext,
) -> pmcp::Result<()> {
self.saw_response.store(true, Ordering::SeqCst);
Ok(())
}
}
async fn spawn_with_auth() -> (SocketAddr, tokio::task::JoinHandle<()>) {
let server = Arc::new(Mutex::new(
Server::builder()
.name("v2-required-headers-auth")
.version("1.0.0")
.capabilities(extensions_capabilities())
.with_supported_protocol_versions([
ProtocolVersion("2025-11-25".to_string()),
ProtocolVersion(V2.to_string()),
])
.tool("search", SearchTool)
.auth_provider(RejectingAuth)
.build()
.expect("server builds"),
));
let addr = SocketAddr::new(Ipv4Addr::LOCALHOST.into(), 0);
let http =
StreamableHttpServer::with_config(addr, server, StreamableHttpServerConfig::stateless());
http.start().await.expect("server starts")
}
async fn spawn_with_middleware(
saw_response: Arc<AtomicBool>,
) -> (SocketAddr, tokio::task::JoinHandle<()>) {
let server = Arc::new(Mutex::new(build_server(true)));
let mut chain = ServerHttpMiddlewareChain::new();
chain.add(Arc::new(RecordingMiddleware { saw_response }));
let mut config = StreamableHttpServerConfig::stateless();
config.http_middleware = Some(Arc::new(chain));
let addr = SocketAddr::new(Ipv4Addr::LOCALHOST.into(), 0);
let http = StreamableHttpServer::with_config(addr, server, config);
http.start().await.expect("server starts")
}
async fn spawn(opt_in_v2: bool) -> (SocketAddr, tokio::task::JoinHandle<()>) {
let server = Arc::new(Mutex::new(build_server(opt_in_v2)));
let addr = SocketAddr::new(Ipv4Addr::LOCALHOST.into(), 0);
let http =
StreamableHttpServer::with_config(addr, server, StreamableHttpServerConfig::stateless());
http.start().await.expect("server starts")
}
struct Resp {
status: u16,
mcp_method: Option<String>,
mcp_name: Option<String>,
mcp_version: Option<String>,
body: serde_json::Value,
raw: String,
}
async fn post(addr: SocketAddr, extra: &[(&str, &str)], body: &str) -> Resp {
let client = reqwest::Client::new();
let mut req = client
.post(format!("http://{addr}"))
.header("content-type", "application/json")
.header("accept", "application/json")
.body(body.to_string());
for (k, v) in extra {
req = req.header(*k, *v);
}
let resp = req.send().await.expect("request sent");
let status = resp.status().as_u16();
let hget = |name: &str| {
resp.headers()
.get(name)
.and_then(|v| v.to_str().ok())
.map(str::to_string)
};
let mcp_method = hget("mcp-method");
let mcp_name = hget("mcp-name");
let mcp_version = hget("mcp-protocol-version");
let text = resp.text().await.unwrap_or_default();
let body = serde_json::from_str(&text).unwrap_or(serde_json::Value::Null);
Resp {
status,
mcp_method,
mcp_name,
mcp_version,
body,
raw: text,
}
}
fn call_body(tool: &str, meta_version: Option<&str>) -> String {
let mut req = CallToolRequest::new(tool, serde_json::json!({}));
if let Some(v) = meta_version {
req._meta = Some(RequestMeta::new().with_meta(
"io.modelcontextprotocol/protocolVersion",
serde_json::json!(v),
));
}
let params = serde_json::to_value(&req).expect("params serialize");
serde_json::json!({
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": params,
})
.to_string()
}
fn prompt_body(name: &str, meta_version: Option<&str>) -> String {
let mut req = GetPromptRequest {
name: name.to_string(),
arguments: HashMap::new(),
_meta: None,
};
if let Some(v) = meta_version {
req._meta = Some(RequestMeta::new().with_meta(
"io.modelcontextprotocol/protocolVersion",
serde_json::json!(v),
));
}
let params = serde_json::to_value(&req).expect("params serialize");
serde_json::json!({
"jsonrpc": "2.0",
"id": 1,
"method": "prompts/get",
"params": params,
})
.to_string()
}
fn resource_body(uri: &str, meta_version: Option<&str>) -> String {
let mut req = ReadResourceRequest {
uri: uri.to_string(),
_meta: None,
};
if let Some(v) = meta_version {
req._meta = Some(RequestMeta::new().with_meta(
"io.modelcontextprotocol/protocolVersion",
serde_json::json!(v),
));
}
let params = serde_json::to_value(&req).expect("params serialize");
serde_json::json!({
"jsonrpc": "2.0",
"id": 1,
"method": "resources/read",
"params": params,
})
.to_string()
}
async fn shutdown(handle: tokio::task::JoinHandle<()>) {
handle.abort();
let _ = handle.await;
}
#[tokio::test]
async fn v2_required_headers_accepts_well_formed_v2_and_echoes_headers() {
let (addr, handle) = spawn(true).await;
let r = post(
addr,
&[
("mcp-protocol-version", V2),
("mcp-method", "tools/call"),
("mcp-name", "search"),
],
&call_body("search", Some(V2)),
)
.await;
shutdown(handle).await;
assert_eq!(r.status, 200, "well-formed v2 request should be accepted");
assert!(
r.body.get("result").is_some(),
"expected a result: {}",
r.body
);
assert_eq!(r.mcp_method.as_deref(), Some("tools/call"));
assert_eq!(r.mcp_name.as_deref(), Some("search"));
assert_eq!(r.mcp_version.as_deref(), Some(V2));
}
#[tokio::test]
async fn v2_required_headers_rejects_v2_header_with_non_v2_meta() {
let (addr, handle) = spawn(true).await;
let r = post(
addr,
&[
("mcp-protocol-version", V2),
("mcp-method", "tools/call"),
("mcp-name", "search"),
],
&call_body("search", None), )
.await;
shutdown(handle).await;
assert_eq!(r.status, 400, "header/_meta disagreement must fail closed");
assert_eq!(
r.body["error"]["code"], HEADER_MISMATCH,
"a missing required header or a header/body disagreement is HEADER_MISMATCH"
);
}
#[tokio::test]
async fn v2_required_headers_rejects_v2_meta_without_version_header() {
let (addr, handle) = spawn(true).await;
let r = post(
addr,
&[("mcp-method", "tools/call"), ("mcp-name", "search")],
&call_body("search", Some(V2)),
)
.await;
shutdown(handle).await;
assert_eq!(r.status, 400, "_meta v2 with no version header must reject");
assert_eq!(
r.body["error"]["code"], HEADER_MISMATCH,
"a missing required header or a header/body disagreement is HEADER_MISMATCH"
);
}
#[tokio::test]
async fn v2_required_headers_rejects_missing_mcp_name() {
let (addr, handle) = spawn(true).await;
let r = post(
addr,
&[("mcp-protocol-version", V2), ("mcp-method", "tools/call")],
&call_body("search", Some(V2)),
)
.await;
shutdown(handle).await;
assert_eq!(r.status, 400, "missing Mcp-Name must reject (D-05)");
assert_eq!(
r.body["error"]["code"], HEADER_MISMATCH,
"a missing required header or a header/body disagreement is HEADER_MISMATCH"
);
assert_eq!(r.body["jsonrpc"], "2.0");
}
#[tokio::test]
async fn v2_required_headers_rejects_method_body_mismatch() {
let (addr, handle) = spawn(true).await;
let r = post(
addr,
&[
("mcp-protocol-version", V2),
("mcp-method", "resources/read"), ("mcp-name", "search"),
],
&call_body("search", Some(V2)), )
.await;
shutdown(handle).await;
assert_eq!(
r.status, 400,
"Mcp-Method vs body-method mismatch must reject"
);
assert_eq!(
r.body["error"]["code"], HEADER_MISMATCH,
"a missing required header or a header/body disagreement is HEADER_MISMATCH"
);
}
#[tokio::test]
async fn v2_required_headers_rejects_name_body_mismatch() {
let (addr, handle) = spawn(true).await;
let r = post(
addr,
&[
("mcp-protocol-version", V2),
("mcp-method", "tools/call"),
("mcp-name", "not-search"), ],
&call_body("search", Some(V2)),
)
.await;
shutdown(handle).await;
assert_eq!(
r.status, 400,
"Mcp-Name vs params.name mismatch must reject"
);
assert_eq!(
r.body["error"]["code"], HEADER_MISMATCH,
"a missing required header or a header/body disagreement is HEADER_MISMATCH"
);
}
#[tokio::test]
async fn v2_required_headers_error_response_still_echoes_headers() {
let (addr, handle) = spawn(true).await;
let r = post(
addr,
&[
("mcp-protocol-version", V2),
("mcp-method", "tools/call"),
("mcp-name", "ghost"),
],
&call_body("ghost", Some(V2)), )
.await;
shutdown(handle).await;
assert_eq!(r.status, 200);
assert!(
r.body.get("error").is_some(),
"expected JSON-RPC error: {}",
r.body
);
assert_eq!(r.mcp_method.as_deref(), Some("tools/call"));
assert_eq!(r.mcp_name.as_deref(), Some("ghost"));
assert_eq!(r.mcp_version.as_deref(), Some(V2));
}
#[tokio::test]
async fn v2_required_headers_rejects_unsupported_version() {
let (addr, handle) = spawn(true).await;
let r = post(
addr,
&[("mcp-method", "tools/call"), ("mcp-name", "search")],
&call_body("search", Some("1999-01-01")), )
.await;
shutdown(handle).await;
assert_eq!(r.status, 400, "unsupported version must reject");
assert_eq!(r.body["error"]["code"], UNSUPPORTED_PROTOCOL_VERSION);
assert!(
r.body["error"]["data"]["supported"].is_array(),
"-32022 MUST carry an error.data.supported ARRAY: {}",
r.raw
);
}
#[tokio::test]
async fn v2_required_headers_v1_request_on_opted_in_server_untouched() {
let (addr, handle) = spawn(true).await;
let r = post(addr, &[], &call_body("search", None)).await;
shutdown(handle).await;
assert_eq!(r.status, 200, "plain v1 tools/call must still work");
assert!(
r.body.get("result").is_some(),
"expected a result: {}",
r.body
);
assert_eq!(r.mcp_method, None);
assert_eq!(r.mcp_name, None);
}
#[tokio::test]
async fn v2_required_headers_non_opted_in_server_ignores_v2_headers() {
let (addr, handle) = spawn(false).await; let r = post(
addr,
&[("mcp-method", "tools/call"), ("mcp-name", "search")],
&call_body("search", None),
)
.await;
shutdown(handle).await;
assert_eq!(
r.status, 200,
"non-opted-in server must not enforce v2 headers"
);
assert!(
r.body.get("result").is_some(),
"expected a result: {}",
r.body
);
}
#[tokio::test]
async fn v2_prompts_get_accepts_and_envelopes() {
let (addr, handle) = spawn(true).await;
let r = post(
addr,
&[
("mcp-protocol-version", V2),
("mcp-method", "prompts/get"),
("mcp-name", "greeting"),
],
&prompt_body("greeting", Some(V2)),
)
.await;
shutdown(handle).await;
assert_eq!(r.status, 200, "well-formed v2 prompts/get must be accepted");
let result = r.body.get("result").expect("expected a result");
assert_eq!(
result.get("resultType").and_then(|v| v.as_str()),
Some("complete"),
"v2 prompts/get result must carry resultType:complete: {}",
r.body
);
assert!(
result["_meta"][META_SERVER_INFO].is_object(),
"v2 prompts/get result must carry _meta[{META_SERVER_INFO}]: {}",
r.body
);
assert!(
result.get("serverInfo").is_none(),
"v2 prompts/get must not carry a top-level serverInfo: {}",
r.body
);
assert_eq!(r.mcp_method.as_deref(), Some("prompts/get"));
assert_eq!(r.mcp_name.as_deref(), Some("greeting"));
assert_eq!(r.mcp_version.as_deref(), Some(V2));
}
#[tokio::test]
async fn v2_resources_read_accepts_and_envelopes() {
let (addr, handle) = spawn(true).await;
let uri = "mem://greeting";
let r = post(
addr,
&[
("mcp-protocol-version", V2),
("mcp-method", "resources/read"),
("mcp-name", uri), ],
&resource_body(uri, Some(V2)),
)
.await;
shutdown(handle).await;
assert_eq!(
r.status, 200,
"standards-shaped v2 resources/read (uri only) must be accepted: {}",
r.body
);
let result = r.body.get("result").expect("expected a result");
assert_eq!(
result.get("resultType").and_then(|v| v.as_str()),
Some("complete"),
"v2 resources/read result must carry resultType:complete: {}",
r.body
);
assert!(
result["_meta"][META_SERVER_INFO].is_object(),
"v2 resources/read result must carry _meta[{META_SERVER_INFO}]: {}",
r.body
);
assert!(
result.get("serverInfo").is_none(),
"v2 resources/read must not carry a top-level serverInfo: {}",
r.body
);
assert_eq!(r.mcp_method.as_deref(), Some("resources/read"));
assert_eq!(r.mcp_name.as_deref(), Some(uri));
assert_eq!(r.mcp_version.as_deref(), Some(V2));
}
#[tokio::test]
async fn v2_prompts_get_rejects_v2_header_with_non_v2_meta() {
let (addr, handle) = spawn(true).await;
let r = post(
addr,
&[
("mcp-protocol-version", V2),
("mcp-method", "prompts/get"),
("mcp-name", "greeting"),
],
&prompt_body("greeting", None), )
.await;
shutdown(handle).await;
assert_eq!(
r.status, 400,
"v2-header prompts/get with non-v2 _meta must fail closed"
);
assert_eq!(
r.body["error"]["code"], HEADER_MISMATCH,
"a missing required header or a header/body disagreement is HEADER_MISMATCH"
);
}
fn assert_v1_byte_identical(raw: &str, expected_result: &serde_json::Value) {
let parsed: serde_json::Value =
serde_json::from_str(raw).expect("v1 response must be valid JSON");
let expected = serde_json::json!({
"jsonrpc": "2.0",
"id": 1,
"result": expected_result,
});
assert_eq!(
parsed, expected,
"v1 wire must be structurally identical to the golden fixture; got raw: {raw}"
);
assert!(
!raw.contains("resultType"),
"v1 raw must not contain resultType: {raw}"
);
assert!(
!raw.contains("serverInfo"),
"v1 raw must not contain serverInfo: {raw}"
);
assert!(
!raw.contains("_meta"),
"v1 raw must not contain _meta: {raw}"
);
}
#[tokio::test]
async fn v1_prompts_get_byte_identical() {
let (addr, handle) = spawn(false).await; let r = post(addr, &[], &prompt_body("greeting", None)).await;
shutdown(handle).await;
assert_eq!(r.status, 200, "plain v1 prompts/get must still work");
assert_v1_byte_identical(
&r.raw,
&serde_json::json!({
"description": "greeting",
"messages": [],
}),
);
}
#[tokio::test]
async fn v1_resources_read_byte_identical() {
let (addr, handle) = spawn(false).await; let uri = "mem://greeting";
let r = post(addr, &[], &resource_body(uri, None)).await;
shutdown(handle).await;
assert_eq!(r.status, 200, "plain v1 resources/read must still work");
assert_v1_byte_identical(
&r.raw,
&serde_json::json!({
"contents": [{
"uri": uri,
"text": "hello",
"mimeType": "text/plain",
}],
}),
);
}
fn discover_v2_headers() -> Vec<(&'static str, &'static str)> {
vec![
("mcp-protocol-version", V2),
("mcp-method", "server/discover"),
("mcp-name", "server/discover"),
]
}
#[tokio::test]
async fn server_discover_v2_returns_capability_projection_with_extensions() {
let (addr, handle) = spawn(true).await;
let r = post(addr, &discover_v2_headers(), &discover_body(Some(V2))).await;
shutdown(handle).await;
assert_eq!(
r.status, 200,
"v2 server/discover must be accepted: {}",
r.body
);
let result = r.body.get("result").expect("expected a result");
assert_eq!(
result["capabilities"]["extensions"][DISCOVER_EXTENSION_KEY]["enabled"],
serde_json::json!(true),
"discover projection must carry the registered extension id: {}",
r.body
);
assert!(
result.get("serverInfo").is_some_and(|v| v.is_object()),
"discover result must keep its OWN serverInfo schema field: {}",
r.body
);
assert!(
result["_meta"][META_SERVER_INFO].is_object(),
"discover result must also carry _meta[{META_SERVER_INFO}]: {}",
r.body
);
assert_eq!(
result.get("resultType").and_then(|v| v.as_str()),
Some("complete"),
"discover result must carry resultType:complete: {}",
r.body
);
assert_eq!(
result.get("protocolVersion").and_then(|v| v.as_str()),
Some(V2)
);
assert_eq!(r.body["id"], 1, "the original request id must be preserved");
assert_eq!(r.mcp_version.as_deref(), Some(V2));
assert_eq!(r.mcp_method.as_deref(), Some("server/discover"));
}
#[tokio::test]
async fn server_discover_rejects_v2_meta_without_header() {
let (addr, handle) = spawn(true).await;
let r = post(
addr,
&[
("mcp-method", "server/discover"),
("mcp-name", "server/discover"),
],
&discover_body(Some(V2)),
)
.await;
shutdown(handle).await;
assert_eq!(r.status, 400, "v2 _meta with no version header must reject");
assert_eq!(
r.body["error"]["code"], HEADER_MISMATCH,
"a missing required header or a header/body disagreement is HEADER_MISMATCH"
);
}
#[tokio::test]
async fn server_discover_rejects_header_without_v2_meta() {
let (addr, handle) = spawn(true).await;
let r = post(addr, &discover_v2_headers(), &discover_body(None)).await;
shutdown(handle).await;
assert_eq!(r.status, 400, "v2 header with no v2 _meta must reject");
assert_eq!(
r.body["error"]["code"], HEADER_MISMATCH,
"a missing required header or a header/body disagreement is HEADER_MISMATCH"
);
}
#[tokio::test]
async fn server_discover_rejects_mismatched_mcp_method() {
let (addr, handle) = spawn(true).await;
let r = post(
addr,
&[
("mcp-protocol-version", V2),
("mcp-method", "tools/call"), ("mcp-name", "server/discover"),
],
&discover_body(Some(V2)),
)
.await;
shutdown(handle).await;
assert_eq!(r.status, 400, "mismatched Mcp-Method must reject");
assert_eq!(
r.body["error"]["code"], HEADER_MISMATCH,
"a missing required header or a header/body disagreement is HEADER_MISMATCH"
);
}
#[tokio::test]
async fn server_discover_accepts_missing_mcp_name() {
let (addr, handle) = spawn(true).await;
let r = post(
addr,
&[
("mcp-protocol-version", V2),
("mcp-method", "server/discover"),
],
&discover_body(Some(V2)),
)
.await;
shutdown(handle).await;
assert_eq!(
r.status, 200,
"server/discover carries no routing name, so a missing Mcp-Name must be \
ACCEPTED (Phase 118 D-13)"
);
assert!(
r.body["error"].is_null(),
"a name-less v2 method with no Mcp-Name must reach dispatch, not the gate; got {}",
r.body
);
}
#[tokio::test]
async fn server_discover_v1_returns_method_not_found() {
let (addr, handle) = spawn(true).await;
let r = post(addr, &[], &discover_body(None)).await;
shutdown(handle).await;
assert_eq!(r.status, 200, "v1 discover is -32601 AT HTTP 200 (D-10)");
assert_eq!(r.body["error"]["code"], -32601);
assert_eq!(r.body["id"], 1, "the original request id must be preserved");
}
#[tokio::test]
async fn server_discover_non_opted_in_returns_method_not_found() {
let (addr, handle) = spawn(false).await;
let r = post(addr, &[], &discover_body(None)).await;
shutdown(handle).await;
assert_eq!(r.status, 200);
assert_eq!(r.body["error"]["code"], -32601);
assert_eq!(r.body["id"], 1);
}
#[tokio::test]
async fn server_discover_requires_auth_when_provider_installed() {
let (addr, handle) = spawn_with_auth().await;
let r = post(addr, &discover_v2_headers(), &discover_body(Some(V2))).await;
shutdown(handle).await;
assert_eq!(
r.status, 401,
"unauthenticated server/discover must be rejected 401 (no auth bypass)"
);
}
#[tokio::test]
async fn server_discover_with_valid_token_is_served() {
let (addr, handle) = spawn_with_auth().await;
let mut headers = discover_v2_headers();
headers.push(("authorization", "Bearer good-token"));
let r = post(addr, &headers, &discover_body(Some(V2))).await;
shutdown(handle).await;
assert_eq!(
r.status, 200,
"authenticated v2 discover must be served: {}",
r.body
);
assert!(
r.body.get("result").is_some(),
"expected a result: {}",
r.body
);
}
#[tokio::test]
async fn server_discover_runs_response_middleware() {
let saw = Arc::new(AtomicBool::new(false));
let (addr, handle) = spawn_with_middleware(Arc::clone(&saw)).await;
let r = post(addr, &discover_v2_headers(), &discover_body(Some(V2))).await;
shutdown(handle).await;
assert_eq!(
r.status, 200,
"v2 discover on the middleware path must be served: {}",
r.body
);
assert!(
r.body.get("result").is_some(),
"expected a result: {}",
r.body
);
assert!(
saw.load(Ordering::SeqCst),
"discover response must pass through response middleware (no bypass)"
);
}