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

nordisk/monetize

crates/monetize-server/tests/the_signed_guests_answer_the_op_table.rs

//! ★ **The guest contract's conformance table, over the SIGNED Gunnar and Holger guests**
//! (monetize v2.0 P1 built it; P2 T13 cut the contract to the six ops; audit A28 HIGH #7, A25 §2).
//!
//! Every op of the SDK's table (`monetize_product_guest::ops::ALL`: exactly the six of the
//! design, A24 §2.2) is called on the REAL modules, signed under a test CA and loaded as the ONE
//! box loads them (the feed's block: an endpoint, the guest, its trust root, and NO grants: a
//! product guest opens no door). Each guest answers all six itself (`Own`); none is the SDK's
//! default and none reaches a door. Every number that left the contract
//! (`monetize_product_guest::ops::RETIRED`: the 17 push ops, the four merged into the manifest
//! and the facts, op 41 that moved to the host, the four retired in 5.4) answers the SDK's
//! unknown-op words: the guests no longer have them.
//!
//! Inputs: the two built guests, as the suite builds them (`cargo build --release --target
//! wasm32-unknown-unknown` in `../gunnar/crates/gunnar-monetize-guest` and in `../holger -p
//! holger-monetize-guest`), read from `$CARGO_TARGET_DIR/wasm32-unknown-unknown/release/`, or
//! `GUNNAR_GUEST_WASM` / `HOLGER_GUEST_WASM`. A missing module FAILS by name: this test loads the
//! real guests and never skips.

use std::path::{Path, PathBuf};

use monetize_product::ProductError;
use monetize_product_guest::conformance::{fate, is_sdk_default, request, FATES};
use monetize_product_guest::ops;
use monetize_server::config::Config;
use monetize_server::registry::Registry;
use serde_json::Value;

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Class {
    Own,
    Door,
    Default,
}
use Class::{Default as Dflt, Door, Own};


/// **The table**: op, Gunnar's class, Holger's class. Exactly the six, each the guest's own.
const TABLE: &[(u32, Class, Class)] = &[
    (ops::MANIFEST, Own, Own),
    (ops::LIFECYCLE_FACTS, Own, Own),
    (ops::LIFECYCLE_LAY_PLAN, Own, Own),
    (ops::GUARDIAN_DIAGNOSE, Own, Own),
    (ops::GUARDIAN_PLAN, Own, Own),
    (ops::GUARDIAN_OBSERVE, Own, Own),
];

/// The grants the ONE box's feed gives a product guest (private-monetize-ops solo.toml): none.
const NO_GRANTS: &str = "[]";

/// The built module of `crate_file` (`gunnar_monetize_guest`), from `env` or the target dir.
fn built(env: &str, crate_file: &str, sibling_target: &str) -> PathBuf {
    if let Ok(p) = std::env::var(env) {
        return PathBuf::from(p);
    }
    let target = std::env::var("CARGO_TARGET_DIR").map(PathBuf::from).unwrap_or_else(|_| Path::new(env!("CARGO_MANIFEST_DIR")).join(sibling_target));
    let p = target.join(format!("wasm32-unknown-unknown/release/{crate_file}.wasm"));
    assert!(p.is_file(), "{} has not been built. This test loads the REAL guest; it does not skip ({env} names another).", p.display());
    p
}

/// The guest signed under a fresh test CA and loaded as the box's feed lays it out.
fn load(product: &str, wasm: &Path, grants: &str) -> (Registry, PathBuf) {
    load_in("op-table", product, wasm, grants)
}

/// [`load`] in a directory of `test`'s own: two tests of this binary run at once.
fn load_in(test: &str, product: &str, wasm: &Path, grants: &str) -> (Registry, PathBuf) {
    use guest_host::verify::sign::{self, dev};
    let dir = std::env::temp_dir().join(format!("monetize-{test}-{product}-{}", std::process::id()));
    let _ = std::fs::remove_dir_all(&dir);
    std::fs::create_dir_all(&dir).unwrap();
    let module = dir.join(format!("plugin.{product}.wasm"));
    std::fs::copy(wasm, &module).unwrap_or_else(|e| panic!("{}: {e}", wasm.display()));
    let (ca_key, ca_der) = dev::mint_ca("op-table-plugin-root").unwrap();
    let root = dir.join("root.der");
    std::fs::write(&root, &ca_der).unwrap();
    sign::sign_module_file(&module, &dev::new_ed25519_signer(&ca_key, &ca_der, "op-table-signing").unwrap()).unwrap();
    let config = dir.join("monetize.toml");
    std::fs::write(
        &config,
        format!(
            "data_dir = {:?}\n[products.{product}]\nendpoint = \"127.0.0.1:1\"\nguest = {:?}\nguest_trust_roots = [{:?}]\ngrants = {grants}\n",
            dir.join("data").display().to_string(),
            module.display().to_string(),
            root.display().to_string()
        ),
    )
    .unwrap();
    let cfg = Config::load(&config).unwrap();
    let registry = Registry::from_config(&cfg).unwrap_or_else(|e| panic!("the signed {product} guest loads: {e}"));
    assert!(registry.guest(product).is_some(), "the signed {product} guest was skipped at start: {:?}", registry.start_refusals());
    (registry, dir)
}

