use assert_cmd::prelude::*;
use net_sdk::capabilities::{CapabilityAnnouncement, CapabilityGroupId, CapabilitySubnetId};
use std::path::PathBuf;
use std::process::Command;
use tempfile::TempDir;
fn generate_identity(dir: &TempDir) -> PathBuf {
let path = dir.path().join("operator.toml");
Command::cargo_bin("net-mesh")
.unwrap()
.args(["identity", "generate", "--out"])
.arg(&path)
.args(["--force"])
.assert()
.success();
path
}
#[test]
fn cap_announce_produces_signed_bytes_with_allow_lists() {
let dir = tempfile::tempdir().unwrap();
let key_path = generate_identity(&dir);
let out_path = dir.path().join("announcement.json");
let subnet_hex = "112233445566778899aabbccddeeff00";
let group_hex = "deadbeefcafef00d0011223344556677889900aabbccddeeff00112233445566";
Command::cargo_bin("net-mesh")
.unwrap()
.args(["cap", "announce"])
.arg("--key")
.arg(&key_path)
.args(["--tag", "nrpc:echo"])
.args(["--tag", "dataforts.blob.overflow"])
.args(["--allow-node", "42"])
.args(["--allow-node", "0xDEADBEEF"])
.args(["--allow-subnet", subnet_hex])
.args(["--allow-group", group_hex])
.args(["--version", "7"])
.args(["--ttl-secs", "120"])
.arg("--out")
.arg(&out_path)
.assert()
.success();
let bytes = std::fs::read(&out_path).unwrap();
let ann = CapabilityAnnouncement::from_bytes(&bytes).expect("decode wire bytes");
assert_eq!(ann.version, 7);
assert_eq!(ann.ttl_secs, 120);
assert_eq!(ann.allowed_nodes, vec![42u64, 0xDEAD_BEEFu64]);
assert_eq!(
ann.allowed_subnets,
vec![CapabilitySubnetId::from_tag(&format!("subnet:{subnet_hex}")).unwrap()]
);
assert_eq!(
ann.allowed_groups,
vec![CapabilityGroupId::from_tag(&format!("group:{group_hex}")).unwrap()]
);
ann.verify().expect("signature verifies");
assert!(ann.capabilities.has_tag("nrpc:echo"));
assert!(ann.capabilities.has_tag("dataforts.blob.overflow"));
}
#[test]
fn cap_announce_stdout_emits_signed_json_with_trailing_newline() {
let dir = tempfile::tempdir().unwrap();
let key_path = generate_identity(&dir);
let stdout_bytes = Command::cargo_bin("net-mesh")
.unwrap()
.args(["cap", "announce"])
.arg("--key")
.arg(&key_path)
.args(["--tag", "nrpc:echo"])
.output()
.unwrap()
.stdout;
assert!(
stdout_bytes.ends_with(b"\n"),
"stdout must end with a newline for line-oriented consumers",
);
let json_bytes = &stdout_bytes[..stdout_bytes.len() - 1];
let ann = CapabilityAnnouncement::from_bytes(json_bytes).expect("decode stdout bytes");
ann.verify().expect("stdout-emitted signature must verify");
}
#[test]
fn cap_announce_rejects_malformed_subnet() {
let dir = tempfile::tempdir().unwrap();
let key_path = generate_identity(&dir);
Command::cargo_bin("net-mesh")
.unwrap()
.args(["cap", "announce"])
.arg("--key")
.arg(&key_path)
.args(["--tag", "nrpc:echo"])
.args(["--allow-subnet", "not-hex"])
.assert()
.code(2);
}
#[test]
fn cap_announce_rejects_malformed_group() {
let dir = tempfile::tempdir().unwrap();
let key_path = generate_identity(&dir);
Command::cargo_bin("net-mesh")
.unwrap()
.args(["cap", "announce"])
.arg("--key")
.arg(&key_path)
.args(["--tag", "nrpc:echo"])
.args(["--allow-group", "deadbeef"])
.assert()
.code(2);
}
#[test]
fn cap_announce_rejects_node_id_mismatch_with_signing_key() {
let dir = tempfile::tempdir().unwrap();
let key_path = generate_identity(&dir);
Command::cargo_bin("net-mesh")
.unwrap()
.args(["cap", "announce"])
.arg("--key")
.arg(&key_path)
.args(["--tag", "nrpc:echo"])
.args(["--node-id", "0x1"])
.assert()
.code(2);
}
#[test]
fn cap_announce_accepts_duplicate_tag() {
let dir = tempfile::tempdir().unwrap();
let key_path = generate_identity(&dir);
Command::cargo_bin("net-mesh")
.unwrap()
.args(["cap", "announce"])
.arg("--key")
.arg(&key_path)
.args(["--tag", "nrpc:echo"])
.args(["--tag", "nrpc:echo"])
.assert()
.success();
}
#[test]
fn cap_announce_rejects_reserved_prefix_tag() {
let dir = tempfile::tempdir().unwrap();
let key_path = generate_identity(&dir);
Command::cargo_bin("net-mesh")
.unwrap()
.args(["cap", "announce"])
.arg("--key")
.arg(&key_path)
.args(["--tag", "scope:tenant:foo"])
.assert()
.code(2);
}
#[test]
fn cap_announce_accepts_node_id_matching_signing_key() {
use net_sdk::capabilities::CapabilityAnnouncement;
let dir = tempfile::tempdir().unwrap();
let key_path = generate_identity(&dir);
let baseline_path = dir.path().join("baseline.json");
Command::cargo_bin("net-mesh")
.unwrap()
.args(["cap", "announce"])
.arg("--key")
.arg(&key_path)
.args(["--tag", "nrpc:echo"])
.arg("--out")
.arg(&baseline_path)
.assert()
.success();
let baseline = CapabilityAnnouncement::from_bytes(&std::fs::read(&baseline_path).unwrap())
.expect("decode baseline");
let derived_hex = format!("{:#x}", baseline.node_id);
Command::cargo_bin("net-mesh")
.unwrap()
.args(["cap", "announce"])
.arg("--key")
.arg(&key_path)
.args(["--tag", "nrpc:echo"])
.args(["--node-id", &derived_hex])
.assert()
.success();
}
#[test]
fn cap_announce_normalizes_whitespace_across_allow_list_parsers() {
let dir = tempfile::tempdir().unwrap();
let key_path = generate_identity(&dir);
let out_path = dir.path().join("ann.json");
let node_padded = " 0xCAFEF00D ";
let subnet_padded = " 00112233445566778899aabbccddeeff ";
let group_padded = " ffeeddccbbaa99887766554433221100ffeeddccbbaa99887766554433221100 ";
Command::cargo_bin("net-mesh")
.unwrap()
.args(["cap", "announce"])
.arg("--key")
.arg(&key_path)
.args(["--tag", "nrpc:echo"])
.args(["--allow-node", node_padded])
.args(["--allow-subnet", subnet_padded])
.args(["--allow-group", group_padded])
.arg("--out")
.arg(&out_path)
.assert()
.success();
let bytes = std::fs::read(&out_path).unwrap();
let ann = CapabilityAnnouncement::from_bytes(&bytes).expect("decode wire bytes");
assert_eq!(ann.allowed_nodes, vec![0xCAFE_F00Du64]);
assert_eq!(
ann.allowed_subnets,
vec![CapabilitySubnetId::from_tag(&format!("subnet:{}", subnet_padded.trim())).unwrap()]
);
assert_eq!(
ann.allowed_groups,
vec![CapabilityGroupId::from_tag(&format!("group:{}", group_padded.trim())).unwrap()]
);
}