use std::sync::Arc;
use assert_cmd::Command as AssertCommand;
use tempfile::TempDir;
use net_sdk::dataforts::{MeshBlobAdapter, Redex};
use net_sdk::transport;
use net_sdk::{Mesh, MeshBuilder};
const PSK_HEX: &str = "4242424242424242424242424242424242424242424242424242424242424242";
fn psk() -> [u8; 32] {
hex::decode(PSK_HEX)
.expect("psk hex")
.try_into()
.expect("32-byte psk")
}
async fn boot_holder_with(
policy: transport::TransferAdminPolicy,
) -> (Mesh, transport::ServeHandle) {
let mesh = MeshBuilder::new("127.0.0.1:0", &psk())
.expect("mesh builder")
.build()
.await
.expect("mesh build");
let adapter = Arc::new(MeshBlobAdapter::new("holder", Arc::new(Redex::new())));
let serve = transport::serve_blob_transfer_rpc_with_policy(&mesh, adapter, policy)
.expect("serve transfers rpc");
mesh.start();
(mesh, serve)
}
async fn boot_holder() -> (Mesh, transport::ServeHandle) {
boot_holder_with(transport::TransferAdminPolicy::AnyAdmittedPeer).await
}
fn cli_cmd(home_dir: &TempDir) -> AssertCommand {
let mut cmd = AssertCommand::cargo_bin("net-mesh").expect("cargo_bin");
cmd.env("HOME", home_dir.path())
.env("XDG_CONFIG_HOME", home_dir.path())
.env("USERPROFILE", home_dir.path());
cmd
}
async fn run_transfer(home: &TempDir, args: Vec<String>) -> (i32, String, String) {
let bin = cli_cmd(home);
tokio::task::spawn_blocking(move || {
let mut cmd = bin;
cmd.arg("transfer");
cmd.args(&args);
let output = cmd.output().expect("invoke net-mesh");
(
output.status.code().unwrap_or(-1),
String::from_utf8_lossy(&output.stdout).into_owned(),
String::from_utf8_lossy(&output.stderr).into_owned(),
)
})
.await
.expect("spawn_blocking")
}
fn attach(holder: &Mesh) -> Vec<String> {
vec![
"--node-addr".into(),
holder.local_addr().to_string(),
"--node-pubkey".into(),
hex::encode(holder.public_key()),
"--node-id".into(),
holder.node_id().to_string(),
"--psk-hex".into(),
PSK_HEX.into(),
]
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn ls_status_cancel_round_trip_over_rpc() {
let (holder, _serve) = boot_holder().await;
let home = TempDir::new().expect("home");
let mut args = vec!["ls".into(), "--output".into(), "json".into()];
args.extend(attach(&holder));
let (code, stdout, stderr) = run_transfer(&home, args).await;
assert_eq!(code, 0, "ls failed: stderr={stderr}\nstdout={stdout}");
let parsed: serde_json::Value =
serde_json::from_str(&stdout).unwrap_or_else(|e| panic!("non-JSON stdout ({e}): {stdout}"));
assert_eq!(parsed["transfer_count"], 0, "stdout={stdout}");
assert!(parsed["transfers"]
.as_array()
.expect("transfers array")
.is_empty());
let mut args = vec![
"status".into(),
"0x42".into(),
"--output".into(),
"json".into(),
];
args.extend(attach(&holder));
let (code, stdout, stderr) = run_transfer(&home, args).await;
assert_eq!(code, 0, "status failed: stderr={stderr}");
let parsed: serde_json::Value = serde_json::from_str(&stdout).expect("parse status");
assert_eq!(parsed["transfer_id"], 0x42);
assert_eq!(parsed["found"], false, "stdout={stdout}");
let mut args = vec![
"cancel".into(),
"0x42".into(),
"--output".into(),
"json".into(),
];
args.extend(attach(&holder));
let (code, stdout, stderr) = run_transfer(&home, args).await;
assert_eq!(code, 0, "cancel failed: stderr={stderr}");
let parsed: serde_json::Value = serde_json::from_str(&stdout).expect("parse cancel");
assert_eq!(parsed["cancelled"], false, "stdout={stdout}");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn closed_policy_refuses_remote_administration() {
let (holder, _serve) = boot_holder_with(transport::TransferAdminPolicy::Closed).await;
let home = TempDir::new().expect("home");
for verb in [
vec!["ls".to_string()],
vec!["status".to_string(), "0x42".to_string()],
vec!["cancel".to_string(), "0x42".to_string()],
] {
let label = verb.join(" ");
let mut args = verb;
args.extend(["--output".to_string(), "json".to_string()]);
args.extend(attach(&holder));
let (code, stdout, stderr) = run_transfer(&home, args).await;
assert_ne!(
code, 0,
"`transfer {label}` succeeded against a Closed holder — \
stdout={stdout} stderr={stderr}"
);
let combined = format!("{stdout}{stderr}");
assert!(
combined.contains("not authorized"),
"`transfer {label}` failed without saying why; an operator who \
forgot to name themselves needs to be able to tell this from a \
network fault. output={combined}"
);
}
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn ls_without_attach_exits_invalid_args() {
let home = TempDir::new().expect("home");
let (code, _stdout, _stderr) =
run_transfer(&home, vec!["ls".into(), "--output".into(), "json".into()]).await;
assert_eq!(
code, 2,
"expected InvalidArgs exit code for ls without attach"
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn status_rejects_non_numeric_id() {
let home = TempDir::new().expect("home");
let (code, _stdout, _stderr) =
run_transfer(&home, vec!["status".into(), "not-an-id".into()]).await;
assert_eq!(
code, 2,
"expected InvalidArgs exit code for a bad transfer-id"
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn a_named_operator_identity_is_admitted_by_a_node_id_allowlist() {
let home = TempDir::new().expect("home");
let id_path = home.path().join("operator.toml");
let mut cmd = cli_cmd(&home);
cmd.args([
"identity",
"generate",
"--out",
id_path.to_str().expect("utf-8 path"),
]);
let out = cmd.output().expect("identity generate");
assert!(
out.status.success(),
"identity generate failed: {}",
String::from_utf8_lossy(&out.stderr)
);
let mut cmd = cli_cmd(&home);
cmd.args([
"identity",
"show",
id_path.to_str().expect("utf-8 path"),
"--output",
"json",
]);
let out = cmd.output().expect("identity show");
assert!(
out.status.success(),
"identity show failed: {}",
String::from_utf8_lossy(&out.stderr)
);
let shown: serde_json::Value =
serde_json::from_slice(&out.stdout).expect("identity show emits JSON");
let node_id_hex = shown["node_id_hex"]
.as_str()
.expect(
"identity show must publish node_id_hex — without it an operator \
cannot populate an allowlist without reimplementing the derivation",
)
.to_string();
let node_id = u64::from_str_radix(node_id_hex.trim_start_matches("0x"), 16)
.expect("node_id_hex parses as hex");
let (holder, _serve) =
boot_holder_with(transport::TransferAdminPolicy::operators([node_id])).await;
let mut args = vec!["ls".to_string(), "--output".to_string(), "json".to_string()];
args.extend([
"--identity".to_string(),
id_path.to_str().expect("utf-8 path").to_string(),
]);
args.extend(attach(&holder));
let (code, stdout, stderr) = run_transfer(&home, args).await;
assert_eq!(
code, 0,
"the named operator identity was refused by its own allowlist — \
the derived node_id and the attached mesh's node_id disagree. \
stdout={stdout} stderr={stderr}"
);
let parsed: serde_json::Value =
serde_json::from_str(&stdout).unwrap_or_else(|e| panic!("non-JSON stdout ({e}): {stdout}"));
assert_eq!(parsed["transfer_count"], 0);
let mut args = vec!["ls".to_string(), "--output".to_string(), "json".to_string()];
args.extend(attach(&holder));
let (code, stdout, stderr) = run_transfer(&home, args).await;
assert_ne!(
code, 0,
"an anonymous attach was admitted by a node-id allowlist. stdout={stdout} stderr={stderr}"
);
}