crates/monetize-admin/tests/site_buy_trials.rs
//! ★ **The buy trials on the ONE box's own program** (A15, moved onto `monetize_admin::linked()`
//! in v2.0 phase 1: "tests test the code that runs in production").
//!
//! The ONE box here is what `monetize-admin server` runs: the registry is
//! [`monetize_admin::linked()`] exactly, its state opened by `monetize_server::cli::open_state`
//! from the `monetize.toml` that `monetize_front::intake::apply_pull` writes out of a FEED shaped
//! as the ONE box's installer feeds it (private-monetize-ops `solo::feed_of`): the SIGNED guests
//! (`vendor.stripe.<instance>.*`, `cloud.upcloud.*`, `dns_guest.cloudflare.*`,
//! `plugin.gunnar.*`, `plugin.holger.*`), each signed in this test under a run CA fed as the
//! plugin root, the secrets by name, and the operator's fragment. The products are PULLED
//! (`[products.<id>] instance = "<site estate>"`, no endpoint): the sites enrol over the box's
//! own attach door (`monetize_front::attach`), and their boxes, played by this test, report
//! over `Fleet` (the product part: tenants, names, servable, room) and pull, verify and ack
//! the signed orders, as a pulled estate's boxes do. The box's own loops run as `serve` runs
//! them (the estate orders, the guardian, the poller, the grow batcher), only faster.
//!
//! The fakes are the EDGES only, each a separate thing the box dials as it dials the real one:
//! the fake Stripe as a PROCESS (monetize-impl `fake_stripe_serve`, its control door the
//! buyer), mock-upcloud in process (`mock_upcloud::serve`), a fake Cloudflare API and a fake
//! SMTP relay (STARTTLS under a CA fed as `secret.smtp_ca`).
//!
//! **The ONE stand-in is the installer**: real boxes do not boot here, so one extra installer
//! ([`STAND_IN`]) is registered on top of `linked()` and named by `[estate_orders] pairing`; it
//! writes a PASS verdict for what was laid, and its sale check proves nothing it reads. The
//! real pairing (`monetize-shop-pairing`, what `linked()` registers) is the fast test's
//! (`monetize-fast`).
//!
//! | trial | relay paths | end state |
//! |---|---|---|
//! | a free account | `/auth/signup/{ladder,start,status}` | the 0 kr card check, the account made |
//! | b paid plan | `/auth/signup/{ladder,start,status}` | paid, the account made |
//! | c private instance, visitor | `/auth/private/start`, `/auth/signup/status`, `/auth/private/status` | the new account made, the instance ready |
//! | d private instance, signed in | `/auth/private/start`, `/auth/private/status` | the instance ready, no e-mail sent |
//! | e grow the account | `/api/account/subscription/{bundles,buy-bundle,status}` | paid, the room granted |
//! | f grow the instance | `/auth/private/{ladder,change,status}`, `/api/account/purchases` | the change paid |
//!
//! Inputs, by environment (feature `trials`; a missing input FAILS by name, never a skip):
//! `MONETIZE_TRIAL_GUESTS` — a directory with `stripe.wasm`, `upcloud.wasm`, `cloudflare.wasm`,
//! `gunnar.wasm`, `holger.wasm` (unsigned; signed here); `MONETIZE_FAKE_STRIPE` — the
//! `fake_stripe_serve` binary. `cargo xtask trials` builds both and runs this. With
//! `MONETIZE_TRIAL_OUT=<dir>` each trial writes its verdict there (`<trial>-<product>.json`).
//! MOCK ONLY: test keys, loopback everything, `*@mock.invalid` addresses.
#![cfg(feature = "trials")]
mod common;
use common::*;
use std::collections::{BTreeMap, BTreeSet};
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
use base64::Engine as _;
use ed25519_dalek::{Signer as _, SigningKey, VerifyingKey};
use monetize::embed::signing::base64_encode;
use monetize::embed::ticket::{ticket_message, ActorTicket};
use monetize_embed::fleet::{
fleet_call_message, verify_order, FleetCall, OrderAck, OrdersAfter, Report,
};
use monetize_server::estate_order::phase;
use monetize_server::registry::InstallerFactory;
use monetize_server::state::AppState;
use monetize_server::testing::{http_post, TestServer};
use monetize_site_buy::testkit::{Shape, TestSite};
use monetize_site_buy::Relay;
use serde_json::{json, Value};
use tower::ServiceExt as _;
const SK: &str = "sk_test_SITEbuyTRIALSneverLIVE";
const WHSEC: &str = "whsec_test_SITEbuyTRIALS";
const UPCLOUD_TOKEN: &str = "ucat_mock_trials_not_a_real_token";
const CF_TOKEN: &str = "cf_mock_trials_not_a_real_token";
const CF_ZONE: &str = "zone-trials";
/// The fake zone's own name: every site of the trials is under it.
const CF_ZONE_NAME: &str = "mock.invalid";
const ZONE: &str = "se-sto1";
const GIB: u64 = 1 << 30;
/// One ONE box at a time is built: `linked()` reads its data root from the process's
/// environment (`MONETIZE_ADMIN_DATA`).
static BUILDING: Mutex<()> = Mutex::new(());
// ── the edges: the fake Stripe (a process), the fake Cloudflare, the fake relay ─────────────
/// **The fake Cloudflare API v4** (the DNS guest's `api` on loopback): the zone named
/// ([`CF_ZONE_NAME`], which the guest asks before it writes: a name outside it is refused),
/// `dns_records` listed, made, replaced and removed, every record kept.
#[derive(Clone, Default)]
struct FakeCloudflare {
rows: Arc<Mutex<Vec<Value>>>,
next: Arc<std::sync::atomic::AtomicU64>,
}
impl FakeCloudflare {
async fn serve(&self) -> u16 {
use axum::extract::{Path as P, Query, State};
use axum::routing::{get, put};
async fn zone(P(z): P<String>) -> axum::Json<Value> {
axum::Json(
json!({"success": true, "result": {"id": z, "name": CF_ZONE_NAME}, "errors": []}),
)
}
async fn list(
State(f): State<FakeCloudflare>,
P(_z): P<String>,
Query(q): Query<BTreeMap<String, String>>,
) -> axum::Json<Value> {
let rows: Vec<Value> = f
.rows
.lock()
.unwrap()
.iter()
.filter(|r| {
["name", "type", "comment"]
.iter()
.all(|k| q.get(*k).is_none_or(|v| r[*k] == v.as_str()))
})
.cloned()
.collect();
axum::Json(json!({"success": true, "result": rows, "errors": []}))
}
async fn make(
State(f): State<FakeCloudflare>,
P(_z): P<String>,
axum::Json(mut v): axum::Json<Value>,
) -> axum::Json<Value> {
v["id"] = json!(format!(
"rec{}",
f.next.fetch_add(1, std::sync::atomic::Ordering::SeqCst)
));
f.rows.lock().unwrap().push(v.clone());
axum::Json(json!({"success": true, "result": v, "errors": []}))
}
async fn replace(
State(f): State<FakeCloudflare>,
P((_z, id)): P<(String, String)>,
axum::Json(mut v): axum::Json<Value>,
) -> axum::Json<Value> {
v["id"] = json!(id);
let mut rows = f.rows.lock().unwrap();
rows.retain(|r| r["id"] != id.as_str());
rows.push(v.clone());
axum::Json(json!({"success": true, "result": v, "errors": []}))
}
async fn remove(
State(f): State<FakeCloudflare>,
P((_z, id)): P<(String, String)>,
) -> axum::Json<Value> {
f.rows.lock().unwrap().retain(|r| r["id"] != id.as_str());
axum::Json(json!({"success": true, "result": {"id": id}, "errors": []}))
}
let app = axum::Router::new()
.route("/client/v4/zones/{zone}", get(zone))
.route("/client/v4/zones/{zone}/dns_records", get(list).post(make))
.route(
"/client/v4/zones/{zone}/dns_records/{id}",
put(replace).delete(remove),
)
.with_state(self.clone());
let l = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let port = l.local_addr().unwrap().port();
tokio::spawn(async move {
let _ = axum::serve(l, app).await;
});
port
}
}
/// **The fake SMTP relay** (STARTTLS required, as the box's relay is): every mail kept as
/// (to, decoded body). Its certificate is for 127.0.0.1 under a CA fed as `secret.smtp_ca`.
#[derive(Clone, Default)]
struct FakeRelay {
mails: Arc<Mutex<Vec<(String, String)>>>,
}
impl FakeRelay {
/// Serve; answers the port and the CA's PEM.
async fn serve(&self) -> (u16, String) {
let _ = rustls::crypto::ring::default_provider().install_default();
let ca = nordisk_pairing::mtls::InstallerCa::mint(
"site-buy-trials-smtp",
Duration::from_secs(86_400),
)
.unwrap();
let door = ca
.issue_door(std::net::Ipv4Addr::LOCALHOST, Duration::from_secs(86_400))
.unwrap();
use rustls::pki_types::pem::PemObject as _;
let certs: Vec<rustls::pki_types::CertificateDer<'static>> =
rustls::pki_types::CertificateDer::pem_slice_iter(door.cert_pem.as_bytes())
.map(|c| c.unwrap())
.collect();
let key =
rustls::pki_types::PrivateKeyDer::from_pem_slice(door.key_pem.as_bytes()).unwrap();
let cfg = rustls::ServerConfig::builder()
.with_no_client_auth()
.with_single_cert(certs, key)
.unwrap();
let acceptor = tokio_rustls::TlsAcceptor::from(Arc::new(cfg));
let l = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let port = l.local_addr().unwrap().port();
let me = self.clone();
tokio::spawn(async move {
while let Ok((tcp, _)) = l.accept().await {
let (me, acceptor) = (me.clone(), acceptor.clone());
tokio::spawn(async move {
let _ = me.converse(tcp, acceptor).await;
});
}
});
(port, ca.cert_pem)
}
async fn converse(
&self,
tcp: tokio::net::TcpStream,
acceptor: tokio_rustls::TlsAcceptor,
) -> std::io::Result<()> {
use tokio::io::{AsyncBufReadExt as _, AsyncWriteExt as _, BufReader};
let mut plain = BufReader::new(tcp);
plain
.get_mut()
.write_all(b"220 relay.mock.invalid ESMTP\r\n")
.await?;
loop {
let mut line = String::new();
if plain.read_line(&mut line).await? == 0 {
return Ok(());
}
let up = line.trim().to_ascii_uppercase();
if up.starts_with("EHLO") || up.starts_with("HELO") {
plain
.get_mut()
.write_all(b"250-relay.mock.invalid\r\n250 STARTTLS\r\n")
.await?;
} else if up == "STARTTLS" {
plain.get_mut().write_all(b"220 go ahead\r\n").await?;
break;
} else if up == "QUIT" {
plain.get_mut().write_all(b"221 bye\r\n").await?;
return Ok(());
} else {
plain.get_mut().write_all(b"530 STARTTLS first\r\n").await?;
}
}
let tls = acceptor.accept(plain.into_inner()).await?;
let mut s = BufReader::new(tls);
let (mut to, mut data, mut in_data) = (Vec::<String>::new(), String::new(), false);
loop {
let mut line = String::new();
if s.read_line(&mut line).await? == 0 {
return Ok(());
}
if in_data {
if line == ".\r\n" || line == ".\n" {
in_data = false;
let body = decode_mail(&data);
for t in to.drain(..) {
self.mails.lock().unwrap().push((t, body.clone()));
}
data.clear();
s.get_mut().write_all(b"250 queued\r\n").await?;
} else {
data.push_str(
line.strip_prefix('.')
.filter(|_| line.starts_with(".."))
.unwrap_or(&line),
);
}
continue;
}
let up = line.trim().to_ascii_uppercase();
let answer: &[u8] = if up.starts_with("EHLO") || up.starts_with("HELO") {
b"250-relay.mock.invalid\r\n250-AUTH PLAIN LOGIN\r\n250 8BITMIME\r\n"
} else if up.starts_with("AUTH") {
b"235 accepted\r\n"
} else if up.starts_with("MAIL FROM") {
b"250 ok\r\n"
} else if up.starts_with("RCPT TO") {
let addr = line.trim()[8..]
.trim()
.trim_start_matches(':')
.trim()
.trim_start_matches('<')
.split('>')
.next()
.unwrap_or_default()
.to_owned();
to.push(addr);
b"250 ok\r\n"
} else if up == "DATA" {
in_data = true;
b"354 go on\r\n"
} else if up == "QUIT" {
s.get_mut().write_all(b"221 bye\r\n").await?;
return Ok(());
} else {
b"250 ok\r\n"
};
s.get_mut().write_all(answer).await?;
}
}
fn mails(&self) -> Vec<(String, String)> {
self.mails.lock().unwrap().clone()
}
}
/// A mail's text: its headers dropped, a quoted-printable or base64 body decoded (every part).
fn decode_mail(raw: &str) -> String {
let raw = raw.replace("\r\n", "\n");
let mut out = String::new();
let mut enc = String::new();
let mut in_head = true;
let mut part = String::new();
let flush = |enc: &str, part: &str, out: &mut String| {
let text = match enc {
"quoted-printable" => {
let joined = part.replace("=\n", "");
let b = joined.as_bytes();
let mut v = Vec::new();
let mut i = 0;
while i < b.len() {
let pair = (b[i] == b'=' && i + 2 < b.len())
.then(|| {
std::str::from_utf8(&b[i + 1..i + 3])
.ok()
.and_then(|h| u8::from_str_radix(h, 16).ok())
})
.flatten();
match pair {
Some(x) => {
v.push(x);
i += 3;
}
None => {
v.push(b[i]);
i += 1;
}
}
}
String::from_utf8_lossy(&v).to_string()
}
"base64" => base64::engine::general_purpose::STANDARD
.decode(part.split_whitespace().collect::<String>())
.map(|b| String::from_utf8_lossy(&b).to_string())
.unwrap_or_else(|_| part.to_owned()),
_ => part.to_owned(),
};
out.push_str(&text);
out.push('\n');
};
for line in raw.lines() {
if line.starts_with("--") && !in_head {
flush(&enc, &part, &mut out);
part.clear();
enc.clear();
in_head = true;
continue;
}
if in_head {
if line.is_empty() {
in_head = false;
} else if let Some(v) = line
.to_ascii_lowercase()
.strip_prefix("content-transfer-encoding:")
{
enc = v.trim().to_owned();
}
continue;
}
part.push_str(line);
part.push('\n');
}
flush(&enc, &part, &mut out);
out
}
// ── the product's boxes, played by the test ─────────────────────────────────────────────────
/// **One box of a pulled estate**: it reports over `Fleet` (its health, its acks, and on a
/// site's main the product part), pulls the orders queued for it, verifies each under the
/// product's key from monetize (`monetize_pubkey`) and runs it, as a real box does.
struct TrialBox {
product: String,
instance: String,
role: String,
key: SigningKey,
/// Send its own key on every call (a laid box pins it inside its window).
pin: bool,
monetize: VerifyingKey,
health: BTreeMap<String, String>,
last_seq: u64,
acks: Vec<OrderAck>,
/// The kinds it ran; `refused:<why>` for an order that did not verify.
ran: Vec<String>,
/// On a site's main: the product part of its report.
site: Option<SiteMain>,
}
/// What a site's main holds of its product: its accounts and their caps.
#[derive(Default)]
struct SiteMain {
accounts: BTreeSet<String>,
caps: BTreeMap<String, BTreeMap<String, u64>>,
}
impl TrialBox {
fn signed(&self, call: &str, body: String, now: u64) -> monetize_proto::FleetSigned {
let c = FleetCall {
product: self.product.clone(),
instance: self.instance.clone(),
role: self.role.clone(),
call: call.into(),
body,
at_unix_ms: now,
};
let signature = self.key.sign(&fleet_call_message(&c)).to_bytes().to_vec();
monetize_proto::FleetSigned {
product: c.product,
instance: c.instance,
role: c.role,
call: c.call,
body: c.body,
at_unix_ms: now,
signature,
box_key: if self.pin {
self.key.verifying_key().as_bytes().to_vec()
} else {
Vec::new()
},
}
}
fn report(&mut self, now: u64) -> monetize_proto::FleetSigned {
let product = self.site.as_ref().map(|s| {
let room: BTreeMap<String, u64> = ["pack_bytes", "disk_bytes"]
.iter()
.map(|m| (m.to_string(), 300 * GIB))
.collect();
let pr = monetize_product::pull::ProductReport {
tenants: s
.accounts
.iter()
.map(|a| monetize_product::TenantOwner {
tenant: monetize_product::TenantId(a.clone()),
person: true,
seller: false,
})
.collect(),
usage: s
.accounts
.iter()
.map(|a| {
(
a.clone(),
monetize_product::Usage {
meters: Default::default(),
measured_at_unix_ms: now,
},
)
})
.collect(),
servable: Some(monetize_product::Servable::Measured {
servable_bytes: 300 * GIB,
total_bytes: 400 * GIB,
growth: monetize_product::Growth::Unknown(
"the trial's box does not say".into(),
),
ceilings: Default::default(),
}),
room,
promised: s.caps.clone(),
names: s.accounts.iter().cloned().collect(),
measured_unix_ms: now,
..Default::default()
};
serde_json::to_string(&pr).unwrap()
});
let r = Report {
build: "site-buy-trials".into(),
health: self.health.clone(),
acked: std::mem::take(&mut self.acks),
product,
measured_unix_ms: now,
..Default::default()
};
self.signed("report", serde_json::to_string(&r).unwrap(), now)
}
fn orders(&self, now: u64) -> monetize_proto::FleetSigned {
self.signed(
"orders",
serde_json::to_string(&OrdersAfter {
after: self.last_seq,
wait_ms: 0,
})
.unwrap(),
now,
)
}
/// Verify and run what it pulled; the acks go with its next report.
fn run(&mut self, orders: &[monetize_proto::FleetOrder], now: u64) {
for o in orders
.iter()
.map(monetize_server::services::fleet::order_of)
{
if let Err(e) = verify_order(
&o,
&self.monetize,
(&self.product, &self.instance, &self.role),
now,
self.last_seq,
) {
self.ran.push(format!("refused:{e}"));
continue;
}
self.last_seq = o.seq;
let (ok, detail) = self.verb(&o.kind, &o.body);
self.ran.push(o.kind.clone());
self.acks.push(OrderAck {
seq: o.seq,
ok,
detail: detail.clone(),
receipt_json: json!({"verb": o.kind, "ok": ok, "detail": detail}).to_string(),
});
}
}
fn verb(&mut self, kind: &str, body: &str) -> (bool, String) {
use monetize_product::pull::order;
let Some(site) = self.site.as_mut() else {
return (true, format!("the {} ran {kind}", self.role));
};
match kind {
order::ADD_PRINCIPAL => {
match serde_json::from_str::<monetize_product::pull::AddPrincipal>(body) {
Ok(a) => {
site.accounts.insert(a.username.clone());
(true, a.username)
}
Err(e) => (false, format!("not an add_principal: {e}")),
}
}
order::ENTITLEMENT => {
match serde_json::from_str::<monetize_embed::EntitlementFact>(body) {
Ok(f) => match monetize_embed::signing::verify_fact(&f, &self.monetize) {
Ok(()) => {
site.caps.insert(f.tenant.0.clone(), f.caps.clone());
(true, format!("{} holds {:?}", f.tenant.0, f.caps))
}
Err(e) => (
false,
format!("the fact does not verify under monetize's key: {e}"),
),
},
Err(e) => (false, format!("not an entitlement fact: {e}")),
}
}
other => (true, format!("the main ran {other}")),
}
}
}
/// **The boxes' pump**: every box reports and pulls every 150 ms; a laid estate's machines
/// join as their estate is known to the guardian.
async fn pump(
state: Arc<AppState>,
boxes: Arc<Mutex<Vec<TrialBox>>>,
fleet: String,
keys: Arc<dyn Fn(&str) -> VerifyingKey + Send + Sync>,
stop: Arc<std::sync::atomic::AtomicBool>,
) {
let mut seed = 100u8;
while !stop.load(std::sync::atomic::Ordering::SeqCst) {
// A laid estate's machines, once the guardian knows the estate.
if let Some(g) = state.fleet.guardian() {
for run in state.estates.all() {
let (Some(laid), Some(product)) =
(run.laid.as_ref(), g.store.product_of(&run.company))
else {
continue;
};
for m in &laid.machines {
let mut b = boxes.lock().unwrap();
if b.iter()
.any(|x| x.instance == run.company && x.role == m.role)
{
continue;
}
seed = seed.wrapping_add(1);
let mut health = BTreeMap::new();
health.insert(
"serving".to_string(),
if m.role == "main" { "yes" } else { "no" }.to_string(),
);
health.insert("data".to_string(), "ok".to_string());
b.push(TrialBox {
product: product.clone(),
instance: run.company.clone(),
role: m.role.clone(),
key: SigningKey::from_bytes(&[seed; 32]),
pin: true,
monetize: keys(&product),
health,
last_seq: 0,
acks: Vec::new(),
ran: Vec::new(),
site: None,
});
}
}
}
let n = boxes.lock().unwrap().len();
for i in 0..n {
let rep = boxes.lock().unwrap()[i].report(now_ms());
let Ok(mut c) = monetize_proto::fleet_client::FleetClient::connect(fleet.clone()).await
else {
continue;
};
if c.report(rep.clone()).await.is_err() {
// Not heard (its estate not known yet): its acks go with the next report.
if let Ok(r) = serde_json::from_str::<Report>(&rep.body) {
let mut b = boxes.lock().unwrap();
let mut back = r.acked;
back.append(&mut b[i].acks);
b[i].acks = back;
}
continue;
}
let ask = boxes.lock().unwrap()[i].orders(now_ms());
if let Ok(a) = c.orders(ask).await {
boxes.lock().unwrap()[i].run(&a.into_inner().orders, now_ms());
}
}
tokio::time::sleep(Duration::from_millis(150)).await;
}
}
// ── the ONE box ─────────────────────────────────────────────────────────────────────────────
fn site_id(shape: Shape) -> &'static str {
match shape {
Shape::Gunnar => "gunnar-mock",
Shape::Holger => "holger-mock",
}
}
/// The site's own Stripe account (`stripe/<instance>`).
fn instance(shape: Shape) -> &'static str {
shape.product()
}
fn site_key(shape: Shape) -> SigningKey {
SigningKey::from_bytes(&[if shape == Shape::Gunnar { 31u8 } else { 32u8 }; 32])
}
fn main_key(shape: Shape) -> SigningKey {
SigningKey::from_bytes(&[if shape == Shape::Gunnar { 41u8 } else { 42u8 }; 32])
}
/// The fragment the operator feeds (`monetize.toml`): the signups, the sites, the shop's
/// estate orders, the room plans — the ONE box's shape, cut to the two trial sites.
fn fragment(media: &BTreeMap<String, String>) -> String {
let mut t = String::new();
for (p, meter) in [("gunnar", "pack_bytes"), ("holger", "disk_bytes")] {
// The paid rows as the ONE box is fed them (solo.toml `[products.<p>.signup.plans]`),
// never `[pricing.<p>.plans]`: the pulled sink signs a signup's fact from
// `[signups.<p>.plans]` alone (`signup_pull::PulledSink::paid_on`), while the ladder
// also sells a priced row (`signup::plan_named`) — fed that way, a paid priced row is
// minted and paid and then REFUSED after the payment (found by this trial, 2026-10-24).
t.push_str(&format!(
"[signups.{p}]\ncap_meter = \"{meter}\"\nmax_sold_percent_of_capacity = 600\n\
[signups.{p}.plans.\"{p}/account\"]\namount_minor = 0\ncurrency = \"Sek\"\ncap_bytes = 0\nperiod_days = 0\n\
[signups.{p}.plans.\"{p}/10gb-1m\"]\namount_minor = 1000\ncurrency = \"Eur\"\ncap_bytes = 10737418240\nperiod_days = 30\n\
[signups.{p}.plans.\"{p}/10gb-6m\"]\namount_minor = 5400\ncurrency = \"Eur\"\ncap_bytes = 10737418240\nperiod_days = 180\n"
));
}
for shape in [Shape::Gunnar, Shape::Holger] {
let id = site_id(shape);
t.push_str(&format!(
"[estates.{id}]\nproduct = \"{}\"\nvendor = \"stripe/{}\"\nowner = \"ignalina\"\nsite = \"{}\"\noperator_site = true\n[estates.{id}.orders]\ndns = \"cloudflare\"\n[estates.{id}.orders.media]\n",
shape.product(),
instance(shape),
shape.host()
));
for (k, v) in media {
t.push_str(&format!("\"{k}\" = \"{v}\"\n"));
}
}
t.push_str(&format!(
"[estate_orders]\nproduct = \"gunnar\"\ncloud = \"upcloud\"\nzone = \"{ZONE}\"\ncurrency = \"EUR\"\nmax_estates = 10\nmax_open = 10\npairing = \"{STAND_IN}\"\nshop_door = \"https://one.mock.invalid\"\napex = \"estates.mock.invalid\"\nspend_cap_monthly_minor = 10000000\n[estate_orders.media]\n"
));
for (k, v) in media {
t.push_str(&format!("\"{k}\" = \"{v}\"\n"));
}
t
}
/// **The feed**, as the ONE box's installer sends it (private-monetize-ops `solo::feed_of`).
#[allow(clippy::too_many_arguments)]
fn feed(
dir: &Path,
inp: &Inputs,
stripe: &str,
upcloud: u16,
cf: u16,
smtp: (u16, &str),
media: &BTreeMap<String, String>,
) -> BTreeMap<String, String> {
use guest_host::verify::sign::dev;
let (ca_key, ca_der) = dev::mint_ca("site-buy-trials-plugin-root").unwrap();
let signer = dev::new_ed25519_signer(&ca_key, &ca_der, "site-buy-trials-signing").unwrap();
let signed =
|name: &str| sign_into(dir, &inp.guests.join(format!("{name}.wasm")), name, &signer);
let mut f = BTreeMap::new();
f.insert("plugin_root.trials".to_string(), b64(&ca_der));
let (sw, sp) = signed("stripe");
for shape in [Shape::Gunnar, Shape::Holger] {
let i = instance(shape);
let env = i.to_ascii_uppercase();
let (key, hook) = (
format!("MONETIZE_STRIPE_{env}_SECRET_KEY"),
format!("MONETIZE_STRIPE_{env}_WEBHOOK_SECRET"),
);
f.insert(format!("vendor.stripe.{i}.wasm"), b64(&sw));
f.insert(format!("vendor.stripe.{i}.p7s"), b64(&sp));
f.insert(
format!("vendor.stripe.{i}.toml"),
format!(
"enabled = true\ncountry = \"IE\"\ngrants = [\"http\", \"hmac\", \"state\"]\nhttp_hosts = [\"{stripe}\"]\nhmac_secrets = [\"{hook}\"]\nbase_url = \"{stripe}\"\nsuccess_url = \"https://{h}/paid/{{reference}}\"\ncancel_url = \"https://{h}/cancel/{{reference}}\"\nlivemode = false\ntax_code = \"txcd_10103001\"\nwebhook_secret = \"{hook}\"\n[[http_auth]]\nhost = \"{stripe}\"\nprefix = \"Bearer \"\nsecret = \"{key}\"\n",
h = shape.host()
),
);
f.insert(format!("secret.{key}"), SK.into());
f.insert(format!("secret.{hook}"), WHSEC.into());
}
let up = format!("http://127.0.0.1:{upcloud}");
let (uw, upp) = signed("upcloud");
f.insert("cloud.upcloud.wasm".into(), b64(&uw));
f.insert("cloud.upcloud.p7s".into(), b64(&upp));
f.insert(
"cloud.upcloud.toml".into(),
format!("enabled = true\ncap = {{ monthly_minor = 10000000, currency = \"Eur\" }}\ncountry = \"FI\"\nzone = \"{ZONE}\"\ngrants = [\"http\", \"state\"]\nhttp_hosts = [\"{up}\"]\napi_base = \"{up}\"\n[[http_auth]]\nhost = \"{up}\"\nprefix = \"Bearer \"\nsecret = \"upcloud_token\"\n"),
);
f.insert("secret.upcloud_token".into(), UPCLOUD_TOKEN.into());
let cfh = format!("http://127.0.0.1:{cf}");
let (cw, cp) = signed("cloudflare");
f.insert("dns_guest.cloudflare.wasm".into(), b64(&cw));
f.insert("dns_guest.cloudflare.p7s".into(), b64(&cp));
f.insert(
"dns_guest.cloudflare.toml".into(),
format!("grants = [\"http\"]\nhttp_hosts = [\"{cfh}\"]\nzone_id = \"{CF_ZONE}\"\napi = \"{cfh}/client/v4\"\n[[http_auth]]\nhost = \"{cfh}\"\nprefix = \"Bearer \"\nsecret = \"dns_cloudflare_token\"\n"),
);
f.insert("secret.dns_cloudflare_token".into(), CF_TOKEN.into());
for (p, grants, bundles) in [
("gunnar", "[]", "[bundles.\"10gb\"]\ncap_delta = { pack_bytes = 10737418240 }\n[bundles.\"50gb\"]\ncap_delta = { pack_bytes = 53687091200 }\n[bundles.\"100gb\"]\ncap_delta = { pack_bytes = 107374182400 }\n"),
("holger", "[]", "[bundles.\"5gib\"]\ncap_delta = { disk_bytes = 5368709120 }\nperiod_days = 30\n[bundles.\"10gib\"]\ncap_delta = { disk_bytes = 10737418240 }\nperiod_days = 30\n"),
] {
let (w, s) = signed(p);
f.insert(format!("plugin.{p}.wasm"), b64(&w));
f.insert(format!("plugin.{p}.p7s"), b64(&s));
let shape = if p == "gunnar" { Shape::Gunnar } else { Shape::Holger };
f.insert(format!("plugin.{p}.toml"), format!("vendor = \"stripe/{}\"\ninstance = \"{}\"\ngrants = {grants}\n{bundles}", instance(shape), site_id(shape)));
f.insert(format!("plugin.{p}.prices"), "[SE]\ndisk_gib_month = 100\n[FI]\ndisk_gib_month = 100\n".into());
}
f.insert(
"secret.smtp_url".into(),
format!("smtp://trials:not-a-real-password@127.0.0.1:{}", smtp.0),
);
f.insert(
"secret.smtp_from".into(),
"ONE box <shop@mock.invalid>".into(),
);
f.insert("secret.smtp_ca".into(), smtp.1.to_string());
f.insert("monetize.toml".into(), fragment(media));
f
}
/// **One site, attached to the ONE box** (its own attach door): its bearer and its relay.
struct Site {
bearer: String,
}
/// **The ONE box** — `monetize-admin server`'s program over the fed config — its edges, and
/// the sites' boxes.
struct Rig {
state: std::mem::ManuallyDrop<Arc<AppState>>,
server: Option<TestServer>,
loops: Vec<tokio::task::JoinHandle<()>>,
stop: Arc<std::sync::atomic::AtomicBool>,
boxes: Arc<Mutex<Vec<TrialBox>>>,
fake: FakeStripe,
relay_mail: FakeRelay,
sites: BTreeMap<&'static str, Site>,
dir: PathBuf,
}
/// The guardian's tick, the jera duty `serve` starts (P4b), held by a task of the rig's loops:
/// the loops' abort drops the task, and its guard stops the duty.
fn guardian_tick(state: &Arc<AppState>) -> tokio::task::JoinHandle<()> {
struct Stops(Arc<monetize_server::jobs::Job>);
impl Drop for Stops {
fn drop(&mut self) {
if !self.0.stop(Duration::from_secs(2)) {
eprintln!("trials: the guardian tick was still in a look at the rig's stop");
}
}
}
let tick = Stops(
monetize_server::fleet::start(state, Duration::from_millis(250))
.unwrap_or_else(|e| panic!("the guardian ticks: {e}")),
);
tokio::spawn(async move {
let _held = tick;
std::future::pending::<()>().await
})
}
async fn rig(name: &str) -> Rig {
let inp = inputs();
let dir = PathBuf::from(env!("CARGO_TARGET_TMPDIR"))
.join(format!("site-buy-trials-{name}-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(dir.join("guests")).unwrap();
let root = dir.join("data");
let fake = FakeStripe::start(&inp.fake_stripe, SK, WHSEC);
let mock = mock_upcloud::Mock::recording(mock_upcloud::estate::Estate::new(
mock_upcloud::Clock::new(1_000_000),
mock_upcloud::Faults::none(),
11,
));
// The release media the shop lays sold estates from: storages at the mock, by kind.
let media: BTreeMap<String, String> = {
let mut e = mock.estate.lock().unwrap();
[
"front",
"gunnar",
"holger",
"holger-front",
"front-keep",
"gunnar-keep",
"holger-keep",
"holger-front-keep",
]
.iter()
.map(|k| {
(
k.to_string(),
e.create_storage(&format!("{k}-r1"), 1, "maxiops", ZONE, Vec::new())
.unwrap(),
)
})
.collect()
};
let upcloud = mock_upcloud::serve(mock.clone(), "127.0.0.1:0")
.await
.unwrap()
.0;
let cf = FakeCloudflare::default().serve().await;
let relay_mail = FakeRelay::default();
let (smtp_port, smtp_ca) = relay_mail.serve().await;
let feed = feed(
&dir.join("guests"),
&inp,
&fake.base(),
upcloud,
cf,
(smtp_port, &smtp_ca),
&media,
);
let r2 = root.clone();
let state = tokio::task::spawn_blocking(move || {
let _one = BUILDING.lock().unwrap_or_else(|e| e.into_inner());
// As PID 1 does: monetize's token and key, then the feed taken in.
monetize_front::intake::prepare(&r2).unwrap();
let refused = monetize_front::intake::apply_pull(&r2, &feed).unwrap();
assert!(
refused.is_empty(),
"the ONE box refused part of the feed: {refused:#?}"
);
let path = r2.join(monetize_front::intake::CONFIG_REL);
let cfg = monetize_server::config::Config::load(&path).unwrap_or_else(|e| {
panic!(
"{}: {e}\n{}",
path.display(),
std::fs::read_to_string(&path).unwrap_or_default()
)
});
std::env::set_var(monetize_admin::DATA_ENV, &r2);
let mut linked = monetize_admin::linked();
// THE ONE STAND-IN (see the module doc).
linked.installer(InstallerFactory {
name: STAND_IN,
build: stand_in,
});
let state = monetize_server::cli::open_state(&cfg, &linked)
.unwrap_or_else(|e| panic!("the ONE box does not open: {e:#}"));
Arc::new(state)
})
.await
.unwrap();
let token = std::fs::read_to_string(root.join(monetize_front::intake::TOKEN_REL))
.unwrap()
.trim()
.to_owned();
let server = TestServer::spawn(state.clone(), &token).await;
// The loops `serve` starts, ticking faster.
monetize_server::grow::arm(&state);
monetize_server::plans::arm(&state);
let mut loops = vec![
tokio::spawn(monetize_server::estate_order::run(
state.clone(),
Duration::from_millis(200),
)),
guardian_tick(&state),
tokio::spawn(monetize_server::poller::run(
state.clone(),
Duration::from_secs(2),
)),
];
// The sites enrol at the box's attach door and name their main.
let central = monetize_front::attach::Central {
root: root.clone(),
plugin_door: format!("127.0.0.1:{}", server.addr.port()),
};
let boxes: Arc<Mutex<Vec<TrialBox>>> = Arc::default();
let mut sites = BTreeMap::new();
let signer = monetize_front::intake::signer(&root).unwrap();
for shape in [Shape::Gunnar, Shape::Holger] {
let (estate, product) = (site_id(shape), shape.product());
let token = monetize_front::attach::mint_token(
¢ral,
estate,
product,
Duration::from_secs(3600),
)
.unwrap();
let (csr, _key) = nordisk_pairing::mtls::box_request().unwrap();
let owner = ssh_key::PrivateKey::from(ssh_key::private::Ed25519Keypair::from_seed(
&[if shape == Shape::Gunnar { 51u8 } else { 52u8 }; 32],
));
let req = plugin_door_wire::attach::EnrolRequest {
v: plugin_door_wire::attach::VERSION,
token,
estate: estate.into(),
product: product.into(),
box_name: "main".into(),
csr_pem: csr,
endpoint: format!("{}:443", shape.host()),
owner_key: owner.public_key().to_openssh().unwrap(),
estate_ca_pem: String::new(),
nonce: format!("{estate}-enrol-0123456789"),
at_unix_ms: now_ms() as i64,
owner_sig: String::new(),
}
.sign(&owner)
.unwrap();
let (st, reply) = monetize_front::attach::enrol_answer(
central.clone(),
serde_json::to_vec(&req).unwrap().into(),
)
.await;
assert_eq!(st.as_u16(), 200, "{estate} enrols: {}", reply.0);
let bearer = reply.0["estate_bearer"].as_str().unwrap().to_owned();
let pubkey = reply.0["monetize_pubkey"].as_str().unwrap().to_owned();
assert_eq!(
pubkey,
hex(&monetize::Keyring::derive(&signer, product).public_key()),
"the key the estate verifies orders with is its product's"
);
let notice = plugin_door_wire::attach::InstalledNotice {
v: plugin_door_wire::attach::VERSION,
estate: estate.into(),
product: product.into(),
roles: vec!["main".into()],
nonce: format!("{estate}-installed-0123456789"),
at_unix_ms: now_ms() as i64,
ticket_key: hex(site_key(shape).verifying_key().as_bytes()),
fleet_keys: [(
"main".to_string(),
hex(main_key(shape).verifying_key().as_bytes()),
)]
.into(),
owner_sig: String::new(),
}
.sign(&owner)
.unwrap();
let (st, said) = monetize_front::attach::installed_answer(
central.clone(),
serde_json::to_vec(¬ice).unwrap().into(),
)
.await;
assert_eq!(st.as_u16(), 200, "{estate}'s install notice: {}", said.0);
let mut health = BTreeMap::new();
for (k, v) in [
("serving", "yes"),
("data", "ok"),
("serves_at", "127.0.0.1"),
("twin", "paired"),
] {
health.insert(k.to_string(), v.to_string());
}
boxes.lock().unwrap().push(TrialBox {
product: product.into(),
instance: estate.into(),
role: "main".into(),
key: main_key(shape),
pin: false,
monetize: VerifyingKey::from_bytes(
&monetize::Keyring::derive(&signer, product).public_key(),
)
.unwrap(),
health,
last_seq: 0,
acks: Vec::new(),
ran: Vec::new(),
site: Some(SiteMain::default()),
});
sites.insert(product, Site { bearer });
}
let stop = Arc::new(std::sync::atomic::AtomicBool::new(false));
let s2 = signer.clone();
let keys: Arc<dyn Fn(&str) -> VerifyingKey + Send + Sync> = Arc::new(move |p: &str| {
VerifyingKey::from_bytes(&monetize::Keyring::derive(&s2, p).public_key()).unwrap()
});
loops.push(tokio::spawn(pump(
state.clone(),
boxes.clone(),
server.endpoint(),
keys,
stop.clone(),
)));
let r = Rig {
state: std::mem::ManuallyDrop::new(state),
server: Some(server),
loops,
stop,
boxes,
fake,
relay_mail,
sites,
dir,
};
// The guardian takes the enrolments (its tick) and hears each main's first report.
let until = Instant::now() + Duration::from_secs(30);
for shape in [Shape::Gunnar, Shape::Holger] {
loop {
let heard = r
.state
.fleet
.guardian()
.and_then(|g| g.store.box_record(site_id(shape), "main"))
.is_some_and(|b| b.heard_unix_ms > 0);
if heard {
break;
}
assert!(
Instant::now() < until,
"{}'s main was not heard in 30 s: {:?}",
site_id(shape),
r.boxes
.lock()
.unwrap()
.iter()
.map(|b| (&b.instance, &b.ran))
.collect::<Vec<_>>()
);
tokio::time::sleep(Duration::from_millis(100)).await;
}
}
r
}
impl Rig {
fn addr(&self) -> std::net::SocketAddr {
self.server.as_ref().unwrap().addr
}
/// The accounts a site's main holds (what its product made from the box's orders).
fn accounts(&self, shape: Shape) -> Vec<String> {
self.boxes
.lock()
.unwrap()
.iter()
.find(|b| b.instance == site_id(shape))
.and_then(|b| b.site.as_ref())
.map(|s| s.accounts.iter().cloned().collect())
.unwrap_or_default()
}
/// **The site's relay** over this ONE box, with the bearer its enrolment gave it and its
/// tickets signed with the site's own key (delivered to the box by its install notice).
fn site(&self, shape: Shape) -> Arc<Relay<TestSite>> {
let key = site_key(shape);
let product = shape.product();
let n = Arc::new(std::sync::atomic::AtomicU64::new(0));
let bearer = self.sites[product].bearer.clone();
let boxes = self.boxes.clone();
let estate = site_id(shape);
let holds = Arc::new(move |u: &str| {
boxes.lock().unwrap().iter().any(|b| {
b.instance == estate && b.site.as_ref().is_some_and(|s| s.accounts.contains(u))
})
});
// The room an account holds is the PRODUCT's word (the real site asks its own
// product, never the ONE box's `Entitlement.Get`, which a site's bearer does not
// open — ST-13): what the site's main applied from the pulled entitlement orders.
let boxes2 = self.boxes.clone();
let held = Arc::new(move |tenant: &str| {
boxes2
.lock()
.unwrap()
.iter()
.find(|b| b.instance == estate)
.and_then(|b| b.site.as_ref())
.and_then(|s| s.caps.get(tenant).cloned())
.unwrap_or_default()
});
let site = TestSite::at(shape, &format!("http://{}", self.addr()), &bearer, holds)
.with_held(held)
.with_minter(Arc::new(move |buyer, purpose, caps| {
let nonce = format!(
"{}-{}",
purpose,
n.fetch_add(1, std::sync::atomic::Ordering::SeqCst)
);
let mut t = ActorTicket::unsigned(
product,
product,
buyer.tenant(),
purpose,
nonce,
now_ms() as i64,
300_000,
);
if !caps.is_empty() {
t = t.covering(caps.clone());
}
t.signature = base64_encode(&key.sign(&ticket_message(&t)).to_bytes());
serde_json::to_vec(&t).unwrap()
}));
let link = monetize_site_buy::ShopLink::connect_lazy(
&monetize_site_buy::Door {
url: format!("http://{}", self.addr()),
..Default::default()
},
&bearer,
product,
)
.unwrap();
Arc::new(Relay::new(site, link))
}
/// The buyer at the fake Stripe's page (`pay`, `check-card/<fp>`), and Stripe's signed event
/// delivered to the ONE box's `/callback/stripe/<instance>`.
fn buyer_does(&self, shape: Shape, what: &str, cs: &str) -> Result<(u16, String), String> {
let (st, ev) = self.fake.control("POST", &format!("/{what}"));
if st != 200 {
return Err(format!(
"the fake Stripe's page refused {what} for {cs}: {st} {ev}"
));
}
let headers = vec![
(
"Stripe-Signature".to_string(),
ev["signature"].as_str().unwrap_or_default().to_owned(),
),
("Content-Type".to_string(), "application/json".to_string()),
];
http_post(
self.addr(),
&format!("/callback/stripe/{}", instance(shape)),
&headers,
ev["body"].as_str().unwrap_or_default().as_bytes(),
)
.map_err(|e| e.to_string())
}
fn pay(&self, shape: Shape, cs: &str) -> Result<(u16, String), String> {
self.buyer_does(shape, &format!("pay/{cs}"), cs)
}
/// Until the order of `company` is `want` or no longer open (90 s): the box's own loops
/// drive it, the laid boxes report as they come up.
async fn order_until(&self, company: &str, want: &str) -> String {
let until = Instant::now() + Duration::from_secs(90);
loop {
let p = self
.state
.estates
.get(company)
.map(|r| r.phase)
.unwrap_or_default();
if p == want || (!p.is_empty() && !phase::is_open(&p)) || Instant::now() > until {
return p;
}
tokio::time::sleep(Duration::from_millis(100)).await;
}
}
fn history(&self, company: &str) -> String {
self.state
.estates
.get(company)
.map(|r| {
format!(
"why: {}; {}",
r.why,
r.history
.iter()
.map(|t| format!("{}: {}", t.phase, t.note))
.collect::<Vec<_>>()
.join(" | ")
)
})
.unwrap_or_else(|| "no order".into())
}
async fn stop(mut self) {
self.stop.store(true, std::sync::atomic::Ordering::SeqCst);
for l in self.loops.drain(..) {
l.abort();
}
if let Some(s) = self.server.take() {
s.stop().await;
}
}
}
impl Drop for Rig {
fn drop(&mut self) {
self.stop.store(true, std::sync::atomic::Ordering::SeqCst);
for l in self.loops.drain(..) {
l.abort();
}
// SAFETY: taken once, here, and never touched again. The state's blocking clients are
// dropped off the runtime.
let state = unsafe { std::mem::ManuallyDrop::take(&mut self.state) };
let server = self.server.take();
let _ = std::thread::spawn(move || drop((server, state))).join();
let _ = std::fs::remove_dir_all(&self.dir);
}
}
async fn ask(
relay: &Arc<Relay<TestSite>>,
shape: Shape,
method: &str,
uri: &str,
who: Option<&str>,
body: Option<Value>,
) -> (u16, Value) {
let mut req = axum::http::Request::builder()
.method(method)
.uri(uri)
.header("accept-language", "fi-FI")
.header("x-forwarded-for", "203.0.113.11");
if let Some(u) = who {
let (name, value) = shape.session(u);
req = req.header(name, value);
}
let req = match body {
Some(b) => req
.header("content-type", "application/json")
.body(axum::body::Body::from(b.to_string()))
.unwrap(),
None => req.body(axum::body::Body::empty()).unwrap(),
};
let res = monetize_site_buy::router(relay.clone())
.oneshot(req)
.await
.unwrap();
let status = res.status().as_u16();
let bytes = axum::body::to_bytes(res.into_body(), 1 << 20)
.await
.unwrap();
(
status,
serde_json::from_slice(&bytes).unwrap_or(Value::Null),
)
}
/// One trial's verdict, written where the runner gathers it.
fn record(trial: &str, shape: Shape, pass: bool, detail: &str, secs: f64) {
if let Some(dir) = std::env::var_os("MONETIZE_TRIAL_OUT") {
let dir = PathBuf::from(dir);
let _ = std::fs::create_dir_all(&dir);
let name = format!("{trial}-{}", shape.product());
let v = json!({"name": name, "trial": trial, "site": shape.word(), "where": "linked", "pass": pass, "detail": detail, "secs": (secs * 10.0).round() / 10.0, "at_unix": now_ms() / 1000});
let _ = std::fs::write(
dir.join(format!("{name}.json")),
serde_json::to_vec_pretty(&v).unwrap(),
);
}
}
/// Run `body`, record its verdict, then fail the test as it failed.
async fn trial<F>(name: &str, shape: Shape, body: F)
where
F: std::future::Future<Output = Result<String, String>>,
{
let t = Instant::now();
let verdict = body.await;
let secs = t.elapsed().as_secs_f64();
match &verdict {
Ok(detail) => record(name, shape, true, detail, secs),
Err(why) => record(name, shape, false, why, secs),
}
if let Err(why) = verdict {
panic!("{name} ({}): {why}", shape.word());
}
}
fn check(cond: bool, why: impl Into<String>) -> Result<(), String> {
if cond {
Ok(())
} else {
Err(why.into())
}
}
/// The signup's state as the welcome page polls it, until `want` (bounded).
async fn poll_signup(
relay: &Arc<Relay<TestSite>>,
shape: Shape,
session: &str,
want: &str,
) -> Value {
let until = Instant::now() + Duration::from_secs(30);
loop {
let (_, s) = ask(
relay,
shape,
"GET",
&format!("/auth/signup/status?session={session}"),
None,
None,
)
.await;
if s["state"] == want || Instant::now() > until {
return s;
}
tokio::time::sleep(Duration::from_millis(100)).await;
}
}
fn session_of(url: &str) -> String {
url.rsplit('/')
.next()
.unwrap_or_default()
.split('#')
.next()
.unwrap_or_default()
.split('?')
.next()
.unwrap_or_default()
.to_owned()
}
/// **A visitor's free account through the site** (`<product>/account`, the page's own row):
/// the start, Stripe's 0 kr card check, the event, the welcome page. Answers the session.
async fn make_account(
r: &Rig,
relay: &Arc<Relay<TestSite>>,
shape: Shape,
user: &str,
) -> Result<String, String> {
let (st, start) = ask(relay, shape, "POST", "/auth/signup/start", None, Some(json!({"username": user, "email": format!("{user}@mock.invalid"), "plan": format!("{}/account", shape.product()), "next": "/"}))).await;
check(st == 200, format!("the start: {st} {start}"))?;
let url = start["url"].as_str().unwrap_or_default().to_owned();
let cs = session_of(&url);
check(
r.fake.minted(&cs),
format!("the checkout is the fake Stripe's: {url}"),
)?;
let (code, said) = r.buyer_does(shape, &format!("check-card/{cs}/fp_trial_{user}"), &cs)?;
check(code == 200, format!("the event: {code} {said}"))?;
let s = poll_signup(relay, shape, &cs, "created").await;
check(s["state"] == "created", format!("the welcome page: {s}"))?;
check(
s["invite_url"]
.as_str()
.is_some_and(|u| u.starts_with("/login?signup=")),
format!("the passkey link on the site: {s}"),
)?;
Ok(cs)
}
async fn free_account(shape: Shape) -> Result<String, String> {
let r = rig(&format!("a-{}", shape.product())).await;
let relay = r.site(shape);
let (st, plans) = ask(&relay, shape, "GET", "/auth/signup/ladder", None, None).await;
check(st == 200, format!("the plans: {st} {plans}"))?;
let made = make_account(&r, &relay, shape, "freya").await;
let accounts = r.accounts(shape);
let held = made
.as_deref()
.map(|cs| r.fake.session(cs)["mode"].clone())
.unwrap_or_default();
r.stop().await;
let cs = made?;
// The pulled shape: the account is made by the site's main from its signed add_principal.
check(
accounts == ["freya"],
format!("one account made, on the site's main: {accounts:?}"),
)?;
Ok(format!(
"0 kr card check {cs} (mode {held}); account freya made on {}'s main; welcome on {}",
site_id(shape),
shape.host()
))
}
async fn paid_plan(shape: Shape) -> Result<String, String> {
let r = rig(&format!("b-{}", shape.product())).await;
let relay = r.site(shape);
let (_, plans) = ask(&relay, shape, "GET", "/auth/signup/ladder", None, None).await;
let paid = plans["plans"]
.as_array()
.and_then(|p| {
p.iter()
.find(|x| x["id"].as_str().is_some_and(|i| i.contains("10gb")))
})
.cloned()
.ok_or(format!("no paid row: {plans}"))?;
let (st, start) = ask(&relay, shape, "POST", "/auth/signup/start", None, Some(json!({"username": "paula", "email": "paula@mock.invalid", "plan": paid["id"], "next": "/"}))).await;
check(st == 200, format!("the start: {st} {start}"))?;
let cs = session_of(start["url"].as_str().unwrap_or_default());
let pending = poll_signup(&relay, shape, &cs, "pending").await;
check(
r.accounts(shape).is_empty(),
format!("no account before the payment: {pending}"),
)?;
let (code, said) = r.pay(shape, &cs)?;
check(code == 200, format!("the event: {code} {said}"))?;
let s = poll_signup(&relay, shape, &cs, "created").await;
let accounts = r.accounts(shape);
let session = r.fake.session(&cs);
r.stop().await;
check(
s["state"] == "created" && s["username"] == "paula",
format!("the welcome page: {s}"),
)?;
check(
accounts == ["paula"],
format!("the account on the site's main: {accounts:?}"),
)?;
Ok(format!(
"{} paid at the fake Stripe ({cs}, {} {}); account paula made",
paid["price_words"].as_str().unwrap_or(""),
session["amount_total"],
session["currency"]
))
}
/// The private ladder's first step, as the order's body.
fn first_step(ladder: &Value) -> Value {
ladder["steps"][0]["sizes"].clone()
}
async fn private_visitor(shape: Shape) -> Result<String, String> {
let r = rig(&format!("c-{}", shape.product())).await;
let relay = r.site(shape);
// As a page reads it (LADDER-2): a ladder not read yet is said by name, and looked at again.
let mut asked = 0;
let (st, ladder) = loop {
let (st, ladder) = ask(&relay, shape, "GET", "/auth/private/ladder", None, None).await;
asked += 1;
if !(st == 503 && ladder["not_read"] == true) || asked >= 60 {
break (st, ladder);
}
tokio::time::sleep(std::time::Duration::from_millis(500)).await;
};
check(st == 200, format!("the ladder: {st} {ladder}"))?;
let step = first_step(&ladder);
let body = json!({"company": "acme", "account": "vera", "email": "vera@mock.invalid", "front_plan": step["front_plan"], "main_plan": step["main_plan"], "main_data_gib": step["main_data_gib"]});
let (st, start) = ask(
&relay,
shape,
"POST",
"/auth/private/start",
None,
Some(body),
)
.await;
check(st == 200, format!("the order: {st} {start}"))?;
let cs = session_of(start["url"].as_str().unwrap_or_default());
let paid = r.pay(shape, &cs);
let p = r.order_until("acme", phase::CAPTURED).await;
let (_, watch) = ask(
&relay,
shape,
"GET",
"/auth/private/status?company=acme",
None,
None,
)
.await;
// Pay once: the new account's sign-in link is MAILED (the shop's own account session, not
// Stripe's), through the box's relay; the welcome page it opens is on the site.
let until = Instant::now() + Duration::from_secs(20);
let mut link = String::new();
while link.is_empty() && Instant::now() < until {
link = r
.relay_mail
.mails()
.iter()
.find(|(to, _)| to == "vera@mock.invalid")
.and_then(|(_, body)| {
body.split_whitespace()
.find(|w| w.contains("/welcome?session="))
.map(str::to_owned)
})
.unwrap_or_default();
if link.is_empty() {
tokio::time::sleep(Duration::from_millis(200)).await;
}
}
let session = link
.split("session=")
.nth(1)
.unwrap_or_default()
.split(['&', ' ', '"', '>'])
.next()
.unwrap_or_default()
.to_owned();
let welcome = if session.is_empty() {
json!({"error": format!("no sign-in link mailed to vera: {:?}", r.relay_mail.mails())})
} else {
poll_signup(&relay, shape, &session, "created").await
};
let made = r.accounts(shape);
let history = r.history("acme");
r.stop().await;
check(
paid.as_ref().is_ok_and(|(c, _)| *c == 200),
format!("the event: {paid:?}"),
)?;
check(
p == phase::CAPTURED,
format!("laid and captured: {p}; {watch}; {history}"),
)?;
check(watch["state"] == "ready", format!("the watch: {watch}"))?;
check(
link.starts_with(&format!("{}/welcome?session=", shape.origin())),
format!("the mailed link is on the site: {link:?}"),
)?;
check(
welcome["state"] == "created",
format!("the welcome page: {welcome}"),
)?;
check(
made.iter().any(|u| u == "vera"),
format!("the new account on the site's main: {made:?}"),
)?;
Ok(format!("paid once ({cs}); account vera made; acme laid on the UpCloud mock through the signed guest, ready"))
}
async fn private_signed_in(shape: Shape) -> Result<String, String> {
let r = rig(&format!("d-{}", shape.product())).await;
let relay = r.site(shape);
make_account(&r, &relay, shape, "sigrid").await?;
let (_, ladder) = ask(&relay, shape, "GET", "/auth/private/ladder", None, None).await;
let step = first_step(&ladder);
let body = json!({"company": "beta", "email": "other@mock.invalid", "front_plan": step["front_plan"], "main_plan": step["main_plan"], "main_data_gib": step["main_data_gib"]});
let (st, start) = ask(
&relay,
shape,
"POST",
"/auth/private/start",
Some("sigrid"),
Some(body),
)
.await;
check(st == 200, format!("the order: {st} {start}"))?;
let cs = session_of(start["url"].as_str().unwrap_or_default());
// What reached Stripe for this page, as the fake serves the session and its create form.
let asked = r.fake.session(&cs).to_string();
let paid = r.pay(shape, &cs);
let p = r.order_until("beta", phase::CAPTURED).await;
let (_, watch) = ask(
&relay,
shape,
"GET",
"/auth/private/status?company=beta",
Some("sigrid"),
None,
)
.await;
let mails = r.relay_mail.mails();
let history = r.history("beta");
r.stop().await;
check(
paid.as_ref().is_ok_and(|(c, _)| *c == 200),
format!("the event: {paid:?}"),
)?;
check(
!asked.contains("other%40mock.invalid") && !asked.contains("other@mock.invalid"),
format!("the page's address never reached Stripe: {asked}"),
)?;
check(
p == phase::CAPTURED,
format!("laid and captured: {p}; {watch}; {history}"),
)?;
check(watch["state"] == "ready", format!("the watch: {watch}"))?;
check(
!mails.iter().any(|(to, _)| to.contains("other@")),
format!("no mail to the page's address: {mails:?}"),
)?;
let to: Vec<&String> = mails.iter().map(|(t, _)| t).collect();
Ok(format!(
"sigrid's own order, its estate ticket; beta laid, ready; mails only to {to:?}"
))
}
async fn grow_account(shape: Shape) -> Result<String, String> {
let r = rig(&format!("e-{}", shape.product())).await;
let relay = r.site(shape);
make_account(&r, &relay, shape, "gro").await?;
// The pulled shape: the account's fact reaches the site's main as a signed order after the
// account is made. Until the main applied it, the site's held caps ({}) and the ONE box's
// ledger (the account fact's 1-byte cap) differ, and a raise's ticket (held + delta) is
// REFUSED as `WHO_NOT_CONFIRMED` — seen in this trial's first runs (2026-10-24). A real
// site's page reads the window after its product holds the account, so the buyer waits here.
let until = Instant::now() + Duration::from_secs(20);
while Instant::now() < until
&& r.boxes
.lock()
.unwrap()
.iter()
.find(|b| b.instance == site_id(shape))
.and_then(|b| b.site.as_ref())
.is_none_or(|s| !s.caps.contains_key("gro"))
{
tokio::time::sleep(Duration::from_millis(100)).await;
}
let (st, window) = ask(
&relay,
shape,
"GET",
"/api/account/subscription/bundles",
Some("gro"),
None,
)
.await;
check(st == 200, format!("the window: {st} {window}"))?;
// ★ Cheapest first (owner, gunnar.rs 2026-10-27: "Grow your plan" opened on 100 GB): the
// ids sort as text ("100gb" before "10gb", "10gib" before "5gib"); the window does not.
let ids: Vec<&str> = window["offers"]
.as_array()
.into_iter()
.flatten()
.filter_map(|o| o["id"].as_str())
.collect();
let cheapest = match shape {
Shape::Gunnar => "10gb",
Shape::Holger => "5gib",
};
check(
ids.first() == Some(&cheapest),
format!("the window opens on the cheapest row ({cheapest}): {ids:?}"),
)?;
let offer = window["offers"]
.as_array()
.and_then(|o| o.first())
.cloned()
.ok_or(format!("no offer: {window}"))?;
let (st, buy) = ask(
&relay,
shape,
"POST",
"/api/account/subscription/buy-bundle",
Some("gro"),
Some(json!({"bundle": offer["id"]})),
)
.await;
// A refused ticket is `WHO_NOT_CONFIRMED` to the buyer; for the finding, both sides' view
// of what the account holds (the ticket covers held + delta on the site; the ONE box
// computes it from its ledger).
let ledger = r
.state
.ledger
.entitlement(shape.product(), "gro")
.ok()
.flatten()
.map(|f| f.caps);
let on_main = r
.boxes
.lock()
.unwrap()
.iter()
.find(|b| b.instance == site_id(shape))
.and_then(|b| b.site.as_ref())
.and_then(|s| s.caps.get("gro").cloned());
// ★ FINDING (2026-10-24, red on the real program): the ONE box resolves a bundle against
// its LEDGER alone (`services::purchase::caps_of_bundle`), which holds nothing for a free
// account, while the site's product holds the account's signed fact
// (`signup::account_fact`, NO_ROOM_CAP = 1 byte; `Entitlement.Get` falls back to it too).
// The site's ticket covers held + delta (1 B + 10 GiB), the box checks 10 GiB, and the
// raise is refused (WHO_NOT_CONFIRMED). The vetra trial read the ledger for the site's
// held caps and so never met it.
check(
st == 200,
format!("the buy: {st} {buy}; FINDING: the ONE box resolves the bundle against its ledger ({ledger:?} for gro) while the site's main holds the account's fact {on_main:?}, so the site's ticket (held + delta) and the box's caps differ; the offer: {offer}"),
)?;
let reference = buy["reference"].as_str().unwrap_or_default().to_owned();
let cs = session_of(buy["url"].as_str().unwrap_or_default());
let (code, said) = r.pay(shape, &cs)?;
check(code == 200, format!("the event: {code} {said}"))?;
let until = Instant::now() + Duration::from_secs(30);
let mut done = Value::Null;
while Instant::now() < until {
done = ask(
&relay,
shape,
"GET",
&format!(
"/api/account/subscription/status?reference={}",
monetize_site_buy::rules::query_value(&reference)
),
Some("gro"),
None,
)
.await
.1;
if done["state"] == "paid" {
break;
}
tokio::time::sleep(Duration::from_millis(100)).await;
}
r.stop().await;
check(
done["state"] == "paid",
format!("the way back: {done}; the buy: {buy}; the offer: {offer}"),
)?;
Ok(format!(
"{} bought ({reference}), paid, granted {}",
offer["label"], done["after"]
))
}
async fn grow_instance(shape: Shape) -> Result<String, String> {
let r = rig(&format!("f-{}", shape.product())).await;
let relay = r.site(shape);
make_account(&r, &relay, shape, "gus").await?;
let (_, ladder) = ask(&relay, shape, "GET", "/auth/private/ladder", None, None).await;
let step = first_step(&ladder);
let (st, start) = ask(&relay, shape, "POST", "/auth/private/start", Some("gus"), Some(json!({"company": "gamma", "front_plan": step["front_plan"], "main_plan": step["main_plan"], "main_data_gib": step["main_data_gib"]}))).await;
check(st == 200, format!("the order: {st} {start}"))?;
let cs = session_of(start["url"].as_str().unwrap_or_default());
r.pay(shape, &cs)?;
let p = r.order_until("gamma", phase::CAPTURED).await;
check(
p == phase::CAPTURED,
format!("the instance first: {p}; {}", r.history("gamma")),
)?;
let (st, grown) = ask(
&relay,
shape,
"GET",
"/auth/private/ladder?company=gamma",
Some("gus"),
None,
)
.await;
check(st == 200, format!("its ladder: {st} {grown}"))?;
let next = grown["steps"]
.as_array()
.and_then(|s| {
s.iter()
.skip(grown["current"].as_u64().unwrap_or(0) as usize + 1)
.find(|x| x["available"] == true)
})
.cloned()
.ok_or(format!("no bigger step: {grown}"))?;
let sizes = &next["sizes"];
let (st, change) = ask(&relay, shape, "POST", "/auth/private/change", Some("gus"), Some(json!({"company": "gamma", "front_plan": sizes["front_plan"], "main_plan": sizes["main_plan"], "main_data_gib": sizes["main_data_gib"]}))).await;
check(st == 200, format!("the change: {st} {change}"))?;
let url = change["url"].as_str().unwrap_or_default().to_owned();
if !url.is_empty() {
r.pay(shape, &session_of(&url))?;
}
// The change followed until the watch names it and two looks agree (the old trial waited
// for the order to leave the open phases, bounded at 90 s; the box's own loop drives it).
let until = Instant::now() + Duration::from_secs(90);
let mut watch = Value::Null;
let mut last = String::new();
while Instant::now() < until {
watch = ask(
&relay,
shape,
"GET",
"/auth/private/status?company=gamma",
Some("gus"),
None,
)
.await
.1;
let now = watch["change_status"]
.as_str()
.unwrap_or_default()
.to_owned();
if !now.is_empty() && now == last {
break;
}
last = now;
tokio::time::sleep(Duration::from_millis(500)).await;
}
let (_, mine) = ask(
&relay,
shape,
"GET",
"/api/account/purchases",
Some("gus"),
None,
)
.await;
r.stop().await;
check(
mine["estates"]
.as_array()
.is_some_and(|e| e.iter().any(|x| x["company"] == "gamma")),
format!("Your purchases list it: {mine}"),
)?;
check(
!watch["change_status"]
.as_str()
.unwrap_or_default()
.is_empty(),
format!("the change is followed: {watch}"),
)?;
Ok(format!(
"gamma: {} → {} ({}), change {}",
grown["steps"][grown["current"].as_u64().unwrap_or(0) as usize]["name"],
next["name"],
change["reference"],
watch["change_status"]
))
}
/// ★ **The owner's walk** (T-OWNERFLOW, owner order 2026-10-26: the purchase the owner made by
/// hand on test2.gunnar.rs, alica then rickardtest, as one test on the ONE box's own program):
///
/// 1. a NEW visitor on the root site signs up for a PAID account plan through the site's relay:
/// one checkout at the fake Stripe, paid once, the welcome page reads `created`, the account is
/// made by the SITE's main, and the site's main holds the plan's room (the console's "plan
/// shown = what was paid": alica's console said Free, ALICA-4);
/// 2. that account, SIGNED IN, orders a private instance: the order body carries no e-mail and no
/// account (the form knows the account), the order is the account's and mails the account's
/// own address (e-mail asked once), paid once;
/// 3. the instance is laid at the UpCloud mock, installed, captured; the watch says `ready`; the
/// ready mail (through the box's relay) names the address, the first admin = the buyer, the
/// one-time link (once, 7 days), where the product serves (Gunnar: its git address) and the
/// paid-until date;
/// 4. the ONE box's Accounts page lists the instance on that account, beside the plan.
///
/// Every step is the product's own door; the console's passkey and the instance's own pages are
/// the product repos' browser tests (gunnar-ui, holger-ui; see ~/plan/T-OWNERFLOW.md).
async fn owner_walk(shape: Shape) -> Result<String, String> {
let r = rig(&format!("g-{}", shape.product())).await;
let relay = r.site(shape);
let product = shape.product();
// ── 1. the account plan, a new visitor ──
let (st, plans) = ask(&relay, shape, "GET", "/auth/signup/ladder", None, None).await;
check(st == 200, format!("the plans: {st} {plans}"))?;
let paid = plans["plans"]
.as_array()
.and_then(|p| {
p.iter()
.find(|x| x["id"].as_str().is_some_and(|i| i.ends_with("/10gb-1m")))
})
.cloned()
.ok_or(format!("no 1-month paid row: {plans}"))?;
let (st, start) = ask(&relay, shape, "POST", "/auth/signup/start", None, Some(json!({"username": "alica", "email": "alica@mock.invalid", "plan": paid["id"], "next": "/"}))).await;
check(st == 200, format!("the plan's start: {st} {start}"))?;
let plan_cs = session_of(start["url"].as_str().unwrap_or_default());
check(
r.fake.minted(&plan_cs),
format!("the plan's checkout is the fake Stripe's: {start}"),
)?;
let (code, said) = r.pay(shape, &plan_cs)?;
check(code == 200, format!("the plan's event: {code} {said}"))?;
let welcome = poll_signup(&relay, shape, &plan_cs, "created").await;
check(
welcome["state"] == "created" && welcome["username"] == "alica",
format!("the welcome page after paying: {welcome}"),
)?;
check(
r.accounts(shape).iter().any(|a| a == "alica"),
format!("the account on the site's main: {:?}", r.accounts(shape)),
)?;
let meter = if shape == Shape::Gunnar {
"pack_bytes"
} else {
"disk_bytes"
};
let until = Instant::now() + Duration::from_secs(30);
let held = loop {
let held = r
.boxes
.lock()
.unwrap()
.iter()
.find(|b| b.instance == site_id(shape))
.and_then(|b| b.site.as_ref())
.and_then(|s| s.caps.get("alica").cloned())
.unwrap_or_default();
if held.get(meter).copied() == Some(10 * GIB) || Instant::now() > until {
break held;
}
tokio::time::sleep(Duration::from_millis(100)).await;
};
check(
held.get(meter).copied() == Some(10 * GIB),
format!("the site's main holds what was paid (10 GiB {meter}) for alica: {held:?}"),
)?;
// ── 2. a private instance, signed in: the form knows the account, no e-mail asked ──
let (st, ladder) = ask(
&relay,
shape,
"GET",
"/auth/private/ladder",
Some("alica"),
None,
)
.await;
check(
st == 200,
format!("the private ladder, signed in: {st} {ladder}"),
)?;
let step = first_step(&ladder);
let body = json!({"company": "rickardtest", "front_plan": step["front_plan"], "main_plan": step["main_plan"], "main_data_gib": step["main_data_gib"]});
let (st, order) = ask(
&relay,
shape,
"POST",
"/auth/private/start",
Some("alica"),
Some(body),
)
.await;
check(
st == 200,
format!("the signed-in order with no e-mail and no account in it: {st} {order}"),
)?;
check(
order["price_words"].as_str().is_some_and(|w| !w.is_empty()),
format!("the order names its price: {order}"),
)?;
let cs = session_of(order["url"].as_str().unwrap_or_default());
check(
cs != plan_cs && r.fake.minted(&cs),
format!("the instance's own checkout: {order}"),
)?;
let run = r
.state
.estates
.get("rickardtest")
.ok_or("no run for rickardtest")?;
check(
run.account == "alica",
format!("the order is the signed-in account's: {:?}", run.account),
)?;
check(
run.email == "alica@mock.invalid",
format!("the order mails the account's own address: {:?}", run.email),
)?;
let (code, said) = r.pay(shape, &cs)?;
check(code == 200, format!("the instance's event: {code} {said}"))?;
// ── 3. laid, installed, captured; the watch; the ready mail ──
let p = r.order_until("rickardtest", phase::CAPTURED).await;
let (_, watch) = ask(
&relay,
shape,
"GET",
"/auth/private/status?company=rickardtest",
Some("alica"),
None,
)
.await;
check(
p == phase::CAPTURED,
format!(
"laid and captured: {p}; {watch}; {}",
r.history("rickardtest")
),
)?;
check(watch["state"] == "ready", format!("the watch: {watch}"))?;
let run = r.state.estates.get("rickardtest").ok_or("the run went")?;
// The instance's own name, under the site it was bought on.
let host = format!("rickardtest.{}", shape.host());
let until = Instant::now() + Duration::from_secs(20);
let mut ready: Vec<String> = Vec::new();
while ready.is_empty() && Instant::now() < until {
ready = r
.relay_mail
.mails()
.into_iter()
.filter(|(to, body)| to == "alica@mock.invalid" && body.contains("/login?invite="))
.map(|(_, b)| b)
.collect();
if ready.is_empty() {
tokio::time::sleep(Duration::from_millis(200)).await;
}
}
let mails: Vec<String> = r
.relay_mail
.mails()
.iter()
.map(|(t, b)| {
format!(
"{t}: {}",
b.split_whitespace()
.take(12)
.filter(|w| !w.contains("invite="))
.collect::<Vec<_>>()
.join(" ")
)
})
.collect();
check(
ready.len() == 1,
format!("ONE ready mail with the one-time link to alica: {mails:?}"),
)?;
let mail = ready[0].clone();
// The one-time link is a secret, even a test's: never in a failure's words.
let shown = mail
.split_whitespace()
.map(|w| {
if w.contains("invite=") {
"<the one-time link>"
} else {
w
}
})
.collect::<Vec<_>>()
.join(" ");
let want: Vec<String> = vec![
format!("{host} is installed and running"),
"its first admin is alica".to_string(),
format!("https://{host}/login?invite="),
"&user=alica".to_string(),
"it works once, for 7 days".to_string(),
format!(
"paid until {}",
monetize_server::estate_order::iso_date(run.paid_until_ms)
),
if shape == Shape::Gunnar {
format!("git is at git.{host}.")
} else {
format!("served from https://{host}.")
},
];
for w in &want {
check(
mail.contains(w.as_str()),
format!("the ready mail says {w:?}: {shown}"),
)?;
}
check(
mail.matches("/login?invite=").count() == 1,
format!("the one-time link once: {shown}"),
)?;
// ── 4. the ONE box's Accounts page: the plan and the instance, on alica's account ──
let token = std::fs::read_to_string(r.dir.join("data").join(monetize_front::intake::TOKEN_REL))
.map_err(|e| e.to_string())?
.trim()
.to_owned();
let channel = tonic::transport::Channel::from_shared(format!("http://{}", r.addr()))
.map_err(|e| e.to_string())?
.connect()
.await
.map_err(|e| e.to_string())?;
let mut accounts = monetize_proto::accounts_client::AccountsClient::new(channel);
fn authed<T>(token: &str, m: T) -> tonic::Request<T> {
let mut q = tonic::Request::new(m);
q.metadata_mut()
.insert("authorization", format!("Bearer {token}").parse().unwrap());
q
}
let listed = accounts
.list(authed(
&token,
monetize_proto::ListAccountsRequest {
product: product.into(),
limit: 100,
},
))
.await
.map_err(|e| format!("Accounts.List: {e}"))?
.into_inner()
.accounts;
let acct = listed
.iter()
.find(|a| a.tenant == "alica" || a.tenant.ends_with("/alica"))
.cloned()
.ok_or(format!("alica is not on the Accounts page: {listed:?}"))?;
if acct.instances != 1 {
let other = if shape == Shape::Gunnar {
"holger"
} else {
"gunnar"
};
let theirs = accounts
.list(authed(
&token,
monetize_proto::ListAccountsRequest {
product: other.into(),
limit: 100,
},
))
.await
.map(|a| a.into_inner().accounts)
.unwrap_or_default();
let ledger = r
.state
.ledger
.instances(product)
.map(|v| v.len())
.unwrap_or(0);
let ledger_other = r
.state
.ledger
.instances(other)
.map(|v| {
v.iter()
.map(|i| format!("{}:{}", i.company, i.tenant))
.collect::<Vec<_>>()
})
.unwrap_or_default();
return Err(format!("one instance on alica's {product} account: {acct:?}; {product} ledger instances {ledger}; {other} ledger instances {ledger_other:?}; {other}'s accounts {theirs:?}"));
}
let rows = accounts
.rows(authed(
&token,
monetize_proto::AccountRowsRequest {
product: product.into(),
tenant: acct.tenant.clone(),
},
))
.await
.map_err(|e| format!("Accounts.Rows: {e}"))?
.into_inner();
check(
rows.rows
.iter()
.any(|x| x.kind == "instance" && x.id.contains("rickardtest")),
format!("the instance is a row on alica's account: {:?}", rows.rows),
)?;
let names: Vec<String> = listed
.iter()
.map(|a| format!("{} (rows {}, instances {})", a.tenant, a.rows, a.instances))
.collect();
check(
rows.rows
.iter()
.any(|x| x.kind == "purchase" || x.kind == "subscription"),
format!(
"the plan is a row on alica's account {}: {:?}; the {product} accounts: {names:?}",
acct.tenant, rows.rows
),
)?;
check(
listed
.iter()
.filter(|a| a.tenant == "alica" || a.tenant.ends_with("/alica"))
.count()
== 1,
format!("ONE account for alica on the Accounts page: {names:?}"),
)?;
r.stop().await;
Ok(format!("alica paid once for {} ({plan_cs}), made on {}'s main holding 10 GiB; rickardtest ordered signed in (no e-mail asked), paid once ({cs}), laid and ready; the ready mail names {host}, alica, the 7-day link, paid until {}; Accounts lists both", paid["id"], site_id(shape), monetize_server::estate_order::iso_date(run.paid_until_ms)))
}
macro_rules! trials {
($($fn_:ident => $name:literal),* $(,)?) => {$(
mod $fn_ {
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn gunnar() {
super::trial($name, super::Shape::Gunnar, super::$fn_(super::Shape::Gunnar)).await
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn holger() {
super::trial($name, super::Shape::Holger, super::$fn_(super::Shape::Holger)).await
}
}
)*};
}
trials!(
free_account => "a-free-account",
paid_plan => "b-paid-plan",
private_visitor => "c-private-visitor",
private_signed_in => "d-private-signed-in",
grow_account => "e-grow-account",
grow_instance => "f-grow-instance",
owner_walk => "g-owner-walk",
);
/// ★ MHUNT-2 (OX-MONHUNT 2026-10-28, `cargo xtask trial buy` on oden): `free_account::gunnar`
/// failed with "the fake Stripe did not listen on 36485/44091 in 20 s" — its child had exited
/// with "Address already in use": the port [`free_port`] named was taken before the child bound
/// it. Here the first port named is held by a socket that never answers: the fake must still
/// come up, on other ports, and answer its own control door.
#[test]
fn the_fake_stripe_comes_up_on_new_ports_when_the_one_it_was_given_is_taken() {
let inp = inputs();
let taken = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
let held = taken.local_addr().unwrap().port();
let mut first = true;
let mut pick = || {
if std::mem::take(&mut first) {
held
} else {
free_port()
}
};
let fake = FakeStripe::start_on(&inp.fake_stripe, SK, WHSEC, &mut pick);
assert_ne!(
fake.api, held,
"the fake never took the held port: it came up on another"
);
assert_ne!(
fake.control("GET", "/seen/cs_none").0,
0,
"its control door answers"
);
drop(taken);
}