#![cfg(all(feature = "net", feature = "cortex"))]
use std::net::SocketAddr;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use std::time::Duration;
use bytes::Bytes;
use futures::StreamExt;
use net::adapter::net::behavior::capability::{CapabilityAnnouncement, CapabilitySet};
use net::adapter::net::cortex::{
RequestStream, RpcClientStreamingHandler, RpcDuplexHandler, RpcHandlerError,
RpcInboundDispatcher, RpcInboundEvent, RpcResponsePayload, RpcResponseSink, RpcStatus,
RpcStreamingContext,
};
use net::adapter::net::mesh_rpc::CallOptions;
use net::adapter::net::{
ChannelName, ChannelPublisher, EntityKeypair, MeshNode, MeshNodeConfig, PublishConfig,
SocketBufferConfig,
};
use parking_lot::Mutex as PlMutex;
const TEST_BUFFER_SIZE: usize = 256 * 1024;
const PSK: [u8; 32] = [0x42u8; 32];
fn test_config() -> MeshNodeConfig {
let addr: SocketAddr = "127.0.0.1:0".parse().unwrap();
let mut cfg = MeshNodeConfig::new(addr, PSK)
.with_heartbeat_interval(Duration::from_millis(200))
.with_session_timeout(Duration::from_secs(10))
.with_handshake(3, Duration::from_secs(2));
cfg.socket_buffers = SocketBufferConfig {
send_buffer_size: TEST_BUFFER_SIZE,
recv_buffer_size: TEST_BUFFER_SIZE,
};
cfg
}
async fn build_node() -> Arc<MeshNode> {
Arc::new(
MeshNode::new(EntityKeypair::generate(), test_config())
.await
.expect("MeshNode::new"),
)
}
async fn connect_accept(a: &Arc<MeshNode>, b: &Arc<MeshNode>) {
let a_id = a.node_id();
let b_pub = *b.public_key();
let b_addr = b.local_addr();
let b_clone = b.clone();
let accept = tokio::spawn(async move { b_clone.accept(a_id).await });
a.connect(b_addr, &b_pub, b.node_id())
.await
.expect("connect");
accept.await.expect("accept task").expect("accept");
}
fn arrange_server_denies(server: &Arc<MeshNode>, caller: &Arc<MeshNode>, service: &str) {
let tag = format!("nrpc:{service}");
let permissive = CapabilityAnnouncement::new(
server.node_id(),
server.entity_id().clone(),
50,
CapabilitySet::new().add_tag(&tag),
);
caller.test_inject_capability_announcement(permissive);
let mut restrictive = CapabilityAnnouncement::new(
server.node_id(),
server.entity_id().clone(),
100,
CapabilitySet::new().add_tag(&tag),
);
restrictive.allowed_nodes = vec![0xDEAD_BEEF_BAAD_F00D];
server.test_inject_capability_announcement(restrictive);
}
fn assert_gated(server: &Arc<MeshNode>, service: &str) {
let snap = server.rpc_metrics_snapshot();
let svc = snap
.services
.iter()
.find(|s| s.service == service)
.unwrap_or_else(|| panic!("{service} tracked in registry"));
assert!(
svc.capability_denied_total >= 1,
"{service}: bridge must bump capability_denied_total; got {svc:?}",
);
assert_eq!(
svc.handler_invocations_total, 0,
"{service}: handler must not run on a denied call",
);
}
struct CountingClientStream(Arc<AtomicUsize>);
#[async_trait::async_trait]
impl RpcClientStreamingHandler for CountingClientStream {
async fn call(
&self,
_ctx: RpcStreamingContext,
mut requests: RequestStream,
) -> Result<RpcResponsePayload, RpcHandlerError> {
self.0.fetch_add(1, Ordering::Relaxed);
while requests.next().await.is_some() {}
Ok(RpcResponsePayload {
status: RpcStatus::Ok,
headers: vec![],
body: Bytes::new(),
})
}
}
struct CountingDuplex(Arc<AtomicUsize>);
#[async_trait::async_trait]
impl RpcDuplexHandler for CountingDuplex {
async fn call(
&self,
_ctx: RpcStreamingContext,
mut requests: RequestStream,
responses: RpcResponseSink,
) -> Result<(), RpcHandlerError> {
self.0.fetch_add(1, Ordering::Relaxed);
while let Some(r) = requests.next().await {
responses.send(r.to_vec());
}
Ok(())
}
}
#[tokio::test]
async fn client_streaming_denies_unauthorized_caller() {
let server = build_node().await;
let caller = build_node().await;
connect_accept(&caller, &server).await;
caller.start();
server.start();
let hits = Arc::new(AtomicUsize::new(0));
let _serve = server
.serve_rpc_client_stream("cs", Arc::new(CountingClientStream(hits.clone())))
.expect("serve");
arrange_server_denies(&server, &caller, "cs");
let mut call = caller
.call_client_stream(server.node_id(), "cs", CallOptions::default())
.await
.expect("the call opens; the denial arrives as a terminal frame");
let _ = call.send(Bytes::from_static(b"x")).await;
let outcome = tokio::time::timeout(Duration::from_secs(5), call.finish())
.await
.expect("a denied client-streaming caller must be told, not left hanging");
assert!(
outcome.is_err(),
"the terminal frame must be a denial, not a success: {outcome:?}",
);
assert_eq!(hits.load(Ordering::Relaxed), 0, "handler must not run");
assert_gated(&server, "cs");
}
#[tokio::test]
async fn duplex_denies_unauthorized_caller() {
let server = build_node().await;
let caller = build_node().await;
connect_accept(&caller, &server).await;
caller.start();
server.start();
let hits = Arc::new(AtomicUsize::new(0));
let _serve = server
.serve_rpc_duplex("dx", Arc::new(CountingDuplex(hits.clone())))
.expect("serve");
arrange_server_denies(&server, &caller, "dx");
let mut call = caller
.call_duplex(server.node_id(), "dx", CallOptions::default())
.await
.expect("the call opens; the denial arrives as a terminal frame");
let _ = call.send(Bytes::from_static(b"x")).await;
let terminal = tokio::time::timeout(Duration::from_secs(5), call.next())
.await
.expect("a denied duplex caller must be told, not left hanging");
match terminal {
Some(Err(_)) => {}
other => panic!("expected a terminal denial on the duplex stream, got {other:?}"),
}
assert_eq!(hits.load(Ordering::Relaxed), 0, "handler must not run");
assert_gated(&server, "dx");
}
struct RejectAllHandler;
#[async_trait::async_trait]
impl net::adapter::net::cortex::RpcHandler for RejectAllHandler {
async fn call(
&self,
ctx: net::adapter::net::cortex::RpcContext,
) -> Result<RpcResponsePayload, RpcHandlerError> {
Ok(RpcResponsePayload {
status: RpcStatus::Ok,
headers: vec![],
body: ctx.payload.body,
})
}
}
#[tokio::test]
async fn denial_is_not_fanned_out_to_the_reply_roster() {
let server = build_node().await;
let caller = build_node().await;
let bystander = build_node().await;
connect_accept(&caller, &server).await;
connect_accept(&bystander, &server).await;
caller.start();
bystander.start();
server.start();
let _serve = server
.serve_rpc("svc", Arc::new(RejectAllHandler))
.expect("serve");
arrange_server_denies(&server, &caller, "svc");
let caller_origin = caller.origin_hash();
let reply_channel = ChannelName::new(&format!("svc.replies.{caller_origin:016x}")).unwrap();
let seen: Arc<PlMutex<Vec<RpcInboundEvent>>> = Arc::new(PlMutex::new(Vec::new()));
let seen_disp = seen.clone();
let disp: RpcInboundDispatcher = Arc::new(move |ev| seen_disp.lock().push(ev));
assert!(bystander
.register_rpc_inbound(reply_channel.hash(), disp)
.is_some());
bystander
.subscribe_channel(server.node_id(), reply_channel.clone())
.await
.expect("bystander subscribes to caller's reply channel");
let _pub = ChannelPublisher::new(reply_channel.clone(), PublishConfig::default());
let err = caller
.call(
server.node_id(),
"svc",
Bytes::from_static(b"x"),
CallOptions {
deadline: Some(std::time::Instant::now() + Duration::from_secs(3)),
..Default::default()
},
)
.await
.expect_err("gate denies");
assert!(
matches!(
err,
net::adapter::net::mesh_rpc::RpcError::CapabilityDenied { .. }
),
"the caller itself must receive the denial, got {err:?}",
);
tokio::time::sleep(Duration::from_millis(200)).await;
assert!(
seen.lock().is_empty(),
"the bystander must NOT receive the denial — it was not fanned out to the roster",
);
}