geth/crates/geth/tests/bootstrap.rs

874 lines
28 KiB
Rust

use std::process::{Child, Command};
use std::time::{Duration, Instant};
fn unix_sockets_available(home: &std::path::Path) -> bool {
let probe = home.join("probe.sock");
match std::os::unix::net::UnixListener::bind(&probe) {
Ok(listener) => {
drop(listener);
let _ = std::fs::remove_file(probe);
true
}
Err(error) => {
eprintln!("skipping daemon socket test; Unix sockets unavailable: {error}");
false
}
}
}
fn geth_bin() -> &'static str {
env!("CARGO_BIN_EXE_geth")
}
fn run_geth(home: &std::path::Path, args: &[&str]) -> std::process::Output {
Command::new(geth_bin())
.env("GETH_HOME", home)
.args(args)
.output()
.expect("run geth")
}
fn spawn_daemon(home: &std::path::Path) -> Child {
Command::new(geth_bin())
.env("GETH_HOME", home)
.args(["daemon", "run"])
.spawn()
.expect("spawn daemon")
}
fn wait_for_socket(path: &std::path::Path) {
let started = Instant::now();
while started.elapsed() < Duration::from_secs(5) {
if path.exists() {
return;
}
std::thread::sleep(Duration::from_millis(50));
}
panic!("socket did not appear: {}", path.display());
}
#[test]
fn geth_init_in_temp_home() {
let home = tempfile::tempdir().expect("tempdir");
let output = run_geth(home.path(), &["init"]);
assert!(
output.status.success(),
"stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
assert!(home.path().join("geth.sqlite").exists());
assert!(home.path().join("identity/agent.ed25519").exists());
assert!(home.path().join("config.toml").exists());
assert!(
std::fs::read_to_string(home.path().join("config.toml"))
.expect("read config")
.contains("relay_mode = \"default\"")
);
assert!(
std::fs::read_to_string(home.path().join("config.toml"))
.expect("read config")
.contains("local_discovery = true")
);
}
#[test]
fn geth_status_against_running_daemon() {
let home = tempfile::tempdir().expect("tempdir");
if !unix_sockets_available(home.path()) {
return;
}
assert!(run_geth(home.path(), &["init"]).status.success());
std::fs::write(
home.path().join("config.toml"),
"[iroh]\nrelay_mode = \"disabled\"\nlocal_discovery = false\n",
)
.expect("write config");
let mut daemon = spawn_daemon(home.path());
wait_for_socket(&home.path().join("run/geth.sock"));
let output = run_geth(home.path(), &["status"]);
let _ = daemon.kill();
let _ = daemon.wait();
assert!(
output.status.success(),
"stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("geth daemon: running"));
assert!(stdout.contains("agent:"));
assert!(stdout.contains("endpoint:"));
assert!(stdout.contains("iroh relay: disabled"));
assert!(stdout.contains("iroh discovery: local-network disabled"));
}
#[test]
fn initialized_node_can_roundtrip_cas_blob() {
let home = tempfile::tempdir().expect("tempdir");
let paths = geth_config::GethPaths::from_home(home.path());
let node = geth_node::init_node(&paths).expect("init node");
assert_eq!(node.paths.home(), home.path());
let cas = geth_cas::LocalCas::new(paths.cas_dir());
let output_path = home.path().join("output.txt");
let added = cas.add_bytes(b"hello geth integration").expect("add blob");
assert!(cas.has(&added.hash).expect("has blob"));
cas.get_to_path(&added.hash, &output_path)
.expect("get blob");
assert_eq!(
std::fs::read(output_path).expect("read output"),
b"hello geth integration"
);
}
#[test]
fn cas_pin_unpin_updates_local_pin_metadata() {
let home = tempfile::tempdir().expect("tempdir");
let paths = geth_config::GethPaths::from_home(home.path());
let node = geth_node::init_node(&paths).expect("init node");
let blob_path = home.path().join("blob.txt");
std::fs::write(&blob_path, b"pinned bytes").expect("write blob");
let response = geth_node::handle_request(
&node,
geth_control::ControlRequest::CasAdd { path: blob_path },
)
.expect("add cas blob");
let hash = match response {
geth_control::ControlResponse::CasAdded { hash, .. } => hash,
other => panic!("unexpected response: {other:?}"),
};
geth_node::handle_request(
&node,
geth_control::ControlRequest::CasPin { hash: hash.clone() },
)
.expect("pin blob");
let response = geth_node::handle_request(&node, geth_control::ControlRequest::CasList)
.expect("list blobs");
match response {
geth_control::ControlResponse::CasList { blobs } => {
assert_eq!(blobs.len(), 1);
assert!(blobs[0].pinned);
}
other => panic!("unexpected response: {other:?}"),
}
geth_node::handle_request(&node, geth_control::ControlRequest::CasUnpin { hash })
.expect("unpin blob");
let response = geth_node::handle_request(&node, geth_control::ControlRequest::CasList)
.expect("list blobs");
match response {
geth_control::ControlResponse::CasList { blobs } => {
assert_eq!(blobs.len(), 1);
assert!(!blobs[0].pinned);
}
other => panic!("unexpected response: {other:?}"),
}
}
#[test]
fn cas_cleanup_removes_unpinned_and_retains_pinned_blobs() {
let home = tempfile::tempdir().expect("tempdir");
let paths = geth_config::GethPaths::from_home(home.path());
let node = geth_node::init_node(&paths).expect("init node");
let pinned_path = home.path().join("pinned.txt");
let unpinned_path = home.path().join("unpinned.txt");
std::fs::write(&pinned_path, b"keep me").expect("write pinned");
std::fs::write(&unpinned_path, b"remove me").expect("write unpinned");
let pinned_hash = match geth_node::handle_request(
&node,
geth_control::ControlRequest::CasAdd { path: pinned_path },
)
.expect("add pinned")
{
geth_control::ControlResponse::CasAdded { hash, .. } => hash,
other => panic!("unexpected response: {other:?}"),
};
let unpinned_hash = match geth_node::handle_request(
&node,
geth_control::ControlRequest::CasAdd {
path: unpinned_path,
},
)
.expect("add unpinned")
{
geth_control::ControlResponse::CasAdded { hash, .. } => hash,
other => panic!("unexpected response: {other:?}"),
};
geth_node::handle_request(
&node,
geth_control::ControlRequest::CasPin {
hash: pinned_hash.clone(),
},
)
.expect("pin blob");
let dry_run = geth_node::handle_request(
&node,
geth_control::ControlRequest::CasCleanup { dry_run: true },
)
.expect("dry-run cleanup");
match dry_run {
geth_control::ControlResponse::CasCleanup {
removed,
retained_pinned,
dry_run,
} => {
assert!(dry_run);
assert_eq!(removed, vec![unpinned_hash.clone()]);
assert_eq!(retained_pinned, vec![pinned_hash.clone()]);
}
other => panic!("unexpected response: {other:?}"),
}
assert!(
geth_cas::LocalCas::new(paths.cas_dir())
.has(&unpinned_hash)
.expect("dry-run keeps unpinned")
);
let cleanup = geth_node::handle_request(
&node,
geth_control::ControlRequest::CasCleanup { dry_run: false },
)
.expect("cleanup");
match cleanup {
geth_control::ControlResponse::CasCleanup {
removed,
retained_pinned,
dry_run,
} => {
assert!(!dry_run);
assert_eq!(removed, vec![unpinned_hash.clone()]);
assert_eq!(retained_pinned, vec![pinned_hash.clone()]);
}
other => panic!("unexpected response: {other:?}"),
}
let cas = geth_cas::LocalCas::new(paths.cas_dir());
assert!(cas.has(&pinned_hash).expect("pinned retained"));
assert!(!cas.has(&unpinned_hash).expect("unpinned removed"));
}
#[test]
fn auth_explain_distinguishes_discovered_peer_candidates() {
let home = tempfile::tempdir().expect("tempdir");
let paths = geth_config::GethPaths::from_home(home.path());
let node = geth_node::init_node(&paths).expect("init node");
let store = geth_store::Store::open(&paths.metadata_db()).expect("open store");
store
.upsert_peer_card(&geth_store::StoredPeerCard {
peer_id: "node:discovered".to_owned(),
card_json: r#"{"node_id":"node:discovered"}"#.to_owned(),
updated_at_ms: 1,
})
.expect("insert peer card");
let response = geth_node::handle_request(
&node,
geth_control::ControlRequest::AuthExplain {
subject: "node:discovered".to_owned(),
resource: "resource:cas:local".to_owned(),
capability: "cas.fetch".to_owned(),
},
)
.expect("auth explain");
match response {
geth_control::ControlResponse::AuthExplain(explanation) => {
assert!(!explanation.allowed);
assert!(explanation.reason.contains("discovered peer candidate"));
}
other => panic!("unexpected response: {other:?}"),
}
}
#[test]
fn auth_grant_revoke_and_explain_use_local_auth_log() {
let home = tempfile::tempdir().expect("tempdir");
let paths = geth_config::GethPaths::from_home(home.path());
let node = geth_node::init_node(&paths).expect("init node");
let response = geth_node::handle_request(
&node,
geth_control::ControlRequest::AuthGrant {
subject: "node:laptop".to_owned(),
resource: "resource:cas:local".to_owned(),
capability: "cas.fetch".to_owned(),
grant_id: Some("grant:test-fetch".to_owned()),
},
)
.expect("grant capability");
match response {
geth_control::ControlResponse::AuthOpRecorded { op } => {
assert_eq!(op.resource.to_string(), "resource:cas:local");
}
other => panic!("unexpected response: {other:?}"),
}
let response = geth_node::handle_request(
&node,
geth_control::ControlRequest::AuthExplain {
subject: "node:laptop".to_owned(),
resource: "resource:cas:local".to_owned(),
capability: "cas.fetch".to_owned(),
},
)
.expect("explain grant");
match response {
geth_control::ControlResponse::AuthExplain(explanation) => {
assert!(explanation.allowed);
assert_eq!(explanation.evaluated_ops, 1);
assert!(explanation.reason.contains("grant:test-fetch"));
}
other => panic!("unexpected response: {other:?}"),
}
geth_node::handle_request(
&node,
geth_control::ControlRequest::AuthRevoke {
resource: "resource:cas:local".to_owned(),
grant_id: "grant:test-fetch".to_owned(),
},
)
.expect("revoke grant");
let response = geth_node::handle_request(
&node,
geth_control::ControlRequest::AuthExplain {
subject: "node:laptop".to_owned(),
resource: "resource:cas:local".to_owned(),
capability: "cas.fetch".to_owned(),
},
)
.expect("explain revoke");
match response {
geth_control::ControlResponse::AuthExplain(explanation) => {
assert!(!explanation.allowed);
assert_eq!(explanation.evaluated_ops, 2);
assert!(explanation.reason.contains("no active"));
}
other => panic!("unexpected response: {other:?}"),
}
}
#[test]
fn keychain_init_and_status_use_local_keychain_log() {
let home = tempfile::tempdir().expect("tempdir");
let paths = geth_config::GethPaths::from_home(home.path());
let node = geth_node::init_node(&paths).expect("init node");
let admin_key_path = home.path().join("admin.pub");
std::fs::write(&admin_key_path, "ssh-ed25519 AAAAADMIN eric@geth\n").expect("write admin key");
let response = geth_node::handle_request(
&node,
geth_control::ControlRequest::KeychainInit {
admin_key_path: Some(admin_key_path),
},
)
.expect("init keychain");
match response {
geth_control::ControlResponse::KeychainInitialized { ops } => {
assert_eq!(ops.len(), 2);
}
other => panic!("unexpected response: {other:?}"),
}
let response = geth_node::handle_request(&node, geth_control::ControlRequest::KeychainStatus)
.expect("keychain status");
match response {
geth_control::ControlResponse::KeychainStatus(status) => {
assert!(status.initialized);
assert_eq!(status.admin_keys, 1);
assert_eq!(status.users, 0);
}
other => panic!("unexpected response: {other:?}"),
}
}
#[test]
fn db_add_and_status_register_local_db_metadata() {
let home = tempfile::tempdir().expect("tempdir");
let paths = geth_config::GethPaths::from_home(home.path());
let node = geth_node::init_node(&paths).expect("init node");
let db_path = home.path().join("notes.sqlite");
let conn = rusqlite::Connection::open(&db_path).expect("open sqlite");
conn.execute(
"CREATE TABLE notes(id INTEGER PRIMARY KEY, body TEXT NOT NULL)",
[],
)
.expect("create notes table");
drop(conn);
let response = geth_node::handle_request(
&node,
geth_control::ControlRequest::DbAdd {
name: "notes".to_owned(),
path: db_path.clone(),
},
)
.expect("add db");
match response {
geth_control::ControlResponse::DbAdded { db } => {
assert_eq!(db.name, "notes");
assert_eq!(db.sync_status, "local-only");
assert!(db.path_exists);
assert!(db.size_bytes.unwrap_or_default() > 0);
assert!(db.schema_metadata.contains("tables=1"));
assert!(db.schema_metadata.contains("schema_hash="));
}
other => panic!("unexpected response: {other:?}"),
}
let response = geth_node::handle_request(
&node,
geth_control::ControlRequest::DbStatus {
name: "notes".to_owned(),
},
)
.expect("db status");
match response {
geth_control::ControlResponse::DbStatus { db } => {
assert_eq!(db.name, "notes");
assert!(db.path.ends_with("notes.sqlite"));
assert!(db.schema_metadata.contains("tables=1"));
assert!(db.schema_metadata.contains("schema_hash="));
}
other => panic!("unexpected response: {other:?}"),
}
assert!(
geth_node::handle_request(
&node,
geth_control::ControlRequest::DbAdd {
name: "../bad".to_owned(),
path: db_path,
},
)
.is_err()
);
assert!(
geth_node::handle_request(
&node,
geth_control::ControlRequest::DbAdd {
name: "missing".to_owned(),
path: home.path().join("missing.sqlite"),
},
)
.is_err()
);
}
#[test]
fn kv_create_set_get_use_local_store() {
let home = tempfile::tempdir().expect("tempdir");
let paths = geth_config::GethPaths::from_home(home.path());
let node = geth_node::init_node(&paths).expect("init node");
let response = geth_node::handle_request(
&node,
geth_control::ControlRequest::KvCreate {
name: "prefs".to_owned(),
},
)
.expect("create kv");
match response {
geth_control::ControlResponse::KvCreated { kv } => {
assert_eq!(kv.name, "prefs");
assert_eq!(kv.sync_status, "local-only");
}
other => panic!("unexpected response: {other:?}"),
}
let response = geth_node::handle_request(
&node,
geth_control::ControlRequest::KvSet {
name: "prefs".to_owned(),
key: "apps/foo/theme".to_owned(),
value: "dark".to_owned(),
},
)
.expect("set kv");
match response {
geth_control::ControlResponse::KvSet { entry } => {
assert_eq!(entry.store.to_string(), "kv:prefs");
assert_eq!(entry.key, "apps/foo/theme");
assert_eq!(entry.value, "dark");
}
other => panic!("unexpected response: {other:?}"),
}
let response = geth_node::handle_request(
&node,
geth_control::ControlRequest::KvGet {
name: "prefs".to_owned(),
key: "apps/foo/theme".to_owned(),
},
)
.expect("get kv");
match response {
geth_control::ControlResponse::KvGet { entry } => {
assert_eq!(entry.expect("entry").value, "dark");
}
other => panic!("unexpected response: {other:?}"),
}
let response = geth_node::handle_request(
&node,
geth_control::ControlRequest::KvGet {
name: "prefs".to_owned(),
key: "apps/foo/missing".to_owned(),
},
)
.expect("get missing kv");
match response {
geth_control::ControlResponse::KvGet { entry } => {
assert!(entry.is_none());
}
other => panic!("unexpected response: {other:?}"),
}
assert!(
geth_node::handle_request(
&node,
geth_control::ControlRequest::KvCreate {
name: "../bad".to_owned(),
},
)
.is_err()
);
assert!(
geth_node::handle_request(
&node,
geth_control::ControlRequest::KvSet {
name: "missing".to_owned(),
key: "apps/foo/theme".to_owned(),
value: "dark".to_owned(),
},
)
.is_err()
);
}
#[test]
fn document_create_and_status_use_local_store() {
let home = tempfile::tempdir().expect("tempdir");
let paths = geth_config::GethPaths::from_home(home.path());
let node = geth_node::init_node(&paths).expect("init node");
let response = geth_node::handle_request(
&node,
geth_control::ControlRequest::DocumentCreate {
name: "notes".to_owned(),
},
)
.expect("create document");
match response {
geth_control::ControlResponse::DocumentCreated { document } => {
assert_eq!(document.name, "notes");
assert_eq!(document.sync_status, "local-only");
assert_eq!(document.state_bytes, 2);
}
other => panic!("unexpected response: {other:?}"),
}
let response = geth_node::handle_request(
&node,
geth_control::ControlRequest::DocumentStatus {
name: "notes".to_owned(),
},
)
.expect("document status");
match response {
geth_control::ControlResponse::DocumentStatus { document } => {
assert_eq!(document.id.to_string(), "document:notes");
assert_eq!(document.resource.to_string(), "resource:document:notes");
assert_eq!(document.state_bytes, 2);
}
other => panic!("unexpected response: {other:?}"),
}
assert!(
geth_node::handle_request(
&node,
geth_control::ControlRequest::DocumentCreate {
name: "../bad".to_owned(),
},
)
.is_err()
);
assert!(
geth_node::handle_request(
&node,
geth_control::ControlRequest::DocumentStatus {
name: "missing".to_owned(),
},
)
.is_err()
);
}
#[test]
fn secret_create_rotate_and_status_track_resource_epochs() {
let home = tempfile::tempdir().expect("tempdir");
let paths = geth_config::GethPaths::from_home(home.path());
let node = geth_node::init_node(&paths).expect("init node");
let first = geth_node::handle_request(
&node,
geth_control::ControlRequest::SecretCreate {
resource: "resource:cas:local".to_owned(),
},
)
.expect("create secret");
match first {
geth_control::ControlResponse::SecretCreated { secret } => {
assert_eq!(secret.resource.to_string(), "resource:cas:local");
assert_eq!(secret.epoch, 1);
}
other => panic!("unexpected response: {other:?}"),
}
let second = geth_node::handle_request(
&node,
geth_control::ControlRequest::SecretRotate {
resource: "resource:cas:local".to_owned(),
},
)
.expect("rotate secret");
match second {
geth_control::ControlResponse::SecretCreated { secret } => {
assert_eq!(secret.resource.to_string(), "resource:cas:local");
assert_eq!(secret.epoch, 2);
}
other => panic!("unexpected response: {other:?}"),
}
let status = geth_node::handle_request(&node, geth_control::ControlRequest::SecretStatus)
.expect("secret status");
match status {
geth_control::ControlResponse::SecretStatus { secrets } => {
assert_eq!(secrets.len(), 2);
assert_eq!(secrets[0].epoch, 1);
assert_eq!(secrets[1].epoch, 2);
}
other => panic!("unexpected response: {other:?}"),
}
assert!(
geth_node::handle_request(
&node,
geth_control::ControlRequest::SecretCreate {
resource: "resource:missing".to_owned(),
},
)
.is_err()
);
}
#[test]
fn bearer_access_create_list_revoke_uses_resource_scoped_auth_ops() {
let home = tempfile::tempdir().expect("tempdir");
let paths = geth_config::GethPaths::from_home(home.path());
let node = geth_node::init_node(&paths).expect("init node");
let response = geth_node::handle_request(
&node,
geth_control::ControlRequest::SecretBearerCreate {
resource: "resource:cas:local".to_owned(),
capabilities: vec!["cas.fetch".to_owned(), "cas.pin".to_owned()],
expires_at_ms: Some(1234),
},
)
.expect("create bearer");
let secret = match response {
geth_control::ControlResponse::SecretBearerCreated { access } => {
assert_eq!(access.resource.to_string(), "resource:cas:local");
assert_eq!(access.capabilities.len(), 2);
assert_eq!(access.expires_at.map(|expires_at| expires_at.0), Some(1234));
assert!(!access.may_delegate);
access.secret.to_string()
}
other => panic!("unexpected response: {other:?}"),
};
let response = geth_node::handle_request(&node, geth_control::ControlRequest::SecretBearerList)
.expect("list bearer");
match response {
geth_control::ControlResponse::SecretBearerList { access } => {
assert_eq!(access.len(), 1);
assert_eq!(access[0].secret.to_string(), secret);
assert!(!access[0].may_delegate);
}
other => panic!("unexpected response: {other:?}"),
}
geth_node::handle_request(
&node,
geth_control::ControlRequest::SecretBearerRevoke {
resource: "resource:cas:local".to_owned(),
secret: secret.clone(),
},
)
.expect("revoke bearer");
let response = geth_node::handle_request(&node, geth_control::ControlRequest::SecretBearerList)
.expect("list revoked bearer");
match response {
geth_control::ControlResponse::SecretBearerList { access } => {
assert!(access.is_empty());
}
other => panic!("unexpected response: {other:?}"),
}
assert!(
geth_node::handle_request(
&node,
geth_control::ControlRequest::SecretBearerCreate {
resource: "resource:cas:local".to_owned(),
capabilities: vec!["auth.delegate".to_owned()],
expires_at_ms: None,
},
)
.is_err()
);
}
#[test]
fn pubsub_pub_sub_uses_lossy_in_memory_runtime() {
let home = tempfile::tempdir().expect("tempdir");
let paths = geth_config::GethPaths::from_home(home.path());
let node = geth_node::init_node(&paths).expect("init node");
let response = geth_node::handle_request(
&node,
geth_control::ControlRequest::PubsubPub {
topic: "presence/laptop".to_owned(),
message: "online".to_owned(),
},
)
.expect("publish");
match response {
geth_control::ControlResponse::PubsubPublished { message } => {
assert_eq!(message.topic.to_string(), "presence/laptop");
assert_eq!(message.message, "online");
}
other => panic!("unexpected response: {other:?}"),
}
let response = geth_node::handle_request(
&node,
geth_control::ControlRequest::PubsubSub {
topic: "presence/laptop".to_owned(),
},
)
.expect("subscribe snapshot");
match response {
geth_control::ControlResponse::PubsubMessages {
topic,
messages,
note,
} => {
assert_eq!(topic, "presence/laptop");
assert_eq!(messages.len(), 1);
assert_eq!(messages[0].message, "online");
assert!(note.contains("not authoritative storage"));
}
other => panic!("unexpected response: {other:?}"),
}
let reopened = geth_node::open_node(&paths).expect("reopen node");
let response = geth_node::handle_request(
&reopened,
geth_control::ControlRequest::PubsubSub {
topic: "presence/laptop".to_owned(),
},
)
.expect("subscribe reopened snapshot");
match response {
geth_control::ControlResponse::PubsubMessages { messages, .. } => {
assert!(messages.is_empty());
}
other => panic!("unexpected response: {other:?}"),
}
assert!(
geth_node::handle_request(
&node,
geth_control::ControlRequest::PubsubPub {
topic: "presence/laptop".to_owned(),
message: String::new(),
},
)
.is_err()
);
}
#[test]
fn ssh_cert_request_approval_and_revocation_export_use_local_state() {
let home = tempfile::tempdir().expect("tempdir");
let paths = geth_config::GethPaths::from_home(home.path());
let node = geth_node::init_node(&paths).expect("init node");
let public_key_path = home.path().join("id_ed25519.pub");
std::fs::write(&public_key_path, "ssh-ed25519 AAAATEST eric@geth\n").expect("write pubkey");
let response = geth_node::handle_request(
&node,
geth_control::ControlRequest::SshCertRequest {
public_key_path: public_key_path.clone(),
cert_kind: "user".to_owned(),
principals: vec!["eric".to_owned()],
requested_validity: Some("+52w".to_owned()),
renewal_of: None,
reason: Some("renewal".to_owned()),
},
)
.expect("request cert");
let request_id = match response {
geth_control::ControlResponse::SshCertRequested { request } => request.id.to_string(),
other => panic!("unexpected response: {other:?}"),
};
let response = geth_node::handle_request(
&node,
geth_control::ControlRequest::SshCertApprove {
request_id: request_id.clone(),
ca_key_path: home.path().join("ca_sk"),
valid_for: Some("+4w".to_owned()),
serial: Some(42),
out: None,
},
)
.expect("approve cert");
match response {
geth_control::ControlResponse::SshCertApproved { approval } => {
assert_eq!(approval.request_id.to_string(), request_id);
assert!(approval.signing_command.contains(&"ssh-keygen".to_owned()));
assert!(approval.signing_command.contains(&"42".to_owned()));
}
other => panic!("unexpected response: {other:?}"),
}
let export_path = home.path().join("revocations.jsonl");
geth_node::handle_request(
&node,
geth_control::ControlRequest::SshRevocationAdd {
kind: "public-key".to_owned(),
target: "ssh:blake3:test".to_owned(),
reason: Some("lost key".to_owned()),
},
)
.expect("add revocation");
geth_node::handle_request(
&node,
geth_control::ControlRequest::SshRevocationExport {
out: export_path.clone(),
},
)
.expect("export revocations");
assert!(
std::fs::read_to_string(export_path)
.expect("read revocations")
.contains("lost key")
);
}