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_parametric_order_rides_the_wire.rs

//! ★ **The parametric order rides the wire** (monetize v2.0 P3, plan §3/§6: "the order carries
//! the product's shape as data"; the Lake's P3_WAITS row 1). `EstateOrder.Start` carries the
//! product's own order in `EstateSizes.order` (JSON); the ONE box checks it against the
//! product's facts' `form` before anything is priced or held, fills in every default, keeps the
//! filled order with the run, and the product's own plan lays it: a Lake of three backends,
//! three frontends and a graph database is laid as those machines. An order out of the form's
//! range, an order that is not a JSON object, or an order for a product with no form of its
//! own is refused by name, and nothing is held.
//!
//! The product is the SIGNED Lake guest (`cargo xtask build-guests`), on the open dummies (the
//! sandbox vendor and cloud); the call is the gRPC handler itself, with the proto's message.

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

use monetize_proto::estate_order_server::EstateOrder as _;
use monetize_proto::{self as pb};
use monetize_server::config::Config;
use monetize_server::custody::Custody;
use monetize_server::estate_order::{self, EstateOrderConfig, EstateOrders, FileDns, InstallRequest, InstallVerdict, Installer, SaleCheckAt, VERDICT_FILE};
use monetize_server::registry::Registry;
use monetize_server::services::estate_order::EstateOrderService;
use monetize_server::state::AppState;

const BUILD_CMD: &str = "cargo xtask build-guests   (in monetize)";
const WELCOME: &str = "https://lake.mock.invalid/welcome?session={session}";

fn scratch(name: &str) -> PathBuf {
    let p = std::env::temp_dir().join(format!("monetize-parametric-order-{name}-{}", std::process::id()));
    let _ = std::fs::remove_dir_all(&p);
    std::fs::create_dir_all(&p).unwrap();
    p
}

/// The Lake guest beside the config, signed under a CA of its own, and that root.
fn signed_lake_guest(dir: &Path) -> (PathBuf, PathBuf) {
    use guest_host::verify::sign::{self, dev};
    let target = std::env::var("CARGO_TARGET_DIR").map(PathBuf::from).unwrap_or_else(|_| Path::new(env!("CARGO_MANIFEST_DIR")).join("../../target"));
    let built = target.join("wasm32-unknown-unknown/release/monetize_guest_lake.wasm");
    assert!(built.is_file(), "{} has not been built. This test loads the REAL Lake guest; it does not skip.\nBuild it with:\n    {BUILD_CMD}", built.display());
    let module = dir.join("plugin.lake.wasm");
    std::fs::copy(&built, &module).unwrap();
    let (ca_key, ca_der) = dev::mint_ca("plugin-root").unwrap();
    let root = dir.join("plugin-root.der");
    std::fs::write(&root, &ca_der).unwrap();
    sign::sign_module_file(&module, &dev::new_ed25519_signer(&ca_key, &ca_der, "plugin-signing").unwrap()).unwrap();
    (module, root)
}

/// The stand-in installer: nothing boots here.
struct StandIn;

impl Installer for StandIn {
    fn install(&self, req: &InstallRequest) -> Result<InstallVerdict, String> {
        let v = InstallVerdict { verdict: "PASS".into(), detail: "stand-in".into(), receipt: serde_json::json!({"verdict": "PASS"}), peers: Vec::new(), appliance_keys: Default::default(), ticket_key: String::new() };
        std::fs::write(req.run.join(VERDICT_FILE), serde_json::to_vec(&v).unwrap()).map_err(|e| e.to_string())?;
        Ok(v)
    }
    fn sale_check(&self, at: &SaleCheckAt<'_>) -> Result<String, String> {
        Ok(format!("stand-in: https://{} is not read here", at.host))
    }
}

/// A shop selling the signed Lake on the open dummies, with an account plan (a visitor's order
/// makes its account when paid).
fn shop(dir: &Path) -> Arc<AppState> {
    let (module, root) = signed_lake_guest(dir);
    let plans = ["STARTER-1xCPU-2GB", "STARTER-1xCPU-4GB", "STARTER-2xCPU-4GB", "STARTER-4xCPU-8GB", "STARTER-4xCPU-16GB"].iter().map(|p| format!("{p:?} = 1000")).collect::<Vec<_>>().join(", ");
    let toml = format!(
        "data_dir = {:?}\n\
         [products.lake]\ninstance = \"lakes\"\nguest = {:?}\nguest_trust_roots = [{:?}]\ngrants = []\n\
         [signups.lake.plans.\"lake/account\"]\namount_minor = 0\ncurrency = \"Eur\"\ncap_bytes = 0\nperiod_days = 0\n\
         [vendors.sandbox.play]\nenabled = true\n\
         [clouds.sandbox]\nenabled = true\ncap = {{ total_minor = 10000000, currency = \"Eur\" }}\nzone = \"se-sto1\"\npool_gib = 0\ndisk_step_gib = 10\n\
         prices = {{ disk_minor_per_gib_month = 23, plan_minor_per_month = {{ {plans} }}, currency = \"Eur\" }}\n",
        dir.join("data").display().to_string(),
        module.display().to_string(),
        root.display().to_string()
    );
    let p = dir.join("monetize.toml");
    std::fs::write(&p, &toml).unwrap();
    let cfg = Config::load(&p).unwrap_or_else(|e| panic!("{}: {e}\n{toml}", p.display()));
    let registry = Registry::from_config(&cfg).expect("the open build loads its dummies and the signed Lake guest");
    let mut state = AppState::in_memory(registry);
    state.signup_plans = monetize_server::signup::Plans::of(cfg.signup_plans(), cfg.signup_cap_meters());
    let orders = EstateOrderConfig {
        product: "lake".into(),
        vendor: "sandbox/play".into(),
        cloud: "sandbox".into(),
        apex: "lake.mock.invalid".into(),
        currency: "EUR".into(),
        zone: "se-sto1".into(),
        shop_door: "https://one.mock.invalid".into(),
        media: [("lake".to_string(), "lake-r1".to_string()), ("lake-keep".to_string(), "lake-keep-r1".to_string())].into_iter().collect(),
        ..EstateOrderConfig::default()
    };
    state.estates = EstateOrders::with(orders, dir.join("estate-orders"), Arc::new(StandIn), Arc::new(FileDns(dir.join("dns.json"))), Custody::open(dir.join("custody")).unwrap());
    let state = Arc::new(state);
    let lines = estate_order::guest_lifecycle::follow(&state.registry);
    assert!(lines.iter().any(|l| l.starts_with("lifecycle lake: taken from the guest of [products.lake]")), "{lines:#?}");
    state
}

