crates/monetize-server/tests/add_users.rs
//! **"Add users"** (owner, 2026-10-04): ONE call — `Purchase.Start` with
//! `users` — for `count` users each holding `room_bytes` of the account's
//! room. The product says whether it fits in the room the account holds and
//! has neither stored nor promised; if so, the users are granted at once and
//! nothing is bought. If not:
//!
//! * a deployment that sells room sells EXACTLY the shortfall, in ONE order
//! with ONE checkout, and grants the users when it is PAID — an expired
//! checkout grants nobody;
//! * the open, free path refuses, naming how much room is missing.
//!
//! Over the wire, against the real router, with the MockProduct standing in
//! for gunnar's books (a 10 GiB first period for `acme`).
use std::sync::{Arc, Mutex};
use monetize::testing::{purchase, Answer, MockCloud, MockPay, MockProduct, NOW};
use monetize::transaction::Outcome;
use monetize_proto::purchase_client::PurchaseClient;
use monetize_proto::{self as pb, PurchaseState};
use monetize_server::registry::Registry;
use monetize_server::state::{AppState, TermsStore};
use monetize_server::testing::{http_post, CallbackPay, TestServer};
use tonic::metadata::MetadataValue;
use tonic::transport::Channel;
use tonic::{Code, Request};
const BEARER: &str = "test-bearer-token-of-sufficient-length";
const SECRET: &str = "mockpay-shared-secret";
const GIB: u64 = 1 << 30;
/// The deployment's one mail path, recording.
#[derive(Default)]
struct Mails(Mutex<Vec<monetize_pay::CustomerNotice>>);
impl monetize_pay::SignupSink for Mails {
fn paid(&self, _: &monetize_pay::signup::PaidSignup) -> Result<monetize_pay::signup::SignupOutcome, monetize_pay::SignupError> {
Err(monetize_pay::SignupError::Refused("not a signup test".into()))
}
fn name_verdict(&self, _: &str) -> Result<monetize_pay::signup::NameVerdict, monetize_pay::SignupError> {
Ok(monetize_pay::signup::NameVerdict::Free)
}
fn notify(&self, n: &monetize_pay::CustomerNotice) -> Result<String, monetize_pay::SignupError> {
self.0.lock().unwrap().push(n.clone());
Ok(format!("sent to {}", n.to))
}
}
impl Mails {
fn of(&self, kind: &str) -> Vec<monetize_pay::CustomerNotice> {
self.0.lock().unwrap().iter().filter(|n| n.kind == kind).cloned().collect()
}
}
struct Rig {
mails: Arc<Mails>,
state: Arc<AppState>,
product: Arc<MockProduct>,
pay: CallbackPay,
server: TestServer,
channel: Channel,
}
impl Rig {
/// `acme` holds 10 GiB (its first period, paid) and stores nothing.
/// `priced`: the deployment sells room (22.00 SEK per GiB-month) through
/// a vendor with a hosted checkout; otherwise it is the open, free one.
async fn up(priced: bool) -> Rig {
let product = Arc::new(MockProduct::gunnar());
product.absorbs_on_hand();
product.holds_room("acme", 0, 0);
let cloud = MockCloud::new();
cloud.answer_all(Answer::OnHand);
let pay = CallbackPay::with_checkout(SECRET, "https://checkout.example/c/");
let mut state = AppState::in_memory(Registry::new().shared_product(product.clone()).sells("gunnar", pay.clone()).cloud(cloud));
*state.terms.write().unwrap() = if priced { TermsStore::fixture() } else { TermsStore { prices: Default::default(), ..TermsStore::fixture() } };
state.clock = Box::new(|| NOW);
let mails = Arc::new(Mails::default());
state.signup_sink = mails.clone();
// acme's address, from its signup: where the BUYER is told things.
let signup = monetize_pay::signup::PaidSignup {
vendor: "mockpay".into(),
session: "cs_acme".into(),
reference: "pi_acme".into(),
email: "admin@acme.test".into(),
username: "acme".into(),
product: "gunnar".into(),
plan: "plan".into(),
amount: monetize_pay::Money { minor: 0, currency: monetize_pay::Currency::Sek },
tax: monetize_pay::Money { minor: 0, currency: monetize_pay::Currency::Sek },
paid_at_unix_ms: NOW as i64,
customer_country: None,
};
state.signups.record_pending(&signup, NOW as i64).unwrap();
state
.signups
.set("mockpay", "cs_acme", monetize_server::signup::SignupState::Created { namespace: "acme".into(), username: "acme".into(), at_unix_ms: NOW as i64 })
.unwrap();
let state = Arc::new(state);
let first = state.transaction(state.registry.products().next().unwrap(), monetize_pay::Vendor::of(&MockPay::new())).run(&purchase("acme")).expect("acme's first period");
assert!(matches!(first, Outcome::Paid { .. }), "{first:?}");
let server = TestServer::spawn(state.clone(), BEARER).await;
let channel = Channel::from_shared(server.endpoint()).unwrap().connect().await.expect("connect");
Rig { mails, state, product, pay, server, channel }
}
fn authed<T>(&self, req: T) -> Request<T> {
let mut r = Request::new(req);
r.metadata_mut().insert("authorization", MetadataValue::try_from(format!("Bearer {BEARER}")).unwrap());
r
}
async fn add(&self, count: u32, room_bytes: u64, name: &str) -> Result<pb::StartPurchaseResponse, tonic::Status> {
self.add_mailing(count, room_bytes, name, "").await
}
/// "Add users" with the address the named user's invite goes to.
async fn add_mailing(&self, count: u32, room_bytes: u64, name: &str, email: &str) -> Result<pb::StartPurchaseResponse, tonic::Status> {
PurchaseClient::new(self.channel.clone())
.start(self.authed(pb::StartPurchaseRequest {
product: "gunnar".into(),
tenant: "acme".into(),
vendor: "mockpay".into(),
market: pb::Market::Se as i32,
users: Some(pb::UsersOrder { count, room_bytes, name: name.into(), email: email.into(), invite_origin: "https://console.test".into() }),
..Default::default()
}))
.await
.map(|r| r.into_inner())
}
async fn status(&self, reference: &str) -> pb::PurchaseStatusResponse {
PurchaseClient::new(self.channel.clone())
.status(self.authed(pb::PurchaseStatusRequest { reference: reference.into(), ..Default::default() }))
.await
.expect("status")
.into_inner()
}
fn deliver(&self, reference: &str, handle: &str, event: &str) -> (u16, String) {
let body = CallbackPay::callback_body(reference, handle, event);
http_post(self.server.addr, "/callback/mockpay", &self.pay.signed_headers(), &body).expect("the webhook answers")
}
fn kinds(&self, reference: &str) -> Vec<String> {
self.state.ledger.rows(reference).unwrap().iter().map(|r| r.step.name().to_owned()).collect()
}
}
/// ★ **It fits: granted AT ONCE, nothing bought.** One named user of 10 GiB
/// on an account holding 10 GiB it does not use: no vendor, no checkout, the
/// user exists, and the ledger says where its room came from.
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn one_named_user_that_fits_is_granted_at_once_and_nothing_is_bought() {
let rig = Rig::up(true).await;
let added = rig.add(1, 10 * GIB, "eve").await.expect("eve fits");
assert_eq!(added.state(), PurchaseState::Paid, "{added:?}");
assert!(added.redirect_url.is_empty(), "a checkout for room the account already has: {}", added.redirect_url);
let users = added.users.clone().expect("the grant");
assert!(users.granted, "{users:?}");
assert_eq!((users.principal.as_str(), users.bought_bytes, users.from_unused_bytes), ("eve", 0, 10 * GIB));
assert!(added.reference.starts_with("gunnar/acme/") && added.reference.contains("+users:"), "{}", added.reference);
assert_eq!(rig.pay.payers().len(), 0, "a vendor was asked to reserve");
assert_eq!(rig.kinds(&added.reference), ["UsersGranted"]);
let granted = rig.product.users_granted();
assert_eq!(granted.len(), 1, "{granted:?}");
assert_eq!(granted[0].2, added.reference, "the grant carries its order");
// Asked again: the same grant, not a second user.
let again = rig.add(1, 10 * GIB, "eve").await;
assert!(again.is_err() || again.as_ref().is_ok_and(|a| a.reference == added.reference), "{again:?}");
assert_eq!(rig.product.users_granted().len(), 1, "a retry added eve twice");
let seen = rig.status(&added.reference).await;
assert_eq!(seen.state(), PurchaseState::Paid);
// Read back off the ledger: granted, and NOT named (G13) — the name is the estate's.
assert!(seen.users.as_ref().is_some_and(|u| u.granted && u.principal.is_empty()), "{seen:?}");
assert_eq!(seen.room.as_ref().map(|r| r.state.as_str()), Some("done"));
rig.server.stop().await;
}
/// ★ **More than one: SEATS, no names.** Ten users of 1 GiB on 10 GiB unused.
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn ten_users_are_ten_seats_with_no_names() {
let rig = Rig::up(true).await;
let added = rig.add(10, GIB, "").await.expect("ten seats fit");
assert_eq!(added.state(), PurchaseState::Paid);
let users = added.users.expect("the grant");
assert_eq!((users.principal.as_str(), users.seats, users.need_bytes, users.bought_bytes), ("", 10, 10 * GIB, 0));
// The same number again is MORE seats, not the first grant again.
let rig2 = Rig::up(true).await;
let first = rig2.add(2, GIB, "").await.expect("two seats");
let again = rig2.add(2, GIB, "").await.expect("two more");
assert_ne!(first.reference, again.reference, "a second ask was answered with the first grant");
assert_eq!(again.users.expect("the grant").seats, 4, "the second ask added no seats");
rig2.server.stop().await;
rig.server.stop().await;
}
/// ★ **The open, free path: short is REFUSED, naming the missing room**, and
/// nothing is granted, reserved or written.
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn the_free_path_refuses_a_short_order_naming_the_missing_room() {
let rig = Rig::up(false).await;
let short = rig.add(4, 10 * GIB, "").await.expect_err("40 GiB of 10");
assert_eq!(short.code(), Code::FailedPrecondition, "{short:?}");
let msg = short.message();
assert!(msg.contains("30.0 GiB short"), "the refusal names the missing room: {msg}");
assert!(msg.contains("10.0 GiB of room unused"), "{msg}");
assert!(msg.contains("Nothing has been charged"), "{msg}");
assert!(rig.product.users_granted().is_empty(), "users were granted on a refusal");
assert!(rig.pay.payers().is_empty(), "a vendor was asked");
assert!(rig.state.ledger.unfinished().unwrap().is_empty(), "an attempt was opened");
rig.server.stop().await;
}
/// ★ **Paid: ONE checkout for exactly the shortfall — the unused room is
/// used first — and the users are granted when it is paid.** Four users of
/// 10 GiB on 10 GiB unused: 30 GiB is sold, once.
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn a_short_order_buys_only_the_shortfall_in_one_checkout_and_grants_the_users_when_paid() {
let rig = Rig::up(true).await;
let started = rig.add(4, 10 * GIB, "").await.expect("the order starts");
assert_eq!(started.state(), PurchaseState::Pending, "{started:?}");
assert!(started.redirect_url.starts_with("https://checkout.example/c/"), "ONE hosted checkout: {}", started.redirect_url);
let users = started.users.clone().expect("what it is for");
assert!(!users.granted, "granted before it was paid: {users:?}");
assert_eq!((users.need_bytes, users.from_unused_bytes, users.bought_bytes), (40 * GIB, 10 * GIB, 30 * GIB));
assert_eq!(rig.pay.payers().len(), 1, "one reservation, one checkout");
let attempt = rig.state.ledger.attempt(&started.reference).unwrap().expect("the attempt");
assert_eq!(attempt.purchase.delta.get("pack_bytes"), Some(&(30 * GIB)), "sold more than the shortfall: {:?}", attempt.purchase.delta);
assert_eq!(attempt.purchase.caps.get("pack_bytes"), Some(&(40 * GIB)));
assert!(rig.product.users_granted().is_empty(), "users granted while the checkout is open");
let pending = rig.status(&started.reference).await;
assert_eq!(pending.state(), PurchaseState::Pending);
assert!(pending.users.as_ref().is_some_and(|u| !u.granted), "{pending:?}");
assert!(pending.room.is_none(), "room progress for an unpaid order: {:?}", pending.room);
rig.pay.authorize(&started.handle_id);
assert_eq!(rig.deliver(&started.reference, &started.handle_id, "authorized").0, 200);
let granted = rig.product.users_granted();
assert_eq!(granted.len(), 1, "{granted:?}");
assert_eq!((granted[0].1.count, granted[0].3.seats), (4, 4));
assert_eq!(granted[0].2, started.reference, "the grant names the order that paid for it");
assert_eq!(rig.kinds(&started.reference).last().map(String::as_str), Some("UsersGranted"));
let paid = rig.status(&started.reference).await;
assert_eq!(paid.state(), PurchaseState::Paid);
let users = paid.users.expect("the grant");
assert!(users.granted && users.bought_bytes == 30 * GIB, "{users:?}");
// No grow batcher on this rig: nothing queued, the room is simply there.
assert_eq!(paid.room.as_ref().map(|r| r.state.as_str()), Some("done"), "{:?}", paid.room);
// A redelivery grants nobody twice.
assert_eq!(rig.deliver(&started.reference, &started.handle_id, "authorized").0, 200);
assert_eq!(rig.product.users_granted().len(), 1, "a redelivered payment granted twice");
rig.server.stop().await;
}
/// ★ **Expired: the reservation is released and NOBODY is granted.**
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn an_expired_checkout_releases_the_order_and_grants_nobody() {
let rig = Rig::up(true).await;
let started = rig.add(1, 20 * GIB, "zed").await.expect("the order starts");
assert_eq!(started.state(), PurchaseState::Pending, "{started:?}");
rig.pay.expire(&started.handle_id);
assert_eq!(rig.deliver(&started.reference, &started.handle_id, "released").0, 200);
let seen = rig.status(&started.reference).await;
assert_eq!(seen.state(), PurchaseState::Released, "{seen:?}");
assert!(seen.users.as_ref().is_some_and(|u| !u.granted), "{seen:?}");
assert!(rig.product.users_granted().is_empty(), "an expired checkout granted a user");
assert!(!rig.kinds(&started.reference).iter().any(|k| k == "UsersGranted"));
rig.server.stop().await;
}
/// ★ **The rest is bought in the unit room is sold in**: a shortfall of
/// 30 GiB and 3 MiB (the account stores 3 MiB) is 31 GiB bought — the
/// customer pays per GiB and gets every GiB they pay for.
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn a_shortfall_is_bought_in_whole_gib() {
let rig = Rig::up(true).await;
rig.product.holds_room("acme", 0, 3 << 20);
let started = rig.add(4, 10 * GIB, "").await.expect("the order starts");
let users = started.users.clone().expect("what it is for");
assert_eq!(users.bought_bytes, 31 * GIB, "{users:?}");
let attempt = rig.state.ledger.attempt(&started.reference).unwrap().expect("the attempt");
assert_eq!(attempt.purchase.delta.get("pack_bytes"), Some(&(31 * GIB)));
rig.server.stop().await;
}
/// ★ **A named user added at once gets the invite the admin's path makes**:
/// the link on the answer (the admin is on the page) and, given an address,
/// the same link in the user's mail.
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn a_named_user_added_at_once_is_invited_and_mailed() {
let rig = Rig::up(true).await;
let added = rig.add_mailing(1, 10 * GIB, "eve", "eve@acme.test").await.expect("eve fits");
let users = added.users.expect("the grant");
assert_eq!(users.invite_url, "https://console.test/login?invite=secret-eve&user=eve", "{users:?}");
let mailed = rig.mails.of("users.invite");
assert_eq!(mailed.len(), 1, "{mailed:?}");
assert_eq!(mailed[0].to, "eve@acme.test");
assert!(mailed[0].body.contains("https://console.test/login?invite=secret-eve&user=eve"), "{}", mailed[0].body);
assert!(!users.invited.is_empty(), "the answer does not say the invite was mailed");
rig.server.stop().await;
}
/// ★ **A named user bought through checkout is invited when the grant
/// lands** — behind the webhook, with nobody on the page — by mail, once.
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn a_named_user_paid_through_checkout_is_invited_by_mail_when_granted() {
let rig = Rig::up(true).await;
let started = rig.add_mailing(1, 20 * GIB, "zed", "zed@acme.test").await.expect("the order starts");
assert_eq!(started.state(), PurchaseState::Pending);
assert!(rig.mails.of("users.invite").is_empty(), "invited before the payment");
rig.pay.authorize(&started.handle_id);
assert_eq!(rig.deliver(&started.reference, &started.handle_id, "authorized").0, 200);
let mailed = rig.mails.of("users.invite");
assert_eq!(mailed.len(), 1, "{mailed:?}");
assert_eq!(mailed[0].to, "zed@acme.test");
assert!(mailed[0].body.contains("https://console.test/login?invite=secret-zed&user=zed"), "{}", mailed[0].body);
let seen = rig.status(&started.reference).await;
let users = seen.users.expect("the grant");
assert!(users.granted && users.invite_url.is_empty() && !users.invited.is_empty(), "{users:?}");
// A redelivery invites nobody twice.
assert_eq!(rig.deliver(&started.reference, &started.handle_id, "authorized").0, 200);
assert_eq!(rig.mails.of("users.invite").len(), 1);
rig.server.stop().await;
}
/// ★ **Paid, and the users can never be added** (the name was taken while the
/// customer paid): the payment is REFUNDED through the vendor, the room is
/// given back, it is journaled, and the buyer is told — once.
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn a_grant_refused_for_good_after_the_capture_is_refunded_and_the_buyer_told() {
let rig = Rig::up(true).await;
let started = rig.add_mailing(1, 20 * GIB, "zed", "zed@acme.test").await.expect("the order starts");
rig.product.users_taken.lock().unwrap().push("zed".into()); // taken during the checkout
rig.pay.authorize(&started.handle_id);
rig.pay.took_signup(&started.handle_id); // the vendor holds the captured money
assert_eq!(rig.deliver(&started.reference, &started.handle_id, "authorized").0, 200);
assert!(rig.product.users_granted().is_empty(), "granted a taken name");
assert_eq!(rig.pay.refunds_made(), 1, "no refund: {:?}", rig.pay.refund_calls());
let kinds = rig.kinds(&started.reference);
assert!(kinds.iter().any(|k| k == "RefundRequested") && kinds.iter().any(|k| k == "Reversed"), "not journaled: {kinds:?}");
assert_eq!(rig.state.ledger.entitlement("gunnar", "acme").unwrap().unwrap().caps["pack_bytes"], 10 * GIB, "the room bought for nobody stayed");
let told = rig.mails.of("users.refunded");
assert_eq!(told.len(), 1, "{told:?}");
assert_eq!(told[0].to, "admin@acme.test", "the BUYER is told");
// The product's name as customers read it (the "Gunnar" rule), never its id.
assert!(told[0].subject.starts_with("Gunnar: "), "{}", told[0].subject);
assert!(told[0].body.contains("the name was taken"), "a name taken is said as such: {}", told[0].body);
assert!(rig.mails.of("users.invite").is_empty());
let seen = rig.status(&started.reference).await;
let users = seen.users.expect("what it was for");
assert!(users.refunded && !users.granted && !users.reason.is_empty(), "{users:?}");
// Again: no second refund, no second mail.
assert_eq!(rig.deliver(&started.reference, &started.handle_id, "authorized").0, 200);
let _ = rig.status(&started.reference).await;
assert_eq!(rig.pay.refunds_made(), 1);
assert_eq!(rig.mails.of("users.refunded").len(), 1);
rig.server.stop().await;
}
/// ★ **Paid, and refused for another reason than a name** (wiring tracer B 2026-10-08):
/// gunnar's explicit NO for an Add-users order that is not a name taken (`FailedPrecondition`,
/// e.g. a clan) is refunded the same way — and the buyer is told what happened, not "the name
/// was taken while you paid" (which every refusal said).
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn a_grant_refused_for_another_reason_is_refunded_and_never_said_to_be_a_name_taken() {
let rig = Rig::up(true).await;
let started = rig.add_mailing(1, 20 * GIB, "zed", "zed@acme.test").await.expect("the order starts");
*rig.product.users_refused.lock().unwrap() = Some("gunnar FailedPrecondition: acme is a clan, and a clan takes no employees".into());
rig.pay.authorize(&started.handle_id);
rig.pay.took_signup(&started.handle_id);
assert_eq!(rig.deliver(&started.reference, &started.handle_id, "authorized").0, 200);
assert_eq!(rig.pay.refunds_made(), 1, "no refund: {:?}", rig.pay.refund_calls());
let told = rig.mails.of("users.refunded");
assert_eq!(told.len(), 1, "{told:?}");
assert!(told[0].subject.starts_with("Gunnar: "), "{}", told[0].subject);
assert!(!told[0].body.contains("name was taken"), "not a name taken: {}", told[0].body);
assert!(told[0].body.contains("zed could not be added after the payment"), "{}", told[0].body);
let users = rig.status(&started.reference).await.users.expect("what it was for");
assert!(users.refunded && !users.reason.contains("name was taken"), "{users:?}");
rig.server.stop().await;
}
/// ★ **A grant whose ledger row cannot be written is granted and mailed ONCE**
/// (review f7): the product added the user and the invite went out, but the
/// `UsersGranted` row failed. That is an error, not a log line, and the next
/// read writes the row without a second invite mail.
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn a_grant_whose_row_cannot_be_written_is_retried_without_a_second_invite() {
let rig = Rig::up(true).await;
let started = rig.add_mailing(1, 20 * GIB, "zed", "zed@acme.test").await.expect("the order starts");
rig.state.ledger.refuse_appends(Some(|step| matches!(step, monetize::Step::UsersGranted { .. })));
rig.pay.authorize(&started.handle_id);
let _ = rig.deliver(&started.reference, &started.handle_id, "authorized");
assert_eq!(rig.mails.of("users.invite").len(), 1, "the user was invited once");
let refused = PurchaseClient::new(rig.channel.clone())
.status(rig.authed(pb::PurchaseStatusRequest { reference: started.reference.clone(), ..Default::default() }))
.await
.expect_err("a grant that cannot be written down is an error, not a quiet log line");
assert_eq!(refused.code(), Code::Unavailable, "{refused:?}");
rig.state.ledger.refuse_appends(None);
let users = rig.status(&started.reference).await.users.expect("the grant");
assert!(users.granted, "{users:?}");
assert_eq!(rig.kinds(&started.reference).iter().filter(|k| *k == "UsersGranted").count(), 1);
assert_eq!(rig.mails.of("users.invite").len(), 1, "never a second invite mail");
rig.server.stop().await;
}
/// ★ **The room a refund takes back is retried until it is taken** (review
/// f8): the refund landed, but the room could not be taken back then (here
/// the fact store refused it; a crash does the same). The buyer is not told
/// yet; the next read takes the room back and tells the buyer, once.
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn the_room_a_refund_takes_back_is_retried_until_it_is_taken() {
let rig = Rig::up(true).await;
let started = rig.add_mailing(1, 20 * GIB, "zed", "zed@acme.test").await.expect("the order starts");
rig.product.users_taken.lock().unwrap().push("zed".into());
rig.pay.authorize(&started.handle_id);
rig.pay.took_signup(&started.handle_id);
rig.state.ledger.refuse_entitlements(Some(|fact| fact.source.starts_with("refund:")));
let _ = rig.deliver(&started.reference, &started.handle_id, "authorized");
assert_eq!(rig.pay.refunds_made(), 1);
assert_eq!(rig.state.ledger.entitlement("gunnar", "acme").unwrap().unwrap().caps["pack_bytes"], 20 * GIB, "the store refused the take-back");
assert!(rig.mails.of("users.refunded").is_empty(), "the buyer is told when the room is back, not before");
rig.state.ledger.refuse_entitlements(None);
let users = rig.status(&started.reference).await.users.expect("what it was for");
assert!(users.refunded, "{users:?}");
assert_eq!(rig.state.ledger.entitlement("gunnar", "acme").unwrap().unwrap().caps["pack_bytes"], 10 * GIB, "the room is taken back on the next read");
let _ = rig.status(&started.reference).await;
assert_eq!(rig.state.ledger.entitlement("gunnar", "acme").unwrap().unwrap().caps["pack_bytes"], 10 * GIB, "taken back once");
assert_eq!(rig.mails.of("users.refunded").len(), 1, "told once");
assert_eq!(rig.pay.refunds_made(), 1);
rig.server.stop().await;
}
/// ★ **G13: no member is NAMED on the ledger** — not on the grant, not in a
/// reference, not on a paid order's `Reserved` row, not by the invite's
/// address: the named user is an opaque id there, and the name and the
/// address live in the estate that holds the user (and, between a checkout
/// and its payment, beside the ledger, until the grant or the refund).
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn no_member_is_named_on_the_ledger() {
let rig = Rig::up(true).await;
let now = rig.add_mailing(1, 10 * GIB, "eve", "eve@acme.test").await.expect("eve fits");
assert_eq!(now.users.as_ref().map(|u| u.principal.as_str()), Some("eve"), "the admin on the page is told whom");
let paid = rig.add_mailing(1, 20 * GIB, "zed", "zed@acme.test").await.expect("the order starts");
assert_eq!(paid.state(), PurchaseState::Pending);
let leaks = |rows: &str| ["\"eve\"", ":eve", "eve@", "\"zed\"", ":zed", "zed@"].into_iter().filter(|n| rows.contains(n)).collect::<Vec<_>>();
let rows = serde_json::to_string(&rig.state.ledger.export_rows().unwrap()).unwrap();
assert!(leaks(&rows).is_empty(), "named on the ledger while the checkout is open: {:?}\n{rows}", leaks(&rows));
rig.pay.authorize(&paid.handle_id);
assert_eq!(rig.deliver(&paid.reference, &paid.handle_id, "authorized").0, 200);
let mailed = rig.mails.of("users.invite");
assert!(mailed.iter().any(|m| m.to == "zed@acme.test" && m.body.contains("user=zed")), "zed was still granted and invited by name: {mailed:?}");
assert_eq!(rig.product.users_granted().iter().map(|g| g.1.name.clone()).collect::<Vec<_>>(), ["eve", "zed"], "the estate holds the names");
let rows = serde_json::to_string(&rig.state.ledger.export_rows().unwrap()).unwrap();
assert!(leaks(&rows).is_empty(), "named on the ledger: {:?}\n{rows}", leaks(&rows));
let ids: Vec<String> = rig
.state
.ledger
.export_rows()
.unwrap()
.into_iter()
.filter_map(|(_, row)| match row.step {
monetize::Step::UsersGranted { principal_id, .. } => Some(principal_id),
_ => None,
})
.collect();
let mut want = vec![monetize::ledger::principal_id("gunnar", "acme", "eve"), monetize::ledger::principal_id("gunnar", "acme", "zed")];
let mut ids = ids;
ids.sort();
want.sort();
assert_eq!(ids, want, "each grant names its member by id");
assert!(rig.state.named_users.get(&paid.reference).is_none(), "the name beside the ledger is forgotten once granted");
rig.server.stop().await;
}