crates/monetize-server/tests/renewal.rs
//! **A renewal, against the three things the audit lane built.**
//!
//! `monetize::transaction::renewal_tests` proves the arithmetic — what is sold,
//! what it costs, who is refused. This file proves the three INTERACTIONS,
//! because each of them is a seam between the renewal and something that moved
//! underneath it, and a seam is not tested by either side alone:
//!
//! 1. **capacity** — `Signup.Start` is gated on `can_absorb`; a renewal must
//! not be, or a full appliance is one a customer cannot pay for;
//! 2. **the ladder** — a renewal is what a Suspended tenant does, so it has to
//! move them back up THROUGH the compare-and-swap the poller now does, and
//! win the race against a tick that is deciding about them at that moment;
//! 3. **the reclamation queue** — a tenant who renews must come OFF the
//! operator's teardown list, with the withdrawal written down.
//!
//! Driven against `AppState` rather than over gRPC: every one of these is about
//! what the poller and the ledger do to each other, and a wire in the middle
//! would only make the interleaving harder to say.
use std::collections::BTreeMap;
use std::sync::Arc;
use monetize::testing::{purchase, Answer, MockCloud, MockPay, MockProduct, NOW};
use monetize::transaction::Outcome;
use monetize_product::{State, TenantId};
use monetize_server::poller::tick;
use monetize_server::registry::Registry;
use monetize_server::state::AppState;
const DAY: u64 = 24 * 60 * 60 * 1000;
struct Rig {
state: Arc<AppState>,
product: Arc<MockProduct>,
pay: MockPay,
/// The instant the first period runs out.
paid_until: u64,
}
impl Rig {
/// alice bought a period at [`NOW`] and holds the disk — a shop customer,
/// one month in.
fn paying() -> Rig {
let product = Arc::new(MockProduct::gunnar());
// The first period really buys a disk.
let cloud = MockCloud::new();
cloud.answer_all(Answer::Provisioned);
let mut state = AppState::in_memory(Registry::new().shared_product(product.clone()).cloud(cloud));
*state.terms.write().unwrap() = monetize_server::state::TermsStore::fixture();
state.clock = Box::new(|| NOW);
let state = Arc::new(state);
let pay = MockPay::new();
let out = state.transaction(state.registry.products().next().unwrap(), monetize_pay::Vendor::of(&pay)).run(&purchase("alice")).expect("the first period must sell");
assert!(matches!(out, Outcome::Paid { .. }), "{out:?}");
let paid_until = state.ledger.entitlement("gunnar", "alice").unwrap().unwrap().paid_until_unix_ms.unwrap();
Rig { state, product, pay, paid_until }
}
fn at(&mut self, now: u64) {
let s = Arc::get_mut(&mut self.state).expect("no other handle while the clock is set");
s.clock = Box::new(move || now);
}
fn product(&self) -> monetize_product::Served<'_> {
self.state.registry.products().next().unwrap()
}
fn renew(&self) -> Outcome {
let terms = self.state.terms.read().unwrap().clone();
let host = terms.host("gunnar");
let t = terms.terms("gunnar", terms.default_market, &host);
let tx = self.state.transaction(self.product(), monetize_pay::Vendor::of(&self.pay));
let q = tx.renewal(&TenantId("alice".into()), &t, None).expect("the renewal must quote");
assert!(q.purchase.delta.is_empty(), "a renewal sells no new room");
self.state.transaction(self.product(), monetize_pay::Vendor::of(&self.pay)).buy(&q.purchase).expect("the renewal must sell")
}
fn state_now(&self) -> State {
self.state.ledger.entitlement("gunnar", "alice").unwrap().unwrap().state
}
}
/// ★ **A SUSPENDED SHOP CUSTOMER RENEWS AND COMES BACK UP.**
///
/// The whole point of the verb. Before the audit lane a paid signup was never
/// moved down the ladder at all, so this tenant did not exist; now they do, and
/// this is what has to happen to them when they pay.
#[test]
fn a_suspended_tenant_renews_and_comes_back_up() {
let mut rig = Rig::paying();
// The ladder does its work: grace, then suspended.
rig.at(rig.paid_until + 15 * DAY);
let t = tick(&rig.state);
assert_eq!(rig.state_now(), State::Suspended, "the fixture must renew from SUSPENDED");
assert_eq!(t.state_changes, 1, "the ladder moved them");
assert_eq!(rig.product.pushed().last().unwrap().state, State::Suspended);
// THEY PAY.
let out = rig.renew();
let Outcome::Paid { fact, .. } = &out else { panic!("{out:?}") };
assert_eq!(fact.state, State::Paid, "a renewal puts them back on the ladder");
assert!(fact.source.starts_with("payment:"), "and the fact says money did it: {}", fact.source);
assert_eq!(rig.state_now(), State::Paid, "monetize's own answer moved");
assert_eq!(rig.product.pushed().last().unwrap().state, State::Paid, "and the appliance was told — it does no clock arithmetic of its own");
// The ladder leaves them be: Paid stands.
let t = tick(&rig.state);
assert_eq!(t.state_changes, 0);
assert_eq!(rig.state_now(), State::Paid);
// And the tick after it is quiet: the renewal is not a thing that repeats.
let t = tick(&rig.state);
assert_eq!(t.state_changes, 0);
}
/// ★ **A RENEWAL THAT LANDS WHILE A TICK IS DECIDING WINS — ON BOTH SIDES.**
///
/// The race the audit lane fixed, from the other side, and it had a second half
/// nobody had measured. The compare-and-swap closes the window between the
/// ladder READING a fact and STORING its verdict. It cannot close the window
/// between storing and the PUSH LANDING, because a push is a gRPC round trip
/// and a payment lands inside it just as readily:
///
/// the tick stores Suspended and starts pushing it ·
/// the renewal clears, blind-writes Paid (`Step::EntitlementPushed`) and
/// pushes Paid to the appliance ·
/// the tick's push arrives LAST.
///
/// MEASURED before this lane: monetize held Paid, the APPLIANCE held Suspended,
/// and nothing was pending — the renewal's own commit had cleared the pending
/// row, and the ladder's successful push cleared it again. A customer who had
/// just paid, locked out on the box, with no row anywhere that would ever fix
/// it.
///
/// So the ladder acknowledges only ITS OWN fact. When a newer one replaced it
/// in flight, the newer one is left pending and the retry delivers it.
#[test]
fn a_renewal_that_lands_during_a_tick_beats_the_ladder_and_is_not_queued() {
let mut rig = Rig::paying();
// One day into grace: the tick will want to move them to Grace…
rig.at(rig.paid_until + 15 * DAY);
// …and the renewal lands while that push is in flight.
let terms = rig.state.terms.read().unwrap().clone();
let host = terms.host("gunnar");
let t = terms.terms("gunnar", terms.default_market, &host);
let quote = rig.state.transaction(rig.product(), monetize_pay::Vendor::of(&rig.pay)).renewal(&TenantId("alice".into()), &t, None).unwrap();
let purchase = quote.purchase.clone();
let weak = Arc::downgrade(&rig.state);
let pay = MockPay::new();
*rig.product.during_push.lock().unwrap() = Some(Box::new(move || {
let s = weak.upgrade().expect("the state outlives the tick");
let product = s.registry.products().next().unwrap();
let out = s.transaction(product, monetize_pay::Vendor::of(&pay)).buy(&purchase).expect("the renewal must sell mid-tick");
assert!(matches!(out, Outcome::Paid { .. }), "{out:?}");
}));
let t = tick(&rig.state);
// THE ASSERTIONS THAT ARE ABOUT MONEY.
assert_eq!(rig.state_now(), State::Paid, "the tick suspended a tenant who had just renewed");
assert_eq!(t.ladder_superseded, 1, "the supersede must be noticed, and counted");
// **THE HAZARD, AS IT REALLY HAPPENS, IN THE ORDER IT HAPPENS.** The
// ladder's Suspended lands on the appliance AFTER the renewal's Paid —
// nothing can un-send bytes already in flight — and the repair is the
// pending row the ladder leaves for the fact that superseded it, which
// `retry_pushes` delivers at the end of this very tick.
let landed: Vec<State> = rig.product.pushed().iter().map(|f| f.state).collect();
assert_eq!(
landed,
vec![State::Paid, State::Paid, State::Suspended, State::Paid],
"the first period, the renewal, the ladder's late Suspended, and the retry that put it right"
);
assert_eq!(t.pushes_retried, 1, "the newer fact must be re-pushed, or the box keeps the older one for good");
assert_eq!(rig.product.pushed().last().unwrap().state, State::Paid, "the appliance ends up holding what the customer paid for");
assert!(rig.state.ledger.pending_push("gunnar", "alice").unwrap().is_none(), "nothing left to do");
// Stable: the next tick moves nothing and pushes nothing.
let before = rig.product.push_attempts().len();
let t = tick(&rig.state);
assert_eq!((t.state_changes, t.ladder_superseded, t.pushes_retried), (0, 0, 0));
assert_eq!(rig.product.push_attempts().len(), before, "a settled tenant costs no push");
assert_eq!(rig.state_now(), State::Paid);
}
/// ★ **A FULL APPLIANCE IS STILL PAYABLE, AND A RENEWAL QUEUES NO GROWTH.**
///
/// Two halves of one rule: a renewal delivers nothing that was not delivered a
/// month ago. `Signup.Start` asks the box for room and refuses when there is
/// none; asking here would mean a box that has filled up can never be paid for
/// again — and the growth batcher must not be handed a job either, because
/// `on_paid_order` sizes a job from the DELTA and a renewal's delta is empty.
#[test]
fn a_full_box_still_takes_a_renewal_and_no_room_is_asked_for_or_queued() {
let mut rig = Rig::paying();
rig.at(rig.paid_until + 15 * DAY);
tick(&rig.state);
*rig.product.refuse_absorb.lock().unwrap() = Some("gunnar: the data set is full; an operator must extend the filesystem".into());
let asked = rig.product.absorb_asked().len();
let out = rig.renew();
assert!(matches!(out, Outcome::Paid { .. }), "a full box must still be payable: {out:?}");
assert_eq!(rig.product.absorb_asked().len(), asked, "can_absorb must not be asked for a renewal");
assert_eq!(rig.state_now(), State::Paid);
// An ORDER at the same instant is still refused — the refuse-twin, so the
// assertion above is not agreeing with a box that would have said yes.
let order = monetize::order::Order { product: "gunnar".into(), tenant: TenantId("alice".into()), caps: BTreeMap::from([("pack_bytes".to_string(), 100u64 << 30)]) };
let terms = rig.state.terms.read().unwrap().clone();
let host = terms.host("gunnar");
let t = terms.terms("gunnar", terms.default_market, &host);
let q = rig.state.transaction(rig.product(), monetize_pay::Vendor::of(&rig.pay)).quote(&order, &t, rig.state.now(), None).unwrap();
assert!(q.serve.is_err(), "the box must really be full, or the renewal proves nothing");
}