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") ); }