use std::collections::HashMap;
use std::path::PathBuf;
use std::process::Command;
use std::sync::atomic::{AtomicU32, Ordering};
use std::time::Duration;
use axum::{Json, Router, extract::Query, routing::get};
use serde_json::{Value, json};
static COUNTER: AtomicU32 = AtomicU32::new(0);
const ORG_DID: &str = "did:wire:org:acme-0123456789abcdef0123456789abcdef";
fn fresh_dir(prefix: &str) -> PathBuf {
let n = COUNTER.fetch_add(1, Ordering::SeqCst);
let path = std::env::temp_dir().join(format!(
"wire-orgbind-e2e-{prefix}-{}-{n}",
std::process::id()
));
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_json(home: &PathBuf, doh_url: &str, args: &[&str]) -> (bool, Value) {
let out = Command::new(wire_bin())
.args(args)
.env("WIRE_HOME", home)
.env("WIRE_HOME_FORCE", "1")
.env("WIRE_DOH_URL", doh_url)
.output()
.expect("spawn wire");
let parsed = serde_json::from_slice(&out.stdout).unwrap_or(Value::Null);
(out.status.success(), parsed)
}
async fn doh_handler(Query(params): Query<HashMap<String, String>>) -> Json<Value> {
let name = params.get("name").cloned().unwrap_or_default();
let qtype = params.get("type").cloned().unwrap_or_default();
if name == "_wire-org.acme.test" && qtype == "TXT" {
Json(json!({
"Status": 0,
"Answer": [
{ "name": name, "type": 16, "data": format!("\"did={ORG_DID}; v=1\"") }
]
}))
} else {
Json(json!({ "Status": 0, "Answer": [] }))
}
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn org_bind_resolves_doh_and_round_trips_policy() {
let app = Router::new().route("/dns-query", get(doh_handler));
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 doh_url = format!("http://{addr}/dns-query");
let home = fresh_dir("receiver");
let (ok, bound) = wire_json(&home, &doh_url, &["org", "bind", "acme.test", "--json"]);
assert!(ok, "org bind failed: {bound:?}");
assert_eq!(bound["org_did"].as_str(), Some(ORG_DID));
assert_eq!(bound["mode"].as_str(), Some("notify"));
let (ok, listed) = wire_json(&home, &doh_url, &["org", "list", "--json"]);
assert!(ok);
let orgs = listed["orgs"].as_array().expect("orgs array");
assert_eq!(orgs.len(), 1);
assert_eq!(orgs[0]["org_did"].as_str(), Some(ORG_DID));
assert_eq!(orgs[0]["mode"].as_str(), Some("notify"));
let (ok, forgotten) = wire_json(&home, &doh_url, &["org", "forget", ORG_DID, "--json"]);
assert!(ok);
assert_eq!(forgotten["forgotten"].as_bool(), Some(true));
let (_ok, after) = wire_json(&home, &doh_url, &["org", "list", "--json"]);
assert_eq!(after["orgs"].as_array().map(|a| a.len()), Some(0));
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn org_bind_errors_when_no_record() {
let app = Router::new().route("/dns-query", get(doh_handler));
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 doh_url = format!("http://{addr}/dns-query");
let home = fresh_dir("nobind");
let (ok, _v) = wire_json(&home, &doh_url, &["org", "bind", "unknown.test", "--json"]);
assert!(!ok, "binding a domain with no wire-org record must fail");
let (_ok, listed) = wire_json(&home, &doh_url, &["org", "list", "--json"]);
assert_eq!(listed["orgs"].as_array().map(|a| a.len()), Some(0));
}