crates/monetize-product-wasm/tests/the_toy_signup_guest.rs
//! **The signup guest kind, end to end on the REAL module** (the toy signup
//! guest, built by `cargo xtask build-guests`): the host computes and signs the
//! fact, the guest makes the account and takes the fact, `Created` only after
//! both; a name another session holds is `NameTaken`; a product that is down
//! is `Unavailable` and nothing is recorded; the claims survive a restart; a
//! module that is not a signup guest, or not vouched for, is refused by name.
use std::path::{Path, PathBuf};
use monetize_pay::signup::{NameVerdict, PaidSignup, SignupError, SignupOutcome, SignupSink};
use monetize_pay::{Currency, Money};
use monetize_product::{EntitlementFact, State, TenantId};
use monetize_product_wasm::signup::{FactFor, GuestSignup};
fn built(stem: &str) -> PathBuf {
let target = std::env::var("CARGO_TARGET_DIR").map(PathBuf::from).unwrap_or_else(|_| Path::new(env!("CARGO_MANIFEST_DIR")).join("../../target"));
let p = target.join(format!("wasm32-unknown-unknown/release/{stem}.wasm"));
assert!(p.is_file(), "{} has not been built. This test loads the REAL guest; it does not skip.\nBuild it with:\n cargo xtask build-guests", p.display());
p
}
fn scratch(name: &str) -> PathBuf {
let p = std::env::temp_dir().join(format!("monetize-signup-guest-{name}-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&p);
std::fs::create_dir_all(&p).unwrap();
p
}
/// Sign `module` as a release does (every guest monetize runs is signed, owner 2026-10-17): a
/// CA of its own beside it (`<module>.root.der`), its `.p7s` beside it. Answers the root.
fn vouched(module: &Path) -> PathBuf {
use guest_host::verify::sign::{self, dev};
let (ca_key, ca_der) = dev::mint_ca("test-plugin-root").unwrap();
let mut root = module.as_os_str().to_owned();
root.push(".root.der");
let root = PathBuf::from(root);
std::fs::write(&root, &ca_der).unwrap();
sign::sign_module_file(module, &dev::new_ed25519_signer(&ca_key, &ca_der, "test-plugin-signing").unwrap()).unwrap();
root
}
fn block(dir: &Path, stem: &str, extra: serde_json::Value) -> serde_json::Value {
let module = dir.join(format!("{stem}.wasm"));
std::fs::copy(built(stem), &module).unwrap();
let digest = monetize_product_wasm::digest_hex(&std::fs::read(&module).unwrap());
let mut b = serde_json::json!({
"guest": module, "guest_digest": digest, "guest_trust_roots": [vouched(&module)], "grants": ["state"], "product": "toy",
"plans": { "toy/10gb-1m": { "amount_minor": 10000, "currency": "Sek", "cap_bytes": 10737418240u64, "period_days": 30 } }
});
if let (Some(o), Some(e)) = (b.as_object_mut(), extra.as_object()) {
for (k, v) in e {
o.insert(k.clone(), v.clone());
}
}
b
}
/// The host's fact: signed (a non-empty signature stands for monetize's).
fn fact_for() -> FactFor {
Box::new(|s: &PaidSignup| {
if s.plan != "toy/10gb-1m" {
return Err(format!("plan {:?} is not sold here", s.plan));
}
Ok(EntitlementFact {
tenant: TenantId(s.username.clone()),
plan: format!("toy/{}/1970-01-01+pack_bytes=10737418240", s.username),
state: State::Paid,
paid_until_unix_ms: Some(30 * 86_400_000),
caps: [("pack_bytes".to_string(), 10_737_418_240)].into_iter().collect(),
source: format!("payment:{}:x", s.vendor),
signature: vec![7; 64],
issued_unix_ms: None,
issued_signature: Vec::new(),
})
})
}
fn paid(user: &str, session: &str) -> PaidSignup {
PaidSignup {
vendor: "stripe".into(),
session: session.into(),
reference: String::new(),
email: "a@b.se".into(),
username: user.into(),
product: "toy".into(),
plan: "toy/10gb-1m".into(),
amount: Money { minor: 10000, currency: Currency::Sek },
tax: Money { minor: 0, currency: Currency::Sek },
paid_at_unix_ms: 0,
customer_country: None,
}
}
#[test]
fn the_guest_creates_the_account_and_takes_the_signed_fact_and_only_then_says_created() {
let dir = scratch("create");
let b = block(&dir, "monetize_guest_toy_signup", serde_json::json!({}));
let sink = GuestSignup::build(&dir, &b, fact_for()).unwrap();
assert!(sink.creates_accounts());
assert_eq!(sink.name_verdict("bob").unwrap(), NameVerdict::Free);
assert_eq!(sink.paid(&paid("bob", "cs_1")).unwrap(), SignupOutcome::Created { namespace: "bob".into(), username: "bob".into() });
// the account and the delivered fact are in the host's durable state
let state: std::collections::BTreeMap<String, Vec<u8>> = serde_json::from_slice(&std::fs::read(dir.join("signup-guest-state.toy.json")).unwrap()).unwrap();
assert_eq!(state["account/bob"], b"stripe:cs_1");
let delivered: EntitlementFact = serde_json::from_slice(&state["fact/bob"]).unwrap();
assert_eq!((delivered.tenant.0.as_str(), delivered.caps["pack_bytes"], delivered.signature.len()), ("bob", 10_737_418_240, 64), "the host's signed fact, as it was");
// the same session again finishes its own account; another session is refused the name
assert!(sink.paid(&paid("bob", "cs_1")).is_ok());
assert!(matches!(sink.paid(&paid("bob", "cs_2")), Err(SignupError::NameTaken(_))));
assert!(matches!(sink.name_verdict("bob").unwrap(), NameVerdict::Taken(_)));
// a plan the host does not sell: refused before the guest is asked
let mut other = paid("carol", "cs_3");
other.plan = "toy/free".into();
assert!(matches!(sink.paid(&other), Err(SignupError::Refused(m)) if m.contains("not sold")));
// the claims survive a restart
drop(sink);
let again = GuestSignup::build(&dir, &b, fact_for()).unwrap();
assert!(matches!(again.name_verdict("bob").unwrap(), NameVerdict::Taken(_)), "a restart forgets no account");
}
#[test]
fn a_product_that_is_down_is_unavailable_and_nothing_is_recorded() {
let dir = scratch("down");
let b = block(&dir, "monetize_guest_toy_signup", serde_json::json!({ "down": true }));
let sink = GuestSignup::build(&dir, &b, fact_for()).unwrap();
assert!(matches!(sink.paid(&paid("dave", "cs_9")), Err(SignupError::Unavailable(_))), "pending and retried, never Created");
assert_eq!(sink.name_verdict("dave").unwrap(), NameVerdict::Free);
}
#[test]
fn a_module_that_is_not_a_signup_guest_or_not_vouched_for_or_refuses_its_settings_is_refused_by_name() {
let dir = scratch("refuse");
// a CLOUD guest in a [signups.<product>] block
let e = GuestSignup::build(&dir, &block(&dir, "monetize_guest_toy_cloud", serde_json::json!({})), fact_for()).unwrap_err();
assert!(e.contains("[signups.") && e.contains("signup"), "{e}");
// nobody vouches
let mut b = block(&dir, "monetize_guest_toy_signup", serde_json::json!({}));
b.as_object_mut().unwrap().remove("guest_digest");
b.as_object_mut().unwrap().remove("guest_trust_roots");
let e = GuestSignup::build(&dir, &b, fact_for()).unwrap_err();
assert!(e.contains("[signups.toy]"), "{e}");
// its own check refuses
let e = GuestSignup::build(&dir, &block(&dir, "monetize_guest_toy_signup", serde_json::json!({ "refuse_check": true })), fact_for()).unwrap_err();
assert!(e.contains("refused its own settings"), "{e}");
}