use serde_json::Value;
use std::path::PathBuf;
use std::process::Command;
use std::sync::atomic::{AtomicU32, Ordering};
use std::time::Duration;
static COUNTER: AtomicU32 = AtomicU32::new(0);
fn fresh_dir(prefix: &str) -> PathBuf {
let n = COUNTER.fetch_add(1, Ordering::SeqCst);
let pid = std::process::id();
let path = std::env::temp_dir().join(format!("wire-e2e-{prefix}-{pid}-{n}"));
let _ = std::fs::remove_dir_all(&path);
std::fs::create_dir_all(&path).unwrap();
path
}
fn wire_bin() -> PathBuf {
PathBuf::from(env!("CARGO_BIN_EXE_wire"))
}
fn wire(home: &PathBuf, args: &[&str]) -> std::process::Output {
let out = Command::new(wire_bin())
.args(args)
.env("WIRE_HOME", home)
.output()
.expect("spawn wire");
if !out.status.success() {
eprintln!(
"wire {args:?} failed:\nstdout: {}\nstderr: {}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr)
);
}
out
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn paul_sends_to_willard_via_relay_and_willard_verifies() {
let relay_dir = fresh_dir("relay");
let relay = wire::relay_server::Relay::new(relay_dir).await.unwrap();
let app = relay.router();
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
tokio::spawn(async move { axum::serve(listener, app).await.ok() });
tokio::time::sleep(Duration::from_millis(50)).await;
let relay_url = format!("http://{addr}");
let paul_home = fresh_dir("paul");
let willard_home = fresh_dir("willard");
assert!(wire(&paul_home, &["init", "paul"]).status.success());
assert!(wire(&willard_home, &["init", "willard"]).status.success());
let paul_bind = wire(&paul_home, &["bind-relay", &relay_url, "--json"]);
assert!(paul_bind.status.success(), "paul bind-relay failed");
let paul_bind_json: Value = serde_json::from_slice(&paul_bind.stdout).unwrap();
assert!(paul_bind_json["slot_id"].as_str().unwrap().len() == 32);
let willard_bind = wire(&willard_home, &["bind-relay", &relay_url, "--json"]);
assert!(willard_bind.status.success());
let willard_relay: Value = serde_json::from_str(
&std::fs::read_to_string(willard_home.join("config/wire/relay.json")).unwrap(),
)
.unwrap();
let w_slot_id = willard_relay["self"]["slot_id"].as_str().unwrap();
let w_slot_token = willard_relay["self"]["slot_token"].as_str().unwrap();
assert!(
wire(
&paul_home,
&[
"add-peer-slot",
"willard",
&relay_url,
w_slot_id,
w_slot_token
]
)
.status
.success()
);
let paul_relay: Value = serde_json::from_str(
&std::fs::read_to_string(paul_home.join("config/wire/relay.json")).unwrap(),
)
.unwrap();
let p_slot_id = paul_relay["self"]["slot_id"].as_str().unwrap();
let p_slot_token = paul_relay["self"]["slot_token"].as_str().unwrap();
assert!(
wire(
&willard_home,
&["add-peer-slot", "paul", &relay_url, p_slot_id, p_slot_token]
)
.status
.success()
);
let paul_card_path = paul_home.join("config/wire/agent-card.json");
assert!(
wire(&willard_home, &["pin", paul_card_path.to_str().unwrap()])
.status
.success()
);
let send_out = wire(
&paul_home,
&[
"send",
"willard",
"decision",
"ship the v0.1 demo",
"--json",
],
);
assert!(send_out.status.success());
let send_json: Value = serde_json::from_slice(&send_out.stdout).unwrap();
let event_id = send_json["event_id"].as_str().unwrap().to_string();
assert_eq!(event_id.len(), 64);
let push_out = wire(&paul_home, &["push", "--json"]);
assert!(push_out.status.success(), "push failed");
let push_json: Value = serde_json::from_slice(&push_out.stdout).unwrap();
assert_eq!(
push_json["pushed"].as_array().unwrap().len(),
1,
"expected exactly 1 push, got {push_json}"
);
let pull_out = wire(&willard_home, &["pull", "--json"]);
assert!(pull_out.status.success(), "pull failed");
let pull_json: Value = serde_json::from_slice(&pull_out.stdout).unwrap();
assert_eq!(
pull_json["written"].as_array().unwrap().len(),
1,
"expected exactly 1 verified inbox write, got {pull_json}"
);
assert_eq!(
pull_json["rejected"].as_array().unwrap().len(),
0,
"got rejections: {pull_json}"
);
let tail_out = wire(&willard_home, &["tail", "paul", "--json"]);
assert!(tail_out.status.success());
let stdout = String::from_utf8(tail_out.stdout).unwrap();
let line = stdout.lines().next().expect("tail produced no output");
let event: Value = serde_json::from_str(line).unwrap();
assert_eq!(event["event_id"], event_id);
{
let from = event["from"].as_str().unwrap();
assert!(from.starts_with("did:wire:paul-"), "from: {from}");
}
assert_eq!(event["to"], "did:wire:willard");
assert_eq!(event["body"], "ship the v0.1 demo");
assert_eq!(event["verified"], true);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn pull_rejects_event_with_unknown_signer() {
let relay_dir = fresh_dir("relay-unknown");
let relay = wire::relay_server::Relay::new(relay_dir).await.unwrap();
let app = relay.router();
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
tokio::spawn(async move { axum::serve(listener, app).await.ok() });
tokio::time::sleep(Duration::from_millis(50)).await;
let relay_url = format!("http://{addr}");
let willard_home = fresh_dir("willard-unknown");
assert!(wire(&willard_home, &["init", "willard"]).status.success());
assert!(
wire(&willard_home, &["bind-relay", &relay_url])
.status
.success()
);
let paul_home = fresh_dir("paul-unknown");
assert!(wire(&paul_home, &["init", "paul"]).status.success());
assert!(
wire(&paul_home, &["bind-relay", &relay_url])
.status
.success()
);
let willard_relay: Value = serde_json::from_str(
&std::fs::read_to_string(willard_home.join("config/wire/relay.json")).unwrap(),
)
.unwrap();
let w_slot_id = willard_relay["self"]["slot_id"].as_str().unwrap();
let w_slot_token = willard_relay["self"]["slot_token"].as_str().unwrap();
assert!(
wire(
&paul_home,
&[
"add-peer-slot",
"willard",
&relay_url,
w_slot_id,
w_slot_token
]
)
.status
.success()
);
assert!(
wire(
&paul_home,
&["send", "willard", "decision", "from a stranger", "--json"]
)
.status
.success()
);
assert!(wire(&paul_home, &["push", "--json"]).status.success());
let pull_out = wire(&willard_home, &["pull", "--json"]);
assert!(pull_out.status.success());
let pull_json: Value = serde_json::from_slice(&pull_out.stdout).unwrap();
assert_eq!(pull_json["written"].as_array().unwrap().len(), 0);
assert_eq!(pull_json["rejected"].as_array().unwrap().len(), 1);
let reason = pull_json["rejected"][0]["reason"].as_str().unwrap();
assert!(
reason.contains("not in trust"),
"expected 'not in trust' rejection, got: {reason}"
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn responder_health_cli_set_get_roundtrip() {
let relay_dir = fresh_dir("relay-responder");
let relay = wire::relay_server::Relay::new(relay_dir).await.unwrap();
let app = relay.router();
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
tokio::spawn(async move { axum::serve(listener, app).await.ok() });
tokio::time::sleep(Duration::from_millis(50)).await;
let relay_url = format!("http://{addr}");
let paul_home = fresh_dir("paul-responder");
assert!(wire(&paul_home, &["init", "paul"]).status.success());
assert!(
wire(&paul_home, &["bind-relay", &relay_url])
.status
.success()
);
let set = wire(
&paul_home,
&[
"responder",
"set",
"offline",
"--reason",
"OAuth expired",
"--json",
],
);
assert!(set.status.success(), "set failed");
let set_json: Value = serde_json::from_slice(&set.stdout).unwrap();
assert_eq!(set_json["status"], "offline");
assert_eq!(set_json["reason"], "OAuth expired");
let get = wire(&paul_home, &["responder", "get", "--json"]);
assert!(get.status.success(), "get failed");
let get_json: Value = serde_json::from_slice(&get.stdout).unwrap();
assert_eq!(get_json["responder_health"]["status"], "offline");
assert_eq!(get_json["responder_health"]["reason"], "OAuth expired");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn status_peer_json_reports_three_health_layers() {
let relay_dir = fresh_dir("relay-status-peer");
let relay = wire::relay_server::Relay::new(relay_dir).await.unwrap();
let app = relay.router();
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
tokio::spawn(async move { axum::serve(listener, app).await.ok() });
tokio::time::sleep(Duration::from_millis(50)).await;
let relay_url = format!("http://{addr}");
let paul_home = fresh_dir("paul-status-peer");
let willard_home = fresh_dir("willard-status-peer");
assert!(wire(&paul_home, &["init", "paul"]).status.success());
assert!(wire(&willard_home, &["init", "willard"]).status.success());
assert!(
wire(&paul_home, &["bind-relay", &relay_url])
.status
.success()
);
assert!(
wire(&willard_home, &["bind-relay", &relay_url])
.status
.success()
);
let willard_relay: Value = serde_json::from_str(
&std::fs::read_to_string(willard_home.join("config/wire/relay.json")).unwrap(),
)
.unwrap();
assert!(
wire(
&paul_home,
&[
"add-peer-slot",
"willard",
&relay_url,
willard_relay["self"]["slot_id"].as_str().unwrap(),
willard_relay["self"]["slot_token"].as_str().unwrap(),
],
)
.status
.success()
);
let out = wire(&paul_home, &["status", "--peer", "willard", "--json"]);
assert!(out.status.success(), "status --peer failed");
let status: Value = serde_json::from_slice(&out.stdout).unwrap();
assert_eq!(status["transport"]["status"], "ok");
assert_eq!(status["attention"]["status"], "never_pulled");
assert_eq!(status["responder"]["status"], "not_reported");
}