#![cfg(unix)]
use std::{
path::{Path, PathBuf},
process::{Child, Command, Stdio},
time::Duration,
};
use libmoshpit::{AgentClient, AgentRequest, AgentResponse};
const TEST_KEY_PATH: &str = concat!(
env!("CARGO_MANIFEST_DIR"),
"/../libmoshpit/tests/keys/id_x25519_test"
);
const TEST_KEY_ENC_PATH: &str = concat!(
env!("CARGO_MANIFEST_DIR"),
"/../libmoshpit/tests/keys/id_x25519_test_enc"
);
const TEST_KEY_ENC_PASSPHRASE: &str = "test";
fn agent_binary() -> PathBuf {
PathBuf::from(env!("CARGO_BIN_EXE_mpa"))
}
fn temp_socket(name: &str) -> PathBuf {
std::env::temp_dir().join(format!(
"moshpit-agent-test-{}-{}.sock",
name,
std::process::id()
))
}
fn temp_vault(name: &str) -> PathBuf {
std::env::temp_dir().join(format!(
"moshpit-agent-test-{}-{}.vault",
name,
std::process::id()
))
}
fn start_agent(socket: &Path, vault: &Path) -> Child {
use std::io::Write as _;
let mut child = Command::new(agent_binary())
.args([
"start",
"--foreground",
"--passphrase-stdin",
"--socket",
socket.to_str().expect("test socket path is valid UTF-8"),
"--vault",
vault.to_str().expect("test vault path is valid UTF-8"),
])
.stdin(Stdio::piped())
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.expect("failed to start mpa");
if let Some(mut stdin) = child.stdin.take() {
let _ = stdin.write_all(b"\n");
}
child
}
async fn wait_for_socket(socket: &Path, timeout: Duration) -> bool {
let deadline = std::time::Instant::now() + timeout;
while std::time::Instant::now() < deadline {
if socket.exists() {
return true;
}
tokio::time::sleep(Duration::from_millis(50)).await;
}
false
}
async fn wait_for_lock(lock: &Path, timeout: Duration) -> bool {
let deadline = std::time::Instant::now() + timeout;
while std::time::Instant::now() < deadline {
if lock.exists() {
return true;
}
tokio::time::sleep(Duration::from_millis(50)).await;
}
false
}
#[tokio::test]
async fn agent_lifecycle_add_list_lock_unlock() {
let socket = temp_socket("lifecycle");
let vault = temp_vault("lifecycle");
let mut child = start_agent(&socket, &vault);
let ready = wait_for_socket(&socket, Duration::from_secs(5)).await;
if !ready {
child.kill().ok();
child.wait().ok();
panic!("agent socket did not appear within 5s");
}
let client = AgentClient::new(socket.clone());
let ids = client.list_identities().await.expect("list");
assert!(ids.is_empty(), "expected empty list on fresh agent");
let resp = client
.send(&AgentRequest::AddIdentity {
key_path: TEST_KEY_PATH.to_string(),
passphrase: None,
})
.await
.expect("add identity");
assert!(
matches!(resp, AgentResponse::Ok),
"add identity should succeed"
);
let ids = client.list_identities().await.expect("list after add");
assert_eq!(ids.len(), 1, "expected 1 identity after add");
let fp = ids[0].fingerprint.clone();
assert!(
fp.starts_with("SHA256:"),
"fingerprint should have SHA256: prefix"
);
let pk = client.get_public_key(&fp).await.expect("get public key");
assert!(!pk.is_empty(), "public key bytes should not be empty");
let resp = client.send(&AgentRequest::Lock).await.expect("lock");
assert!(matches!(resp, AgentResponse::Ok));
let ids_after_lock = client.list_identities().await.expect("list after lock");
assert!(
ids_after_lock.is_empty(),
"agent should have no identities after lock"
);
let resp = client.get_public_key(&fp).await;
assert!(
resp.is_err(),
"get_public_key should fail when agent is locked"
);
let resp = client
.send(&AgentRequest::Unlock(String::new()))
.await
.expect("unlock");
assert!(matches!(resp, AgentResponse::Ok));
let ids_after_unlock = client.list_identities().await.expect("list after unlock");
assert_eq!(
ids_after_unlock.len(),
1,
"agent should have 1 identity after unlock"
);
let resp = client
.send(&AgentRequest::RemoveAllIdentities)
.await
.expect("remove all");
assert!(matches!(resp, AgentResponse::Ok));
let ids_empty = client.list_identities().await.expect("final list");
assert!(ids_empty.is_empty());
child.kill().ok();
child.wait().ok();
let _ = std::fs::remove_file(&socket);
let _ = std::fs::remove_file(&vault);
}
#[tokio::test]
async fn duplicate_start_is_rejected() {
use std::io::Write as _;
let socket = temp_socket("dup");
let vault = temp_vault("dup");
let lock = socket.with_extension("lock");
let mut daemon = start_agent(&socket, &vault);
let ready = wait_for_socket(&socket, Duration::from_secs(5)).await
&& wait_for_lock(&lock, Duration::from_secs(5)).await;
if !ready {
daemon.kill().ok();
daemon.wait().ok();
panic!("agent socket/lock did not appear within 5s");
}
let mut second = Command::new(agent_binary())
.args([
"start",
"--passphrase-stdin",
"--socket",
socket.to_str().expect("test socket path is valid UTF-8"),
"--vault",
vault.to_str().expect("test vault path is valid UTF-8"),
])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("spawn second mpa start");
if let Some(mut stdin) = second.stdin.take() {
let _ = stdin.write_all(b"\n");
}
let output = second.wait_with_output().expect("wait second mpa");
assert!(
!output.status.success(),
"second start should exit non-zero"
);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("already running"),
"expected 'already running' in stderr; got: {stderr}"
);
assert!(
stderr.contains("mpa stop"),
"expected 'mpa stop' hint in stderr; got: {stderr}"
);
daemon.kill().ok();
daemon.wait().ok();
let _ = std::fs::remove_file(&socket);
let _ = std::fs::remove_file(&lock);
let _ = std::fs::remove_file(&vault);
}
#[tokio::test]
async fn agent_add_encrypted_key() {
let socket = temp_socket("enc");
let vault = temp_vault("enc");
let mut child = start_agent(&socket, &vault);
let ready = wait_for_socket(&socket, Duration::from_secs(5)).await;
if !ready {
child.kill().ok();
child.wait().ok();
panic!("agent socket did not appear");
}
let client = AgentClient::new(socket.clone());
let resp = client
.send(&AgentRequest::AddIdentity {
key_path: TEST_KEY_ENC_PATH.to_string(),
passphrase: Some(TEST_KEY_ENC_PASSPHRASE.to_string()),
})
.await
.expect("add encrypted identity");
assert!(
matches!(resp, AgentResponse::Ok),
"add encrypted identity should succeed, got {resp:?}"
);
let ids = client.list_identities().await.expect("list");
assert_eq!(ids.len(), 1);
let resp = client
.send(&AgentRequest::AddIdentity {
key_path: TEST_KEY_ENC_PATH.to_string(),
passphrase: Some("wrong".to_string()),
})
.await
.expect("request succeeded but response should be error");
assert!(
matches!(resp, AgentResponse::Error(_)),
"expected error for wrong passphrase"
);
child.kill().ok();
child.wait().ok();
let _ = std::fs::remove_file(&socket);
let _ = std::fs::remove_file(&vault);
}