/// A visitor's order for `company` with the product's own `order` on the wire.
fn start(company: &str, account: &str, order: &str) -> pb::EstateOrderStartRequest {
    pb::EstateOrderStartRequest {
        company: company.into(),
        email: format!("{account}@mock.invalid"),
        account: account.into(),
        new_account: true,
        welcome_url: WELCOME.into(),
        sizes: Some(pb::EstateSizes { order: order.into(), ..Default::default() }),
        ..Default::default()
    }
}

#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn the_lakes_own_order_is_checked_filled_in_kept_and_laid_by_its_plan() {
    let dir = scratch("lake");
    let state = shop(&dir);
    let svc = EstateOrderService(state.clone());

    // ── P3-FACTS: the Lake's primary and roles are its facts ─────────────────────────────
    // Its FE leader is the primary (where its tenants are, the entitlements the shop queues go
    // to; never the first data role, `be`), and its five roles, the optional graph database
    // among them (never only those its update order and data roles happen to name).
    let life = estate_order::lifecycle::find("lake").expect("the Lake's lifecycle, from its guest");
    assert_eq!(life.primary_role(), "fe", "the FE leader holds the Lake's tenants");
    assert_eq!(life.roles(), ["front", "ingestor", "be", "fe", "graph"], "every role the Lake's plan may lay");

    // ── the buyer's own numbers ride the wire, are checked, filled in and priced ──────────
    let started = svc.start(tonic::Request::new(start("acme", "alice", r#"{"be": 3, "fe": 3, "graph": true}"#))).await.unwrap_or_else(|e| panic!("a Lake order of the buyer's own numbers starts: {e:?}")).into_inner();
    assert!(!started.reference.is_empty(), "{started:?}");
    let run = state.estates.get("acme").expect("the run");
    assert_eq!(
        run.quote.sizes.order,
        serde_json::json!({"store_gib": 100, "ingestors": 1, "ingestor_plan": "STARTER-1xCPU-2GB", "be": 3, "fe": 3, "graph": true}),
        "the order as the product's form fills it in, kept with the run"
    );
    let roles: Vec<&str> = run.quote.machines.iter().map(|m| m.role.as_str()).collect();
    assert_eq!(roles, ["front", "ingestor", "be", "be-2", "be-3", "fe", "fe-2", "fe-3", "graph"], "the product's plan lays the order, priced as laid");

    // ── what the form refuses is refused by name, before anything is held ─────────────────
    for (company, order, words) in [
        ("bolt", r#"{"fe": 2}"#, "one of 1, 3, 5"),
        ("cove", r#"{"store_gib": 150}"#, "steps of 100"),
        ("dune", r#"{"replicas": 2}"#, "names `replicas`"),
        ("echo", "[1, 2]", "a set of named values"),
        ("fjord", "not json", "not JSON"),
    ] {
        let e = svc.start(tonic::Request::new(start(company, company, order))).await.expect_err(company);
        assert_eq!(e.code(), tonic::Code::InvalidArgument, "{company}: {e:?}");
        assert!(e.message().contains(words), "{company}: {}", e.message());
        assert!(state.estates.get(company).is_none(), "{company}: nothing is held for an order the form refuses");
    }

    let _ = std::fs::remove_dir_all(&dir);
}

/// ★ **A product with no order form of its own** is sized by its three plan fields: an order
/// for it is refused by name, and with none its sizes stay as they were.
#[test]
fn a_product_with_no_form_takes_no_order_of_its_own() {
    use monetize_server::estate_order::price::{order_from_wire, order_of, order_to_wire};
    use monetize_server::estate_order::EstateSizes;
    let gunnar = monetize_server::testing::lifecycles::gunnar_facts();
    assert!(gunnar.form.is_none(), "Gunnar is sized by its plans");
    let plain = EstateSizes { main_plan: "STARTER-4xCPU-8GB".into(), main_data_gib: 20, ..Default::default() };
    assert_eq!(order_of(&gunnar, &plain).unwrap(), plain, "no order: the sizes as they were");
    let asked = EstateSizes { order: serde_json::json!({"be": 3}), ..plain };
    assert!(order_of(&gunnar, &asked).unwrap_err().contains("takes no order of its own"));
    // The wire's words: a JSON object or nothing, and back.
    assert_eq!(order_from_wire("  ").unwrap(), serde_json::Value::Null);
    let v = order_from_wire(r#"{"be":3}"#).unwrap();
    assert_eq!(order_from_wire(&order_to_wire(&v)).unwrap(), v);
    assert_eq!(order_to_wire(&serde_json::Value::Null), "");
}