Test owner approved enrollment sync flow

This commit is contained in:
Eric Wendland 2026-05-22 01:28:45 +02:00
commit 94ad64d0db
3 changed files with 288 additions and 6 deletions

View file

@ -36,6 +36,24 @@ fn spawn_daemon(home: &std::path::Path) -> Child {
.expect("spawn daemon")
}
fn ssh_keygen_available() -> bool {
Command::new("ssh-keygen").arg("-?").output().is_ok()
}
fn generate_ssh_key(path: &std::path::Path) {
let status = Command::new("ssh-keygen")
.arg("-q")
.arg("-t")
.arg("ed25519")
.arg("-N")
.arg("")
.arg("-f")
.arg(path)
.status()
.expect("generate ssh key");
assert!(status.success());
}
fn wait_for_socket(path: &std::path::Path) {
let started = Instant::now();
while started.elapsed() < Duration::from_secs(5) {
@ -197,6 +215,262 @@ fn peer_ping_uses_daemon_owned_iroh_endpoint() {
assert!(stdout.contains("candidate status does not grant resource capabilities"));
}
#[test]
fn sync_now_completes_owner_approved_node_enrollment_flow() {
if !ssh_keygen_available() {
return;
}
let owner_home = tempfile::tempdir().expect("owner tempdir");
let requester_home = tempfile::tempdir().expect("requester tempdir");
if !unix_sockets_available(owner_home.path()) || !unix_sockets_available(requester_home.path())
{
return;
}
let owner_key = owner_home.path().join("owner_ed25519");
generate_ssh_key(&owner_key);
let owner_pub = owner_key.with_extension("pub");
let owner_pub_arg = owner_pub.to_str().expect("owner pub path");
let owner_key_arg = owner_key.to_str().expect("owner key path");
let owner_init = run_geth(
owner_home.path(),
&[
"init",
"--admin-key",
owner_pub_arg,
"--signing-key",
owner_key_arg,
"--owner",
"Eric",
"--node-name",
"owner-laptop",
],
);
assert!(
owner_init.status.success(),
"owner init stderr: {}",
String::from_utf8_lossy(&owner_init.stderr)
);
let requester_init = run_geth(requester_home.path(), &["init"]);
assert!(
requester_init.status.success(),
"requester init stderr: {}",
String::from_utf8_lossy(&requester_init.stderr)
);
for home in [owner_home.path(), requester_home.path()] {
std::fs::write(
home.join("config.toml"),
"[iroh]\nrelay_mode = \"disabled\"\nlocal_discovery = false\n",
)
.expect("write config");
}
let mut owner_daemon = spawn_daemon(owner_home.path());
let mut requester_daemon = spawn_daemon(requester_home.path());
wait_for_socket(&owner_home.path().join("run/geth.sock"));
wait_for_socket(&requester_home.path().join("run/geth.sock"));
let requester_trust = run_geth(
requester_home.path(),
&["keychain", "init", "--admin-key", owner_pub_arg],
);
assert!(
requester_trust.status.success(),
"requester trust stderr: {}",
String::from_utf8_lossy(&requester_trust.stderr)
);
let owner_card_path = owner_home.path().join("owner-peer-card.json");
let requester_card_path = requester_home.path().join("requester-peer-card.json");
let owner_export = run_geth(
owner_home.path(),
&[
"peer",
"export",
"--out",
owner_card_path.to_str().expect("owner card path"),
],
);
let requester_export = run_geth(
requester_home.path(),
&[
"peer",
"export",
"--out",
requester_card_path.to_str().expect("requester card path"),
],
);
assert!(
owner_export.status.success(),
"owner export stderr: {}",
String::from_utf8_lossy(&owner_export.stderr)
);
assert!(
requester_export.status.success(),
"requester export stderr: {}",
String::from_utf8_lossy(&requester_export.stderr)
);
let owner_card_json = std::fs::read_to_string(&owner_card_path).expect("read owner card");
let owner_card: geth_discovery::PeerCard =
serde_json::from_str(&owner_card_json).expect("decode owner card");
let requester_card_json =
std::fs::read_to_string(&requester_card_path).expect("read requester card");
let requester_card: geth_discovery::PeerCard =
serde_json::from_str(&requester_card_json).expect("decode requester card");
let import_owner = run_geth(
requester_home.path(),
&[
"peer",
"import",
owner_card_path.to_str().expect("owner card path"),
],
);
let import_requester = run_geth(
owner_home.path(),
&[
"peer",
"import",
requester_card_path.to_str().expect("requester card path"),
],
);
assert!(
import_owner.status.success(),
"requester import owner stderr: {}",
String::from_utf8_lossy(&import_owner.stderr)
);
assert!(
import_requester.status.success(),
"owner import requester stderr: {}",
String::from_utf8_lossy(&import_requester.stderr)
);
let enroll_request = run_geth(
requester_home.path(),
&[
"node",
"enroll",
"request",
"--node-name",
"workstation",
"--capability",
"resource:ssh-proxy:local=ssh_proxy.connect",
],
);
assert!(
enroll_request.status.success(),
"enroll request stderr: {}",
String::from_utf8_lossy(&enroll_request.stderr)
);
let enroll_submit = run_geth(
requester_home.path(),
&["node", "enroll", "submit", owner_card.node_id.as_str()],
);
assert!(
enroll_submit.status.success(),
"enroll submit stderr: {}",
String::from_utf8_lossy(&enroll_submit.stderr)
);
assert!(String::from_utf8_lossy(&enroll_submit.stdout).contains("accepted: true"));
let enroll_list = run_geth(owner_home.path(), &["node", "enroll", "list"]);
assert!(
enroll_list.status.success(),
"enroll list stderr: {}",
String::from_utf8_lossy(&enroll_list.stderr)
);
let enroll_list_stdout = String::from_utf8_lossy(&enroll_list.stdout);
let request_id = enroll_list_stdout
.lines()
.find_map(|line| {
let mut fields = line.split('\t');
let id = fields.next()?;
if id.starts_with("node-enrollment:") {
Some(id.to_owned())
} else {
None
}
})
.expect("request id in enrollment list");
let approve = run_geth(
owner_home.path(),
&[
"node",
"enroll",
"approve",
&request_id,
"--signing-key",
owner_key_arg,
"--capability",
"resource:cas:local=cas.fetch",
],
);
assert!(
approve.status.success(),
"approve stderr: {}",
String::from_utf8_lossy(&approve.stderr)
);
let sync = run_geth(
requester_home.path(),
&["sync", "now", owner_card.node_id.as_str()],
);
assert!(
sync.status.success(),
"sync now stderr: {}",
String::from_utf8_lossy(&sync.stderr)
);
let sync_stdout = String::from_utf8_lossy(&sync.stdout);
assert!(sync_stdout.contains("keychain\tok"));
assert!(sync_stdout.contains("auth\tok"));
let requester_nodes = run_geth(requester_home.path(), &["node", "list"]);
assert!(
requester_nodes.status.success(),
"requester node list stderr: {}",
String::from_utf8_lossy(&requester_nodes.stderr)
);
let requester_nodes_stdout = String::from_utf8_lossy(&requester_nodes.stdout);
assert!(requester_nodes_stdout.contains("owner-laptop"));
assert!(requester_nodes_stdout.contains("workstation"));
let explain = run_geth(
requester_home.path(),
&[
"auth",
"explain",
requester_card.node_id.as_str(),
"resource:ssh-proxy:local",
"ssh_proxy.connect",
],
);
assert!(
explain.status.success(),
"auth explain stderr: {}",
String::from_utf8_lossy(&explain.stderr)
);
assert!(String::from_utf8_lossy(&explain.stdout).contains("allowed: true"));
let sync_status = run_geth(requester_home.path(), &["sync", "status"]);
let _ = owner_daemon.kill();
let _ = requester_daemon.kill();
let _ = owner_daemon.wait();
let _ = requester_daemon.wait();
assert!(
sync_status.status.success(),
"sync status stderr: {}",
String::from_utf8_lossy(&sync_status.stderr)
);
let sync_status_stdout = String::from_utf8_lossy(&sync_status.stdout);
assert!(sync_status_stdout.contains("peer:"));
assert!(sync_status_stdout.contains("keychain"));
assert!(sync_status_stdout.contains("auth"));
}
#[test]
fn peer_card_export_import_and_list_are_candidate_only() {
let source_home = tempfile::tempdir().expect("source tempdir");