Files
signer/tests/integration.rs

213 lines
8.0 KiB
Rust

//! Integration tests — end-to-end server + client over Unix socket.
//!
//! These tests verify the role-name-as-password model:
//! caller sends a request with a role name → selector resolution → dispatch.
//! No policy table, no approval prompt. Knowing a valid role name is sufficient.
use signer::{
alg_cache::AlgorithmKeyCache,
dispatcher::DispatcherContext,
key_store::KeyStore,
mnemonic::MnemonicState,
role_table::{RoleCurve, RolePurpose, RoleTable},
server::{AuthMode, ListenMode, ServerContext},
};
use std::os::unix::net::UnixListener;
use std::time::Duration;
const TEST_MNEMONIC: &str = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
/// Set up a server context with a test mnemonic and role table.
fn setup_server(socket_name: &str) -> (
ServerContext,
RoleTable,
MnemonicState,
KeyStore,
AlgorithmKeyCache,
) {
let mut mnemonic = MnemonicState::new();
mnemonic.load(TEST_MNEMONIC).unwrap();
let mut role_table = RoleTable::new();
role_table
.register_role_path(
"main",
"m/44'/1237'/0'/0/0",
RolePurpose::Nostr,
RoleCurve::Secp256k1,
-1, -1, -1, &[],
)
.unwrap();
let mut key_store = KeyStore::new();
key_store.derive_all(&mut role_table, &mnemonic).unwrap();
let mut server = ServerContext::new(socket_name, ListenMode::Unix, AuthMode::Off);
server.start().unwrap();
(server, role_table, mnemonic, key_store, AlgorithmKeyCache::new())
}
/// Run the server poll loop in a background thread.
///
/// Returns a handle and a stop flag. Set the stop flag to `true` to
/// terminate the loop (the thread will exit on the next iteration).
fn spawn_server_loop(
mut server: ServerContext,
mut role_table: RoleTable,
mnemonic: MnemonicState,
mut key_store: KeyStore,
mut alg_cache: AlgorithmKeyCache,
) -> (std::thread::JoinHandle<()>, std::sync::Arc<std::sync::atomic::AtomicBool>) {
let stop = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
let stop_clone = stop.clone();
let handle = std::thread::spawn(move || {
while server.running && !stop_clone.load(std::sync::atomic::Ordering::SeqCst) {
let mut dispatcher = DispatcherContext {
role_table: &mut role_table,
mnemonic: &mnemonic,
key_store: &mut key_store,
alg_key_cache: &mut alg_cache,
};
match server.handle_one(&mut dispatcher) {
Ok(Some(_activity)) => {}
Ok(None) => {
std::thread::sleep(Duration::from_millis(10));
}
Err(_) => break,
}
}
});
(handle, stop)
}
/// Send a framed request to a Unix socket and return the response.
fn send_request(socket_name: &str, request: &str) -> String {
let mut stream = signer::transport::connect_abstract_unix(socket_name).unwrap();
signer::transport::send_framed(&mut stream, request).unwrap();
signer::transport::recv_framed(&mut stream).unwrap()
}
/// Wait for the server socket to be ready.
fn wait_for_server(socket_name: &str, attempts: u32) {
for _ in 0..attempts {
if signer::transport::connect_abstract_unix(socket_name).is_ok() {
return;
}
std::thread::sleep(Duration::from_millis(20));
}
panic!("server socket {} not ready", socket_name);
}
#[test]
fn test_get_info_works() {
let socket_name = format!("signer_test_info_{}", std::process::id());
let (server, role_table, mnemonic, key_store, alg_cache) = setup_server(&socket_name);
let (handle, stop) = spawn_server_loop(server, role_table, mnemonic, key_store, alg_cache);
wait_for_server(&socket_name, 100);
// get_info is metadata — no role needed
let resp = send_request(&socket_name, r#"{"id":"1","method":"get_info","params":[]}"#);
assert!(resp.contains("\"result\""), "get_info failed: {}", resp);
let _ = signer::transport::connect_abstract_unix(&socket_name);
stop.store(true, std::sync::atomic::Ordering::SeqCst);
handle.join().ok();
}
#[test]
fn test_role_as_password_allows_with_valid_role() {
let socket_name = format!("signer_test_allow_{}", std::process::id());
let (server, role_table, mnemonic, key_store, alg_cache) = setup_server(&socket_name);
let (handle, stop) = spawn_server_loop(server, role_table, mnemonic, key_store, alg_cache);
wait_for_server(&socket_name, 100);
// Knowing the "main" role name is sufficient — no authorization.
let resp = send_request(
&socket_name,
r#"{"id":"2","method":"nostr_get_public_key","params":[{"role":"main","role_path":"m/44'/1237'/0'/0/0"}]}"#,
);
assert!(resp.contains("\"result\""), "role-as-password should allow, got: {}", resp);
// Result is a plain hex pubkey string (64 hex chars)
assert!(resp.contains("e8bcf3823669444d0b49ad45d65088635d9fd8500a75b5f20b59abefa56a144f"),
"expected pubkey in result, got: {}", resp);
let _ = signer::transport::connect_abstract_unix(&socket_name);
stop.store(true, std::sync::atomic::Ordering::SeqCst);
handle.join().ok();
}
#[test]
fn test_unknown_role_returns_selector_error() {
let socket_name = format!("signer_test_unknown_{}", std::process::id());
let (server, role_table, mnemonic, key_store, alg_cache) = setup_server(&socket_name);
let (handle, stop) = spawn_server_loop(server, role_table, mnemonic, key_store, alg_cache);
wait_for_server(&socket_name, 100);
// Unknown role name → selector error (the "password" is wrong)
let resp = send_request(
&socket_name,
r#"{"id":"3","method":"nostr_get_public_key","params":[{"role":"nonexistent","role_path":"m/44'/1237'/0'/0/0"}]}"#,
);
assert!(resp.contains("unknown_role"), "expected unknown_role, got: {}", resp);
let _ = signer::transport::connect_abstract_unix(&socket_name);
stop.store(true, std::sync::atomic::Ordering::SeqCst);
handle.join().ok();
}
#[test]
fn test_ed25519_sign_allowed_no_authorization() {
let socket_name = format!("signer_test_alg_{}", std::process::id());
let (server, role_table, mnemonic, key_store, alg_cache) = setup_server(&socket_name);
let (handle, stop) = spawn_server_loop(server, role_table, mnemonic, key_store, alg_cache);
wait_for_server(&socket_name, 100);
// Algorithm-based verbs have no authorization — dispatched directly.
let msg_hex = hex::encode(b"hello");
let req = format!(
r#"{{"id":"4","method":"sign","params":["{}",{{"algorithm":"ed25519","index":0}}]}}"#,
msg_hex
);
let resp = send_request(&socket_name, &req);
assert!(resp.contains("\"result\""), "expected success, got: {}", resp);
assert!(resp.contains("signature"), "expected signature in result: {}", resp);
let _ = signer::transport::connect_abstract_unix(&socket_name);
stop.store(true, std::sync::atomic::Ordering::SeqCst);
handle.join().ok();
}
#[test]
fn test_repeated_requests_all_allowed() {
let socket_name = format!("signer_test_repeat_{}", std::process::id());
let (server, role_table, mnemonic, key_store, alg_cache) = setup_server(&socket_name);
let (handle, stop) = spawn_server_loop(server, role_table, mnemonic, key_store, alg_cache);
wait_for_server(&socket_name, 100);
// Multiple requests with the same valid role — all allowed (no session state needed)
for i in 5..=7 {
let req = format!(
r#"{{"id":"{}","method":"nostr_get_public_key","params":[{{"role":"main","role_path":"m/44'/1237'/0'/0/0"}}]}}"#,
i
);
let resp = send_request(&socket_name, &req);
assert!(resp.contains("\"result\""), "request {} failed: {}", i, resp);
}
let _ = signer::transport::connect_abstract_unix(&socket_name);
stop.store(true, std::sync::atomic::Ordering::SeqCst);
handle.join().ok();
}
// Keep UnixListener import used (for potential future filesystem socket tests)
#[allow(dead_code)]
fn _unused(_l: UnixListener) {}