crates/monetize-admin/tests/door.rs
//! **The operator's door, over the wire**: mutual TLS under the box's
//! installer CA; the operator's certificate puts the solo feed (products,
//! vendors, prices, secrets — what the installer fed, changed after the
//! install), a box's own certificate (of the same CA) is refused by name, a
//! feed monetize would not start on is refused whole and nothing is written,
//! and what is read back is key NAMES, never a value.
use std::sync::Arc;
use std::time::Duration;
use nordisk_pairing::mtls::{self, InstallerCa, Issued};
use tokio::io::{AsyncReadExt as _, AsyncWriteExt as _};
async fn put(addr: std::net::SocketAddr, ca: &str, me: &Issued, body: &str) -> (u16, String) {
send(addr, ca, me, "PUT", "/feed", body).await
}
/// How long one request at the door may take (RA1-11, as tests/door_security.rs).
const DEADLINE: Duration = Duration::from_secs(30);
async fn send(
addr: std::net::SocketAddr,
ca: &str,
me: &Issued,
method: &str,
path: &str,
body: &str,
) -> (u16, String) {
tokio::time::timeout(DEADLINE, send_once(addr, ca, me, method, path, body))
.await
.unwrap_or_else(|_| panic!("{method} {path}: no answer within {DEADLINE:?}"))
}
async fn send_once(
addr: std::net::SocketAddr,
ca: &str,
me: &Issued,
method: &str,
path: &str,
body: &str,
) -> (u16, String) {
let cfg = mtls::client_config(ca, me).unwrap();
let tcp = tokio::net::TcpStream::connect(addr).await.unwrap();
let mut tls = tokio_rustls::TlsConnector::from(cfg)
.connect(mtls::vm_name(), tcp)
.await
.unwrap();
let req = format!("{method} {path} HTTP/1.1\r\nHost: {}\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}", mtls::VM_SERVER_NAME, body.len());
tls.write_all(req.as_bytes()).await.unwrap();
// write_all can leave the last TLS records in rustls's buffer (RA1-11): flush them.
tls.flush().await.unwrap();
let mut out = Vec::new();
let _ = tls.read_to_end(&mut out).await;
let t = String::from_utf8_lossy(&out).to_string();
(
t.split_whitespace()
.nth(1)
.and_then(|s| s.parse().ok())
.unwrap_or(0),
t.split_once("\r\n\r\n")
.map(|(_, b)| b.to_string())
.unwrap_or_default(),
)
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn the_operator_puts_the_feed_and_nobody_else_does() {
use base64::Engine as _;
let ca = InstallerCa::mint("monetize-admin", Duration::from_secs(3600)).unwrap();
let box_identity = ca.issue_bootstrap(Duration::from_secs(3600)).unwrap();
let operator = ca.issue_installer(Duration::from_secs(3600)).unwrap();
let (csr, key) = mtls::box_request().unwrap();
let stranger = Issued {
cert_pem: ca
.sign_box(&csr, "admin", Duration::from_secs(3600))
.unwrap(),
key_pem: key,
};
let dir = tempfile::tempdir().unwrap();
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
let id = box_identity.clone();
tokio::spawn(monetize_admin::door::serve(
listener,
ca.cert_pem.clone(),
Arc::new(move || id.clone()),
dir.path().to_path_buf(),
));
let wasm = base64::engine::general_purpose::STANDARD.encode(b"\0asm\x01\0\0\0");
let feed = |vendor: &str| {
serde_json::json!({
"plugin.holger.wasm": wasm,
"plugin.holger.toml": format!("{vendor}instance = \"holger-rs\"\nguest_digest = \"ab\"\ngrants = []\n"),
"plugin.holger.prices": "[SE]\nrepo_month = 4900\n",
"vendor.stripe.holger.wasm": wasm,
"vendor.stripe.holger.toml": "enabled = true\nguest_digest = \"cd\"\n",
"secret.MONETIZE_STRIPE_HOLGER_SECRET_KEY": "sk_test_never_shown",
})
.to_string()
};
let good = feed("vendor = \"stripe/holger\"\n");
let (st, body) = put(addr, &ca.cert_pem, &stranger, &good).await;
assert_eq!(st, 403, "{body}");
assert!(body.contains("[not-the-operator]"), "{body}");
assert!(
!dir.path()
.join(monetize_admin::solo::DOOR_FEED_REL)
.exists(),
"nothing written for a stranger"
);
let (st, body) = put(addr, &ca.cert_pem, &operator, &feed("")).await;
assert_eq!(st, 422, "{body}");
assert!(
body.contains("vendor"),
"a priced product that names no vendor: {body}"
);
assert!(
!dir.path()
.join(monetize_admin::solo::DOOR_FEED_REL)
.exists(),
"a refused feed writes nothing"
);
let (st, body) = put(addr, &ca.cert_pem, &operator, "[1, 2]").await;
assert_eq!(st, 400, "{body}");
assert!(body.contains("[feed-json]"), "{body}");
// Break-glass recovery: the operator only; it leaves the console's marker.
let (st, _) = send(addr, &ca.cert_pem, &stranger, "POST", "/passkeys/reset", "").await;
assert_eq!(st, 403);
assert!(!dir.path().join(monetize_admin::solo::RESET_REL).exists());
let (st, body) = send(addr, &ca.cert_pem, &operator, "POST", "/passkeys/reset", "").await;
assert_eq!(st, 202, "{body}");
assert!(dir.path().join(monetize_admin::solo::RESET_REL).exists());
let (st, body) = put(addr, &ca.cert_pem, &operator, &good).await;
assert_eq!(st, 200, "{body}");
assert!(body.contains("holger"), "{body}");
assert!(
!body.contains("sk_test_never_shown"),
"a value is never said: {body}"
);
let kept = monetize_admin::solo::read_door_feed(dir.path());
assert_eq!(
kept.get("secret.MONETIZE_STRIPE_HOLGER_SECRET_KEY")
.map(String::as_str),
Some("sk_test_never_shown")
);
use std::os::unix::fs::PermissionsExt as _;
assert_eq!(
std::fs::metadata(dir.path().join(monetize_admin::solo::DOOR_FEED_REL))
.unwrap()
.permissions()
.mode()
& 0o777,
0o600
);
let (st, body) = send(addr, &ca.cert_pem, &operator, "GET", "/feed", "").await;
assert_eq!(st, 200, "{body}");
assert!(
body.contains("secret.MONETIZE_STRIPE_HOLGER_SECRET_KEY")
&& body.contains("plugin.holger.toml"),
"{body}"
);
assert!(
!body.contains("sk_test_never_shown") && !body.contains("4900"),
"key names only: {body}"
);
}
/// G7: **an enrolment token is the operator's to mint** on the ONE monetize's door
/// (`POST /attach/token`); the estate redeems it at the edge's `POST /attach/enrol`.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn the_operator_mints_an_enrolment_token_and_nobody_else_does() {
let ca = InstallerCa::mint("monetize-admin", Duration::from_secs(3600)).unwrap();
let box_identity = ca.issue_bootstrap(Duration::from_secs(3600)).unwrap();
let operator = ca.issue_installer(Duration::from_secs(3600)).unwrap();
let (csr, key) = mtls::box_request().unwrap();
let stranger = Issued {
cert_pem: ca
.sign_box(&csr, "admin", Duration::from_secs(3600))
.unwrap(),
key_pem: key,
};
let dir = tempfile::tempdir().unwrap();
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
let id = box_identity.clone();
tokio::spawn(monetize_admin::door::serve(
listener,
ca.cert_pem.clone(),
Arc::new(move || id.clone()),
dir.path().to_path_buf(),
));
let ask = r#"{"estate":"acme","product":"gunnar"}"#;
let tokens = dir.path().join(monetize_front::attach::TOKENS_REL);
let (st, body) = send(addr, &ca.cert_pem, &stranger, "POST", "/attach/token", ask).await;
assert_eq!(st, 403, "{body}");
assert!(!tokens.exists(), "nothing minted for a stranger");
let (st, body) = send(addr, &ca.cert_pem, &operator, "POST", "/attach/token", ask).await;
assert_eq!(st, 200, "{body}");
let v: serde_json::Value = serde_json::from_str(&body).unwrap();
assert!(v["token"].as_str().is_some_and(|t| !t.is_empty()), "{body}");
assert!(tokens.exists());
}
/// **A product the binary LINKS is fed with no module** (lane L26, 2026-10-19): the kit
/// (private-monetize-ops `[products.<id>] linked = true`) sends `plugin.<id>.toml` and
/// `.prices` and no `.wasm`, because the build serves that product with its own native
/// plugin. On a build that links `holger`, the door takes it (200) and the block names no
/// guest; a product the build does NOT link is still refused by name
/// (`[plugin-no-module]`), and so is holger on a build that links no holger — the server
/// would not start on a block with neither a module nor a plugin.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn a_linked_product_is_fed_without_a_module_and_an_unlinked_one_is_refused() {
let ca = InstallerCa::mint("monetize-admin", Duration::from_secs(3600)).unwrap();
let box_identity = ca.issue_bootstrap(Duration::from_secs(3600)).unwrap();
let operator = ca.issue_installer(Duration::from_secs(3600)).unwrap();
let start = |linked: Option<Vec<&'static str>>| {
let (ca_pem, id) = (ca.cert_pem.clone(), box_identity.clone());
async move {
let dir = tempfile::tempdir().unwrap();
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
let identity: Arc<dyn Fn() -> Issued + Send + Sync> = Arc::new(move || id.clone());
match linked {
Some(l) => tokio::spawn(monetize_admin::door::serve_linked(
listener,
ca_pem,
identity,
dir.path().to_path_buf(),
l,
)),
None => tokio::spawn(monetize_admin::door::serve(
listener,
ca_pem,
identity,
dir.path().to_path_buf(),
)),
};
(dir, addr)
}
};
let feed = |id: &str| {
serde_json::json!({
format!("plugin.{id}.toml"): format!("free = true\ninstance = \"{id}-rs\"\ngrants = []\n"),
})
.to_string()
};
// The holger-linked build (the vetra set): taken, and the block names no guest.
let (dir, addr) = start(Some(vec!["holger"])).await;
let (st, body) = put(addr, &ca.cert_pem, &operator, &feed("holger")).await;
assert_eq!(st, 200, "a linked product needs no module: {body}");
assert!(body.contains("holger"), "{body}");
assert!(monetize_admin::solo::read_door_feed(dir.path()).contains_key("plugin.holger.toml"));
// A product that build does NOT link, with no module: refused by name, nothing written.
let (dir, addr) = start(Some(vec!["holger"])).await;
let (st, body) = put(addr, &ca.cert_pem, &operator, &feed("kassa")).await;
assert_eq!(st, 422, "{body}");
assert!(
body.contains("[plugin-no-module]") && body.contains("plugin.kassa.wasm"),
"{body}"
);
assert!(
!dir.path()
.join(monetize_admin::solo::DOOR_FEED_REL)
.exists(),
"a refused feed writes nothing"
);
// This binary's own set: it links no holger, so holger with no module is refused.
let (_dir, addr) = start(None).await;
let linked = monetize_admin::linked().product_names();
let (st, body) = put(addr, &ca.cert_pem, &operator, &feed("holger")).await;
if linked.contains(&"holger") {
assert_eq!(st, 200, "{body}");
} else {
assert_eq!(st, 422, "this build links {linked:?}: {body}");
assert!(body.contains("[plugin-no-module]"), "{body}");
}
}
fn sha256_hex(b: &[u8]) -> String {
ring::digest::digest(&ring::digest::SHA256, b)
.as_ref()
.iter()
.map(|x| format!("{x:02x}"))
.collect()
}
/// ★ **R34-5: the door answers a feed APPLIED only once the box applied it** (rehearsal
/// 2026-10-25: the door said "taken" 2-3 s before the box applied the feed live, and an
/// order sent at once used the old vendor settings). PID 1 says which door feed it took
/// (`solo/door-feed.taken`: the feed file's sha256, or `<sha256> NOT taken: <why>`), the
/// server which config it applied (`<config>.applied`: the config's sha256); the door waits
/// for both, and says a refusal as one. (A stand-in plays PID 1 and the server here.)
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn the_door_answers_applied_only_after_the_box_applied_the_feed() {
use base64::Engine as _;
let ca = InstallerCa::mint("monetize-admin", Duration::from_secs(3600)).unwrap();
let box_identity = ca.issue_bootstrap(Duration::from_secs(3600)).unwrap();
let operator = ca.issue_installer(Duration::from_secs(3600)).unwrap();
let dir = tempfile::tempdir().unwrap();
let root = dir.path().to_path_buf();
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
let id = box_identity.clone();
tokio::spawn(monetize_admin::door::serve(
listener,
ca.cert_pem.clone(),
Arc::new(move || id.clone()),
root.clone(),
));
let wasm = base64::engine::general_purpose::STANDARD.encode(b"\0asm\x01\0\0\0");
let feed = |price: u32| {
serde_json::json!({
"plugin.holger.wasm": wasm,
"plugin.holger.toml": "vendor = \"stripe/holger\"\ninstance = \"holger-rs\"\nguest_digest = \"ab\"\ngrants = [\"grpc\"]\n",
"plugin.holger.prices": format!("[SE]\nrepo_month = {price}\n"),
"vendor.stripe.holger.wasm": wasm,
"vendor.stripe.holger.toml": "enabled = true\nguest_digest = \"cd\"\n",
})
.to_string()
};
// PID 1 runs and has taken an older feed; the server applied the config it wrote.
let taken = root.join("solo/door-feed.taken");
let config = root.join(monetize_front::intake::CONFIG_REL);
let applied = std::path::PathBuf::from(format!("{}.applied", config.display()));
std::fs::create_dir_all(config.parent().unwrap()).unwrap();
std::fs::create_dir_all(taken.parent().unwrap()).unwrap();
std::fs::write(&taken, "0".repeat(64)).unwrap();
std::fs::write(&config, "# old\n").unwrap();
std::fs::write(&applied, sha256_hex(b"# old\n")).unwrap();
// The stand-in: a second after the door feed moves it is taken, a second later applied.
let stand_in = |refuse: bool| {
let (root, taken, config, applied) =
(root.clone(), taken.clone(), config.clone(), applied.clone());
tokio::spawn(async move {
let door_feed = root.join(monetize_admin::solo::DOOR_FEED_REL);
let was = std::fs::read(&door_feed).ok();
while std::fs::read(&door_feed).ok() == was {
tokio::time::sleep(Duration::from_millis(50)).await;
}
let d = sha256_hex(&std::fs::read(&door_feed).unwrap());
tokio::time::sleep(Duration::from_secs(1)).await;
if refuse {
std::fs::write(
&taken,
format!("{d} NOT taken: REFUSED [stand-in] the box would not start on it"),
)
.unwrap();
return std::time::Instant::now();
}
let text = format!("# config of {d}\n");
std::fs::write(&config, &text).unwrap();
std::fs::write(&taken, &d).unwrap();
tokio::time::sleep(Duration::from_secs(1)).await;
std::fs::write(&applied, sha256_hex(text.as_bytes())).unwrap();
std::time::Instant::now()
})
};
let box_side = stand_in(false);
let (st, body) = put(addr, &ca.cert_pem, &operator, &feed(4900)).await;
let answered = std::time::Instant::now();
let applied_at = box_side.await.unwrap();
assert_eq!(st, 200, "{body}");
assert!(
answered >= applied_at,
"the door answered before the box applied the feed: {body}"
);
assert!(body.starts_with("applied:"), "{body}");
// A feed PID 1 refuses is said as refused, never as taken.
let box_side = stand_in(true);
let (st, body) = put(addr, &ca.cert_pem, &operator, &feed(5900)).await;
box_side.await.unwrap();
assert_eq!(st, 422, "{body}");
assert!(
body.contains("NOT taken") && body.contains("[stand-in]"),
"{body}"
);
}