#![cfg(all(feature = "net", feature = "nat-traversal"))]
use std::net::SocketAddr;
use std::sync::Arc;
use std::time::Duration;
use net::adapter::net::behavior::capability::{CapabilityFilter, CapabilitySet};
use net::adapter::net::traversal::classify::NatClass;
use net::adapter::net::{EntityKeypair, MeshNode, MeshNodeConfig, SocketBufferConfig};
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(5))
.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> {
let cfg = test_config();
let keypair = EntityKeypair::generate();
Arc::new(MeshNode::new(keypair, cfg).await.expect("MeshNode::new"))
}
async fn three_node_star() -> (Arc<MeshNode>, Arc<MeshNode>, Arc<MeshNode>) {
let a = build_node().await;
let b = build_node().await;
let c = build_node().await;
{
let a_id = a.node_id();
let b_pub = *b.public_key();
let b_addr = b.local_addr();
let b_id = b.node_id();
let b_clone = b.clone();
let accept = tokio::spawn(async move { b_clone.accept(a_id).await });
a.connect(b_addr, &b_pub, b_id)
.await
.expect("connect A→B failed");
accept
.await
.expect("accept B panicked")
.expect("accept B failed");
}
{
let a_id = a.node_id();
let c_pub = *c.public_key();
let c_addr = c.local_addr();
let c_id = c.node_id();
let c_clone = c.clone();
let accept = tokio::spawn(async move { c_clone.accept(a_id).await });
a.connect(c_addr, &c_pub, c_id)
.await
.expect("connect A→C failed");
accept
.await
.expect("accept C panicked")
.expect("accept C failed");
}
a.start();
b.start();
c.start();
(a, b, c)
}
async fn two_node_pair() -> (Arc<MeshNode>, Arc<MeshNode>) {
let a = build_node().await;
let b = build_node().await;
let a_id = a.node_id();
let b_pub = *b.public_key();
let b_addr = b.local_addr();
let b_id = b.node_id();
let b_clone = b.clone();
let accept = tokio::spawn(async move { b_clone.accept(a_id).await });
a.connect(b_addr, &b_pub, b_id)
.await
.expect("connect A→B failed");
accept
.await
.expect("accept B panicked")
.expect("accept B failed");
a.start();
b.start();
(a, b)
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn reclassify_on_localhost_is_open() {
let (a, _b, _c) = three_node_star().await;
assert_eq!(
a.nat_class(),
NatClass::Unknown,
"pre-sweep classification should be Unknown",
);
assert!(
a.reflex_addr().is_none(),
"reflex_addr should be None before the first sweep",
);
a.reclassify_nat().await;
assert_eq!(
a.nat_class(),
NatClass::Open,
"localhost loopback: reflex equals bind → Open",
);
assert_eq!(
a.reflex_addr(),
Some(a.local_addr()),
"reflex_addr should equal the bind addr on localhost",
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn nat_tag_propagates_through_capability_broadcast() {
let (a, b, _c) = three_node_star().await;
a.reclassify_nat().await;
assert_eq!(a.nat_class(), NatClass::Open);
a.announce_capabilities(CapabilitySet::new())
.await
.expect("announce_capabilities");
let filter = CapabilityFilter::new().require_tag("nat:open");
let mut found = false;
for _ in 0..30 {
let peers = b.find_nodes_by_filter(&filter);
if peers.contains(&a.node_id()) {
found = true;
break;
}
tokio::time::sleep(Duration::from_millis(100)).await;
}
assert!(
found,
"B should see A's `nat:open` tag within 3s; got peers: {:?}",
b.find_nodes_by_filter(&filter),
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn reclassify_with_single_peer_stays_unknown() {
let (a, _b) = two_node_pair().await;
a.reclassify_nat().await;
assert_eq!(
a.nat_class(),
NatClass::Unknown,
"one peer is insufficient for classification",
);
assert!(
a.reflex_addr().is_none(),
"reflex_addr should stay None when classification didn't run",
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn background_classify_loop_seeds_state() {
let (a, _b, _c) = three_node_star().await;
let handle = a.spawn_nat_classify_loop();
let mut classified = false;
for _ in 0..30 {
if a.nat_class() == NatClass::Open {
classified = true;
break;
}
tokio::time::sleep(Duration::from_millis(100)).await;
}
assert!(
classified,
"background loop should classify within 3s; got {:?}",
a.nat_class(),
);
handle.abort();
}