#![allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)]
use std::net::SocketAddr;
use std::path::PathBuf;
use std::time::Duration;
use x0x::server::{serve, DaemonConfig};
fn hermetic_config(dir: &std::path::Path) -> DaemonConfig {
let mut config = DaemonConfig::default();
config.api_address = SocketAddr::from(([127, 0, 0, 1], 0));
config.bind_address = SocketAddr::from(([127, 0, 0, 1], 0));
config.bootstrap_peers = Some(Vec::new());
config.data_dir = dir.join("data");
config.identity_dir = Some(dir.join("identity"));
config
}
fn free_udp_port() -> u16 {
let sock = std::net::UdpSocket::bind(("127.0.0.1", 0)).expect("bind probe udp socket");
let port = sock.local_addr().expect("probe local_addr").port();
drop(sock);
port
}
#[tokio::test]
#[ignore]
async fn serve_binds_serves_health_and_shuts_down() {
let tmp = tempfile::tempdir().expect("tempdir");
let config = hermetic_config(tmp.path());
let handle = serve(config).await.expect("serve() should start");
let addr = handle.local_addr();
assert_ne!(
addr.port(),
0,
"local_addr() must resolve the ephemeral port"
);
let client = reqwest::Client::builder()
.timeout(Duration::from_secs(10))
.build()
.expect("client");
let resp = client
.get(format!("http://{addr}/health"))
.send()
.await
.expect("GET /health");
assert_eq!(
resp.status(),
reqwest::StatusCode::OK,
"/health should be 200"
);
handle
.shutdown_and_wait()
.await
.expect("shutdown_and_wait should return Ok");
let rebound = tokio::net::TcpListener::bind(addr).await;
assert!(
rebound.is_ok(),
"API port {addr} must be released after shutdown"
);
}
#[tokio::test]
#[ignore]
async fn dropping_handle_requests_shutdown() {
let tmp = tempfile::tempdir().expect("tempdir");
let config = hermetic_config(tmp.path());
let handle = serve(config).await.expect("serve() should start");
let addr = handle.local_addr();
drop(handle);
let mut rebound = false;
for _ in 0..40 {
if tokio::net::TcpListener::bind(addr).await.is_ok() {
rebound = true;
break;
}
tokio::time::sleep(Duration::from_millis(100)).await;
}
assert!(rebound, "dropping the handle must shut the server down");
}
#[tokio::test]
#[ignore]
async fn serve_tears_down_cleanly_and_rebinds() {
let tmp = tempfile::tempdir().expect("tempdir");
let mut config = hermetic_config(tmp.path());
let quic_port = free_udp_port();
config.bind_address = SocketAddr::from(([127, 0, 0, 1], quic_port));
let client = reqwest::Client::builder()
.timeout(Duration::from_secs(10))
.build()
.expect("client");
let handle = serve(config.clone()).await.expect("first serve() starts");
let first_addr = handle.local_addr();
let resp = client
.get(format!("http://{first_addr}/health"))
.send()
.await
.expect("GET /health (run 1)");
assert_eq!(resp.status(), reqwest::StatusCode::OK);
handle
.shutdown_and_wait()
.await
.expect("first shutdown_and_wait returns Ok");
let rebound = tokio::net::TcpListener::bind(first_addr).await;
assert!(
rebound.is_ok(),
"API port {first_addr} must be released after the first shutdown"
);
drop(rebound);
let handle = serve(config)
.await
.expect("second serve() must rebind the SAME fixed QUIC port (ant-quic #196)");
let addr = handle.local_addr();
let resp = client
.get(format!("http://{addr}/health"))
.send()
.await
.expect("GET /health (run 2)");
assert_eq!(
resp.status(),
reqwest::StatusCode::OK,
"second instance must serve /health on the same fixed QUIC port — proves #196 releases the socket"
);
handle
.shutdown_and_wait()
.await
.expect("second shutdown_and_wait returns Ok");
}
#[tokio::test]
#[ignore]
async fn serve_writes_nothing_under_sentinel_home() {
let tmp = tempfile::tempdir().expect("tempdir");
let sentinel_home = tmp.path().join("sentinel-home");
std::fs::create_dir_all(&sentinel_home).expect("create sentinel home");
std::env::set_var("HOME", &sentinel_home);
std::env::set_var("XDG_DATA_HOME", sentinel_home.join("xdg-data"));
std::env::set_var("XDG_CONFIG_HOME", sentinel_home.join("xdg-config"));
std::env::set_var("XDG_CACHE_HOME", sentinel_home.join("xdg-cache"));
let config = hermetic_config(tmp.path());
let handle = serve(config).await.expect("serve() should start");
let addr = handle.local_addr();
let client = reqwest::Client::builder()
.timeout(Duration::from_secs(10))
.build()
.expect("client");
let resp = client
.get(format!("http://{addr}/health"))
.send()
.await
.expect("GET /health");
assert_eq!(resp.status(), reqwest::StatusCode::OK);
handle.shutdown_and_wait().await.expect("clean shutdown");
let offenders = find_x0x_dirs(&sentinel_home);
assert!(
offenders.is_empty(),
"serve() must not write x0x state under the sentinel home; found: {offenders:?}"
);
}
fn find_x0x_dirs(root: &std::path::Path) -> Vec<PathBuf> {
let mut found = Vec::new();
let mut stack = vec![root.to_path_buf()];
while let Some(dir) = stack.pop() {
let Ok(entries) = std::fs::read_dir(&dir) else {
continue;
};
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
if name.starts_with(".x0x") || name == "x0x" {
found.push(path.clone());
}
}
stack.push(path);
}
}
}
found
}
#[tokio::test]
#[ignore]
async fn serve_stop_loop_leaks_no_tasks() {
let tmp = tempfile::tempdir().expect("tempdir");
let config = hermetic_config(tmp.path());
let client = reqwest::Client::builder()
.timeout(Duration::from_secs(10))
.build()
.expect("client");
for cycle in 0..3 {
let handle = serve(config.clone())
.await
.unwrap_or_else(|e| panic!("serve() must start on cycle {cycle}: {e:?}"));
let addr = handle.local_addr();
assert_ne!(
addr.port(),
0,
"cycle {cycle}: ephemeral API port must resolve"
);
let resp = client
.get(format!("http://{addr}/health"))
.send()
.await
.unwrap_or_else(|e| panic!("GET /health on cycle {cycle}: {e}"));
assert_eq!(
resp.status(),
reqwest::StatusCode::OK,
"cycle {cycle}: /health must be 200"
);
handle
.shutdown_and_wait()
.await
.unwrap_or_else(|e| panic!("shutdown_and_wait must return Ok on cycle {cycle}: {e}"));
let rebound = tokio::net::TcpListener::bind(addr).await;
assert!(
rebound.is_ok(),
"cycle {cycle}: API port {addr} must be released after shutdown"
);
}
}
#[tokio::test]
#[ignore]
async fn serve_then_immediate_shutdown_is_ok_and_prompt() {
let tmp = tempfile::tempdir().expect("tempdir");
let mut config = hermetic_config(tmp.path());
config.bootstrap_peers = Some(vec![SocketAddr::from(([192, 0, 2, 1], 5483))]);
let handle = serve(config).await.expect("serve() should start");
let addr = handle.local_addr();
tokio::time::sleep(Duration::from_millis(50)).await;
let started = std::time::Instant::now();
handle
.shutdown_and_wait()
.await
.expect("immediate shutdown_and_wait should return Ok");
let elapsed = started.elapsed();
assert!(
elapsed < Duration::from_secs(15),
"immediate shutdown must return promptly, took {elapsed:?}"
);
let rebound = tokio::net::TcpListener::bind(addr).await;
assert!(
rebound.is_ok(),
"API port {addr} must be released after an immediate shutdown"
);
}