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