Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/monetize

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}");
}