use std::sync::Arc;
use std::sync::Mutex as StdMutex;
use std::time::Duration;
use dig_nat::relay::{
backoff_secs, relay_enabled, relay_url_from_env, run_relay_connection_with, Backoff,
RelayState, RelayStatus, MAX_KNOWN_PEERS,
};
use dig_nat::wire::RelayMessage;
use dig_nat::wire::RelayPeerInfo;
use futures_util::{SinkExt, StreamExt};
use tokio::net::TcpListener;
use tokio_tungstenite::tungstenite::Message;
static ENV_LOCK: StdMutex<()> = StdMutex::new(());
#[test]
fn backoff_is_capped_exponential() {
assert_eq!(backoff_secs(0), 5);
assert_eq!(backoff_secs(1), 10);
assert_eq!(backoff_secs(2), 20);
assert_eq!(backoff_secs(3), 40);
assert_eq!(backoff_secs(20), 300, "capped");
assert_eq!(backoff_secs(64), 300, "overflow saturates to cap");
}
#[test]
fn status_transitions_and_snapshot() {
let s = RelayStatus::new();
assert_eq!(s.state(), RelayState::Disconnected);
assert!(!s.is_connected());
s.set_connecting();
assert_eq!(s.state(), RelayState::Connecting);
s.set_connected(7);
assert!(s.is_connected());
let v = s.snapshot_json("wss://relay.dig.net:9450", "pk");
assert_eq!(v["state"], "connected");
assert_eq!(v["connected"], true);
assert_eq!(v["connected_peers"], 7);
assert_eq!(v["reconnect_attempts"], 0);
assert!(v["last_error"].is_null());
s.set_disconnected(Some("read: reset".into()));
let v = s.snapshot_json("e", "p");
assert_eq!(v["state"], "disconnected");
assert_eq!(v["reconnect_attempts"], 1);
assert_eq!(v["last_error"], "read: reset");
}
#[test]
fn disabled_is_distinct_from_disconnected() {
let s = RelayStatus::new();
s.set_disabled();
assert_eq!(s.state(), RelayState::Disabled);
assert_eq!(s.snapshot_json("e", "p")["state"], "disabled");
}
#[test]
fn repeated_disconnects_count_but_stay_disconnected() {
let s = RelayStatus::new();
s.set_connecting();
for i in 1..=5 {
s.set_disconnected(Some(format!("attempt {i}")));
assert_eq!(s.state(), RelayState::Disconnected);
s.set_connecting();
}
assert_eq!(s.reconnect_attempts(), 5);
}
#[test]
fn env_off_opt_out_and_url_resolution() {
let _g = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
std::env::set_var("DIG_RELAY_URL", "off");
assert!(!relay_enabled());
std::env::set_var("DIG_RELAY_URL", " DISABLED ");
assert!(!relay_enabled(), "trimmed, case-insensitive");
assert_eq!(relay_url_from_env(), dig_constants::DIG_RELAY_URL);
std::env::set_var("DIG_RELAY_URL", "ws://example:1234");
assert!(relay_enabled());
assert_eq!(relay_url_from_env(), "ws://example:1234");
std::env::remove_var("DIG_RELAY_URL");
assert!(relay_enabled());
assert_eq!(relay_url_from_env(), dig_constants::DIG_RELAY_URL);
}
#[test]
fn default_endpoint_is_canonical() {
assert_eq!(dig_constants::DIG_RELAY_URL, "wss://relay.dig.net:9450");
}
#[tokio::test]
async fn client_registers_with_loopback_relay() {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
let server = tokio::spawn(async move {
let (tcp, _) = listener.accept().await.unwrap();
let ws = tokio_tungstenite::accept_async(tcp).await.unwrap();
let (mut write, mut read) = ws.split();
if let Some(Ok(Message::Text(t))) = read.next().await {
let msg: RelayMessage = serde_json::from_str(&t).unwrap();
assert!(
matches!(msg, RelayMessage::Register { .. }),
"first frame is Register"
);
let ack = RelayMessage::RegisterAck {
success: true,
message: "registered".into(),
connected_peers: 1,
};
write
.send(Message::Text(serde_json::to_string(&ack).unwrap()))
.await
.unwrap();
}
tokio::time::sleep(Duration::from_millis(200)).await;
});
let status = RelayStatus::new();
let endpoint = format!("ws://{addr}");
let task_status = Arc::clone(&status);
let client = tokio::spawn(async move {
run_relay_connection_with(
endpoint,
"peerhex".into(),
"DIG_MAINNET".into(),
Vec::new(),
task_status,
Backoff {
base_secs: 0,
cap_secs: 0,
},
)
.await;
});
let mut connected = false;
for _ in 0..50 {
if status.is_connected() {
connected = true;
break;
}
tokio::time::sleep(Duration::from_millis(20)).await;
}
assert!(connected, "client reached Connected after RegisterAck");
client.abort();
let _ = server.await;
}
#[tokio::test]
async fn dead_relay_degrades_gracefully_without_crashing() {
let status = RelayStatus::new();
let task_status = Arc::clone(&status);
let client = tokio::spawn(async move {
run_relay_connection_with(
"ws://127.0.0.1:1".into(),
"peerhex".into(),
"DIG_MAINNET".into(),
Vec::new(),
task_status,
Backoff {
base_secs: 0,
cap_secs: 0,
},
)
.await;
});
let mut retried = false;
for _ in 0..300 {
if status.reconnect_attempts() >= 1 {
retried = true;
break;
}
tokio::time::sleep(Duration::from_millis(20)).await;
}
assert!(
retried,
"dead relay → keeps retrying (bounded), never a crash/hang"
);
let v = status.snapshot_json("ws://127.0.0.1:1", "p");
assert!(
v["last_error"].as_str().unwrap_or("").contains("connect"),
"recorded a connect error, got {:?}",
v["last_error"]
);
client.abort();
}
#[tokio::test]
async fn client_handles_frames_and_error_drops_session() {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
let server = tokio::spawn(async move {
let (tcp, _) = listener.accept().await.unwrap();
let ws = tokio_tungstenite::accept_async(tcp).await.unwrap();
let (mut write, mut read) = ws.split();
let _ = read.next().await;
let ack = RelayMessage::RegisterAck {
success: true,
message: "ok".into(),
connected_peers: 2,
};
write
.send(Message::Text(serde_json::to_string(&ack).unwrap()))
.await
.unwrap();
write
.send(Message::Text(
serde_json::to_string(&RelayMessage::Ping { timestamp: 1 }).unwrap(),
))
.await
.unwrap();
let info = dig_nat::wire::RelayPeerInfo::new("other".into(), "DIG_MAINNET".into(), 1);
write
.send(Message::Text(
serde_json::to_string(&RelayMessage::PeerConnected { peer: info }).unwrap(),
))
.await
.unwrap();
tokio::time::sleep(Duration::from_millis(150)).await;
write
.send(Message::Text(
serde_json::to_string(&RelayMessage::Error {
code: 3,
message: "PEER_NOT_FOUND".into(),
})
.unwrap(),
))
.await
.unwrap();
tokio::time::sleep(Duration::from_millis(300)).await;
});
let status = RelayStatus::new();
let task_status = Arc::clone(&status);
let endpoint = format!("ws://{addr}");
let endpoint_for_task = endpoint.clone();
let client = tokio::spawn(async move {
run_relay_connection(
endpoint_for_task,
"peerhex".into(),
"DIG_MAINNET".into(),
Vec::new(),
task_status,
)
.await
});
let mut connected = false;
for _ in 0..150 {
if status.is_connected() {
connected = true;
break;
}
tokio::time::sleep(Duration::from_millis(20)).await;
}
assert!(connected, "reached Connected via RegisterAck");
let mut dropped = false;
for _ in 0..150 {
if status.state() == RelayState::Disconnected && status.reconnect_attempts() >= 1 {
dropped = true;
break;
}
tokio::time::sleep(Duration::from_millis(20)).await;
}
assert!(dropped, "error frame dropped the session to Disconnected");
let v = status.snapshot_json(&endpoint, "peerhex");
assert!(
v["last_error"]
.as_str()
.unwrap_or("")
.contains("relay error 3"),
"recorded the relay error, got {:?}",
v["last_error"]
);
client.abort();
let _ = server.await;
}
use dig_nat::relay::run_relay_connection;
#[tokio::test]
async fn persistent_reservation_discovers_peers_over_live_socket() {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
let registers = Arc::new(std::sync::atomic::AtomicUsize::new(0));
let server_registers = Arc::clone(®isters);
let server = tokio::spawn(async move {
let (tcp, _) = listener.accept().await.unwrap();
let ws = tokio_tungstenite::accept_async(tcp).await.unwrap();
let (mut write, mut read) = ws.split();
while let Some(Ok(msg)) = read.next().await {
let Message::Text(t) = msg else { continue };
let parsed: RelayMessage = serde_json::from_str(&t).unwrap();
match parsed {
RelayMessage::Register { .. } => {
server_registers.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
let ack = RelayMessage::RegisterAck {
success: true,
message: "ok".into(),
connected_peers: 1,
};
write
.send(Message::Text(serde_json::to_string(&ack).unwrap()))
.await
.unwrap();
}
RelayMessage::GetPeers { .. } => {
let peers = RelayMessage::Peers {
peers: vec![RelayPeerInfo::new("peerA".into(), "DIG_MAINNET".into(), 1)],
};
write
.send(Message::Text(serde_json::to_string(&peers).unwrap()))
.await
.unwrap();
let joined = RelayMessage::PeerConnected {
peer: RelayPeerInfo::new("peerB".into(), "DIG_MAINNET".into(), 1),
};
write
.send(Message::Text(serde_json::to_string(&joined).unwrap()))
.await
.unwrap();
}
_ => {}
}
}
});
let status = RelayStatus::new();
let endpoint = format!("ws://{addr}");
let task_status = Arc::clone(&status);
let client = tokio::spawn(async move {
run_relay_connection_with(
endpoint,
"self".into(),
"DIG_MAINNET".into(),
Vec::new(),
task_status,
Backoff {
base_secs: 0,
cap_secs: 0,
},
)
.await;
});
let mut discovered = false;
for _ in 0..100 {
if status.known_peer_count() >= 2 {
discovered = true;
break;
}
tokio::time::sleep(Duration::from_millis(20)).await;
}
assert!(
discovered,
"discovered both peerA (get_peers) and peerB (peer_connected) over the live socket"
);
let ids: Vec<String> = status
.known_peers()
.into_iter()
.map(|p| p.peer_id)
.collect();
assert!(ids.contains(&"peerA".to_string()) && ids.contains(&"peerB".to_string()));
assert_eq!(
registers.load(std::sync::atomic::Ordering::SeqCst),
1,
"registered exactly once (persistent socket, not re-registered per discovery pass)"
);
client.abort();
server.abort();
let _ = server.await;
}
#[tokio::test]
async fn known_peers_set_is_bounded_under_a_peer_connected_flood() {
let flood = MAX_KNOWN_PEERS + 100;
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
let server = tokio::spawn(async move {
let (tcp, _) = listener.accept().await.unwrap();
let ws = tokio_tungstenite::accept_async(tcp).await.unwrap();
let (mut write, mut read) = ws.split();
while let Some(Ok(msg)) = read.next().await {
let Message::Text(t) = msg else { continue };
let parsed: RelayMessage = serde_json::from_str(&t).unwrap();
match parsed {
RelayMessage::Register { .. } => {
let ack = RelayMessage::RegisterAck {
success: true,
message: "ok".into(),
connected_peers: 1,
};
write
.send(Message::Text(serde_json::to_string(&ack).unwrap()))
.await
.unwrap();
}
RelayMessage::GetPeers { .. } => {
for i in 0..flood {
let joined = RelayMessage::PeerConnected {
peer: RelayPeerInfo::new(format!("peer{i}"), "DIG_MAINNET".into(), 1),
};
write
.send(Message::Text(serde_json::to_string(&joined).unwrap()))
.await
.unwrap();
}
}
_ => {}
}
}
});
let status = RelayStatus::new();
let endpoint = format!("ws://{addr}");
let task_status = Arc::clone(&status);
let client = tokio::spawn(async move {
run_relay_connection_with(
endpoint,
"self".into(),
"DIG_MAINNET".into(),
Vec::new(),
task_status,
Backoff {
base_secs: 0,
cap_secs: 0,
},
)
.await;
});
let mut reached_cap = false;
for _ in 0..200 {
let count = status.known_peer_count();
assert!(
count <= MAX_KNOWN_PEERS,
"discovered set grew past the cap: {count} > {MAX_KNOWN_PEERS}"
);
if count == MAX_KNOWN_PEERS {
reached_cap = true;
break;
}
tokio::time::sleep(Duration::from_millis(20)).await;
}
assert!(
reached_cap,
"the flood should fill the discovered set up to MAX_KNOWN_PEERS"
);
tokio::time::sleep(Duration::from_millis(100)).await;
assert_eq!(
status.known_peer_count(),
MAX_KNOWN_PEERS,
"the discovered set stays pinned at the cap under a continuing flood"
);
client.abort();
server.abort();
let _ = server.await;
}