/// A failure to reach the product's control plane over the `grpc` door (the endpoint is dead).
fn reached_the_door(m: &str) -> bool {
    let m = m.to_ascii_lowercase();
    ["grpc", "127.0.0.1:1", "connection refused", "transport", "unavailable"].iter().any(|w| m.contains(w))
}

fn classify(op: u32, answer: &Result<Value, String>) -> Class {
    if is_sdk_default(op, answer) {
        return Dflt;
    }
    match answer {
        Err(m) if reached_the_door(m) => Door,
        _ => Own,
    }
}

/// Every op of the table on one loaded guest: (op, class, the answer in short).
fn observe(registry: &Registry, product: &str) -> Vec<(u32, Class, String)> {
    let g = registry.guest(product).unwrap_or_else(|| panic!("no guest for {product}"));
    ops::ALL
        .iter()
        .map(|(op, _)| {
            let answer = g.call_json(*op, &request(*op)).map_err(|e: ProductError| e.to_string());
            let short = match &answer {
                Ok(v) => format!("ok {}", v.to_string().chars().take(90).collect::<String>()),
                Err(m) => format!("err {}", m.chars().take(140).collect::<String>()),
            };
            (*op, classify(*op, &answer), short)
        })
        .collect()
}

#[test]
fn the_table_names_every_op_once_and_the_sdk_gives_each_a_fate() {
    let listed: Vec<u32> = TABLE.iter().map(|r| r.0).collect();
    let all: Vec<u32> = ops::ALL.iter().map(|r| r.0).collect();
    assert_eq!(listed, all, "the table follows the SDK's op table row by row");
    let fated: Vec<u32> = FATES.iter().map(|r| r.0).take(all.len()).collect();
    assert_eq!(fated, all, "the SDK's conformance table gives every op its fate (the six are kept)");
    assert_eq!(all.len(), 6, "the contract is the six ops");
}

#[test]
fn the_signed_gunnar_and_holger_guests_answer_the_op_table_as_written() {
    let gunnar = built("GUNNAR_GUEST_WASM", "gunnar_monetize_guest", "../../../gunnar/crates/gunnar-monetize-guest/target");
    let holger = built("HOLGER_GUEST_WASM", "holger_monetize_guest", "../../../holger/target");
    // The grants the ONE box's feed gives each (private-monetize-ops solo.toml): none.
    let (g_reg, g_dir) = load("gunnar", &gunnar, NO_GRANTS);
    let (h_reg, h_dir) = load("holger", &holger, NO_GRANTS);
    let g = observe(&g_reg, "gunnar");
    let h = observe(&h_reg, "holger");
    let mut table = String::from("op   name                 gunnar   holger   fate\n");
    let mut wrong = Vec::new();
    for (i, (op, want_g, want_h)) in TABLE.iter().enumerate() {
        let (gc, hc) = (g[i].1, h[i].1);
        let fate = fate(*op).map(|f| format!("{f:?}")).unwrap_or_else(|| "?".into());
        table.push_str(&format!("{op:<4} {:<20} {:<8} {:<8} {fate}\n", ops::name(*op).unwrap_or("?"), format!("{gc:?}"), format!("{hc:?}")));
        if gc != *want_g {
            wrong.push(format!("op {op} ({}) gunnar: {gc:?}, the table says {want_g:?} — {}", ops::name(*op).unwrap_or("?"), g[i].2));
        }
        if hc != *want_h {
            wrong.push(format!("op {op} ({}) holger: {hc:?}, the table says {want_h:?} — {}", ops::name(*op).unwrap_or("?"), h[i].2));
        }
    }
    // Every number that left the contract: the guests no longer have it.
    for (product, reg) in [("gunnar", &g_reg), ("holger", &h_reg)] {
        let guest = reg.guest(product).unwrap();
        assert!(guest.manifest().grants.is_empty() && guest.linker_defines().iter().all(|d| ["host_clock", "host_log", "host_sleep"].contains(d)), "{product}: a product guest opens no door: {:?}", guest.linker_defines());
        for (op, name) in ops::RETIRED {
            match guest.call_json(*op, &serde_json::Value::Null) {
                Err(e) if ops::is_unknown_op(&e.to_string()) => {}
                other => wrong.push(format!("op {op} ({name}) {product}: still answered: {other:?}")),
            }
        }
    }
    println!("{table}");
    let _ = (std::fs::remove_dir_all(g_dir), std::fs::remove_dir_all(h_dir));
    assert!(wrong.is_empty(), "the signed guests answer differently from the table (fix the guest or the table, by row):\n{}\n\n{table}", wrong.join("\n"));
}

