use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use std::sync::Arc;
use axum::body::Body;
use axum::extract::ConnectInfo;
use axum::http::{Request, StatusCode};
use serde_json::{Value, json};
use tempfile::TempDir;
use tower::ServiceExt;
use trusty_common::uds::server::{CODE_INVALID_PARAMS, RpcResponse, RpcRouter};
use super::{METHOD, register};
use crate::core::paths::FrameworkPaths;
use crate::daemon::api;
use crate::daemon::state::DaemonState;
fn hermetic() -> (Arc<DaemonState>, TempDir) {
let dir = tempfile::tempdir().expect("temp dir for hermetic DaemonState");
let paths = FrameworkPaths::under(dir.path());
(Arc::new(DaemonState::with_paths(&paths)), dir)
}
async fn http_rpc(state: &Arc<DaemonState>, peer: IpAddr, body: &str) -> (StatusCode, Value) {
let mut request = Request::builder()
.method("POST")
.uri("/rpc")
.header("content-type", "application/json")
.body(Body::from(body.to_owned()))
.expect("build request");
request
.extensions_mut()
.insert(ConnectInfo(SocketAddr::new(peer, 50505)));
let response = api::router(Arc::clone(state))
.oneshot(request)
.await
.expect("the router must answer");
let status = response.status();
let bytes = axum::body::to_bytes(response.into_body(), usize::MAX)
.await
.expect("read the response body");
let value = serde_json::from_slice(&bytes)
.unwrap_or_else(|_| Value::String(String::from_utf8_lossy(&bytes).into_owned()));
(status, value)
}
async fn rpc_frame(router: &RpcRouter, params: Value) -> Value {
let frame = json!({ "jsonrpc": "2.0", "id": 1, "method": METHOD, "params": params });
let response: RpcResponse = router
.dispatch(&serde_json::to_vec(&frame).expect("encode the request frame"))
.await;
serde_json::to_value(response).expect("the response frame must serialise")
}
#[tokio::test]
async fn parity_mcp_dispatch_agrees_across_transports() {
let (state, _dir) = hermetic();
let router = register(RpcRouter::new(), &state);
for envelope in [
json!({"jsonrpc": "2.0", "id": 7, "method": "tools/list"}),
json!({"jsonrpc": "2.0", "id": 8, "method": "ping"}),
json!({"jsonrpc": "2.0", "id": 9, "method": "tools/call",
"params": {"name": "session_list", "arguments": {}}}),
json!({"jsonrpc": "2.0", "id": 10, "method": "tools/call",
"params": {"name": "session_status", "arguments": {}}}),
json!({"jsonrpc": "2.0", "id": 11, "method": "tools/call",
"params": {"name": "no_such_tool", "arguments": {}}}),
json!({"jsonrpc": "2.0", "id": "s", "method": "no/such/method"}),
json!({"jsonrpc": "2.0", "method": "notifications/initialized"}),
] {
let (status, body) = http_rpc(
&state,
IpAddr::V4(Ipv4Addr::LOCALHOST),
&envelope.to_string(),
)
.await;
assert_eq!(status, StatusCode::OK, "{envelope}: {body}");
let frame = rpc_frame(&router, envelope.clone()).await;
assert!(frame.get("error").is_none(), "{envelope}: {frame}");
assert_eq!(
body, frame["result"],
"{envelope} must answer identically over HTTP and the socket"
);
}
}
#[tokio::test]
async fn rpc_mcp_dispatch_refuses_a_params_object_that_is_not_an_mcp_envelope() {
let (state, _dir) = hermetic();
let router = register(RpcRouter::new(), &state);
for bad in [
json!({"jsonrpc": "2.0", "id": 1}),
json!({"method": 5}),
json!(null),
json!("tools/list"),
] {
let (status, _) = http_rpc(&state, IpAddr::V4(Ipv4Addr::LOCALHOST), &bad.to_string()).await;
assert!(status.is_client_error(), "HTTP refuses {bad}: {status}");
let frame = rpc_frame(&router, bad.clone()).await;
assert!(
frame.get("result").is_none(),
"{bad} must not answer: {frame}"
);
assert_eq!(
frame["error"]["code"],
json!(CODE_INVALID_PARAMS),
"{bad}: {frame}"
);
}
}
#[tokio::test]
async fn http_rpc_still_refuses_a_non_loopback_peer() {
let (state, _dir) = hermetic();
let (status, _) = http_rpc(
&state,
IpAddr::V4(Ipv4Addr::new(192, 168, 1, 50)),
r#"{"jsonrpc":"2.0","id":1,"method":"tools/list"}"#,
)
.await;
assert_eq!(status, StatusCode::FORBIDDEN);
}