Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/monetize

crates/monetize-server/tests/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");
}