/// ★ **v2.0 P3 (A28 H11, H12): each signed guest declares its own contract and sale proof.**
/// Its `lifecycle.facts` answer (op 90) carries every field the shop's pairing speaks for its
/// estates (the variants, the box kind, the free switch, the welcome, the pay page, the console
/// bearer's key, the off-site copy, the boxes' bearer and passkey keys) and the sale proof —
/// exactly as the TEST stand-ins the shop's own tests sell on say them, so the core's tests
/// and the product's guest cannot drift apart. And the front is upgraded LAST (A28 H17).
#[test]
fn the_signed_guests_declare_their_contract_and_sale_proof() {
    use monetize_product_guest::facts::Facts;
    let gunnar = built("GUNNAR_GUEST_WASM", "gunnar_monetize_guest", "../../../gunnar/crates/gunnar-monetize-guest/target");
    let holger = built("HOLGER_GUEST_WASM", "holger_monetize_guest", "../../../holger/target");
    let contract = |f: &Facts| Facts {
        billing: f.billing,
        offsite: f.offsite,
        boxes_hold_console_token: f.boxes_hold_console_token,
        welcome: f.welcome,
        welcome_readme: f.welcome_readme.clone(),
        sold_variant: f.sold_variant.clone(),
        site_variant: f.site_variant.clone(),
        box_kind: f.box_kind.clone(),
        free_feed: f.free_feed.clone(),
        console_token_feed: f.console_token_feed.clone(),
        passkey_rp_feed: f.passkey_rp_feed.clone(),
        // HOL-PUBSITE-FEED: the refeed of the public-site switch is sold on these facts too.
        public_site: f.public_site,
        reserved_accounts: f.reserved_accounts.clone(),
        proof: f.proof.clone(),
        kind: f.kind.clone(),
        ..Facts::default()
    };
    let want = [("gunnar", monetize_server::testing::lifecycles::gunnar_facts()), ("holger", monetize_server::testing::lifecycles::holger_facts())];
    // What ops 40, 54 and 60 answered now rides in the facts: Gunnar's main mints tickets,
    // Holger's does not; both declare test buttons and an estate guardian.
    for ((product, wasm, tickets), (_, fixture)) in [("gunnar", &gunnar, true), ("holger", &holger, false)].into_iter().zip(want) {
        let (reg, dir) = load_in("facts", product, wasm, NO_GRANTS);
        let answer = reg.guest(product).unwrap().call_json(ops::LIFECYCLE_FACTS, &request(ops::LIFECYCLE_FACTS)).unwrap_or_else(|e| panic!("{product}: lifecycle.facts: {e}"));
        let facts: Facts = serde_json::from_value(answer).unwrap_or_else(|e| panic!("{product}: its facts: {e}"));
        assert_eq!(facts.kind, product);
        assert_eq!(contract(&facts), contract(&fixture), "{product}: the guest's contract and sale proof are the ones the shop's tests sell on");
        assert!(facts.front_is_last(), "{product}: the front is upgraded LAST: {:?} (front {:?})", facts.update_order, facts.front_role);
        assert_eq!(facts.tickets, tickets, "{product}: facts.tickets (op 40, merged)");
        assert!(!facts.test_actions.is_empty() && facts.guardian, "{product}: the test buttons (op 54) and the guardian (op 60), merged: {:?}", facts.test_actions);
        let p = reg.find_product(product).unwrap();
        assert_eq!(p.tenant_renewal(), tickets, "{product}: the host reads tenant_renewal from the facts");
        assert_eq!(p.test_actions(), facts.test_actions, "{product}: and the test buttons");
        assert!(p.guardian().is_some(), "{product}: and the guardian");
        // ★ P3-FACTS: the primary, the roles, the twin and the grow rule are NAMED in the
        // guest's facts, and they are what the core derived before (today's values unchanged):
        // main, front/main/twin, the twin mirroring main, main's data never shrinking.
        assert_eq!(facts.primary_role, "main", "{product}: the primary is named");
        assert_eq!(facts.roles.iter().map(|r| r.role.as_str()).collect::<Vec<_>>(), ["front", "main", "twin"], "{product}: its roles are named");
        assert!(facts.roles.iter().all(|r| !r.many && !r.optional), "{product}: each role laid once");
        assert_eq!(facts.twin, Some(monetize_product_guest::facts::TwinFacts { role: "twin".into(), mirrors: "main".into() }), "{product}: its twin is named");
        assert_eq!(facts.grow, Some(monetize_product_guest::facts::GrowRule::sizes()), "{product}: its grow rule is named");
        let derived = Facts { primary_role: String::new(), roles: Vec::new(), twin: None, grow: None, ..facts.clone() };
        assert_eq!((facts.primary(), facts.role_names(), facts.twin_of(), facts.grow_rule()), (derived.primary(), derived.role_names(), derived.twin_of(), derived.grow_rule()), "{product}: named as the core derived them before");
        let _ = std::fs::remove_dir_all(dir);
    }
}