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/src/testing.rs

//! **Test doubles the server's own tests and the console's need.**
//!
//! * [`CallbackPay`] — a vendor that needs the CUSTOMER: `reserve` succeeds,
//!   `capture` answers `Reserved` until the customer has authorized at the vendor
//!   ([`CallbackPay::authorize`]), and the vendor tells us by webhook. Its
//!   `parse_callback` verifies a shared secret in constant time and MUTATES NOTHING
//!   — a verified callback is a fact about the vendor's state, not a command.
//!   It can produce every arm the webhook answers differently, the `Unavailable`
//!   one (`{"thin": …}`) included, so no branch of [`crate::callback`] is
//!   unreachable from a test.
//! * [`TestServer`] — the real router on an ephemeral loopback port inside the
//!   caller's tokio runtime.
//! * [`http_post`] — a raw HTTP/1.1 POST over `std::net`, so the webhook is tested
//!   on the wire without a client crate.

/// **A fake estate main**: the report and the acks a pulled product is answered from (A32-13).
pub mod estate;
/// **The flock: a parametric TEST product of Gunnar's shape**, its twin optional (A4-FACTS).
pub mod flock;
/// **A Gunnar-shaped TEST estate guardian** (the native plugins' rules, for the tests).
pub mod guardian;
/// **Gunnar- and Holger-shaped TEST lifecycles** the estate_order suite sells (v2.0 P2, A28 H6).
pub mod lifecycles;
/// **Gunnar- and Holger-shaped TEST products** that dial nothing (v2.0 P2, A28 H6).
pub mod products;

use std::collections::BTreeMap;
use std::net::SocketAddr;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, Mutex};

use monetize_pay::{
    CallbackEvent, ChargeHandle, ChargeRequest, ChargeStatus, Market, Money, PayError, RawCallback,
};

use crate::state::AppState;

pub const SIGNATURE_HEADER: &str = "x-mockpay-signature";

/// Cheap to clone: one vendor, shared between the registry and the test.
#[derive(Clone)]
pub struct CallbackPay {
    inner: Arc<Inner>,
}

struct Inner {
    secret: String,
    /// **The refund side of the vendor** — see [`VendorRefunds`].
    refunds: VendorRefunds,
    /// **A per-order checkout URL, off by default.** Klarna and Stripe mint one
    /// per reservation; `invoice` has none. Both shapes have to be reachable
    /// from a test, and a double that always answered would make the config
    /// template unreachable — the fallback is still the right answer for a
    /// vendor with one fixed checkout page.
    checkout_prefix: Option<String>,
    next: AtomicU64,
    /// Vendor-side truth by handle id.
    state: Mutex<BTreeMap<String, ChargeStatus>>,
    /// Handles the customer has authorized at the vendor's checkout page.
    authorized: Mutex<Vec<String>>,
    captures: AtomicU64,
    /// **Every signup checkout this vendor was asked to mint.** Recorded rather
    /// than counted: what a `Signup.Start` test has to see is the NAME and the
    /// price that reached the vendor, because the whole feature is that the name
    /// gets there before the card does.
    signup_orders: Mutex<Vec<monetize_pay::signup::SignupOrder>>,
    /// **The payer every reservation named** (`ChargeRequest::payer`), in
    /// order: what a hosted page is pre-filled with (Stripe's
    /// `customer_email`).
    payers: Mutex<Vec<Option<String>>>,
    /// **What this vendor says it took**, for `paid_since`: the facts a test
    /// puts here with [`CallbackPay::report_paid`]. Empty means the vendor
    /// answers an empty list, which is a vendor that CAN be asked and took
    /// nothing — not the default "cannot be asked"; that arm is the open
    /// vendors' and `monetize_pay::reconcile`'s own test holds it.
    paid: Mutex<Vec<monetize_pay::reconcile::PaidAtVendor>>,
    /// Every `since` this vendor was asked from, in order — so a test can pin
    /// the window the reconciler uses rather than infer it.
    asked_since: Mutex<Vec<i64>>,
    /// **Which ledger the double's "key" sees** (the test button): `None` cannot
    /// say (the default, as a real vendor nobody taught), `Some(false)` test.
    livemode: Mutex<Option<bool>>,
    /// Every signup the double paid with its test payment method, in order.
    test_paid: Mutex<Vec<monetize_pay::signup::SignupOrder>>,
    /// ★ The card check (owner 2026-10-15): every card-check page minted, by session, with
    /// where it stands — `Open` until a test checks a card on it ([`CallbackPay::check_card`]).
    card_checks: Mutex<
        BTreeMap<
            String,
            (
                monetize_pay::signup::SignupOrder,
                monetize_pay::signup::CardCheck,
            ),
        >,
    >,
    /// ★ Every paid signup checkout minted, by session, with where it stands at the "vendor"
    /// (owner 2026-10-15, B): `Open` until a test pays it ([`CallbackPay::pay_checkout`]) or lets it
    /// expire ([`CallbackPay::expire_checkout`]).
    checkouts: Mutex<
        BTreeMap<
            String,
            (
                monetize_pay::signup::SignupOrder,
                monetize_pay::signup::SignupAtVendor,
            ),
        >,
    >,
}

impl CallbackPay {
    fn i(&self) -> &Inner {
        &self.inner
    }
}

/// **What this vendor refunded, keyed the way Stripe keys it.**
///
/// A real vendor makes a refund idempotent on the key it is sent under —
/// Stripe's is `<handle>:refund:<amount_minor>` (monetize-impl
/// `stripe/mod.rs`, `Stripe::refund`) — so a second call with the same key is
/// answered from the first and gives nothing back twice. This double keeps
/// the same promise, and records every CALL beside every refund MADE, so a
/// test can tell "asked twice, refunded once" from "refunded twice".
#[derive(Default)]
struct VendorRefunds {
    /// Sessions this vendor took a signup's money for ([`CallbackPay::took_signup`]).
    taken: Mutex<Vec<String>>,
    /// Every idempotency key `refund` was called with, in order.
    calls: Mutex<Vec<String>>,
    /// The distinct keys a refund was actually made under.
    made: Mutex<std::collections::BTreeSet<String>>,
    /// The next refund is MADE and its reply is lost (`Unavailable`): the
    /// crash between "the vendor said yes" and "we wrote it down".
    lose_next_reply: std::sync::atomic::AtomicBool,
}

impl CallbackPay {
    /// **The vendor holds a signup's money for `session`** — what its
    /// dashboard would show after the webhook. `refund` on a session it never
    /// took answers `UnknownHandle`, as a real vendor does.
    pub fn took_signup(&self, session: &str) {
        self.i()
            .refunds
            .taken
            .lock()
            .unwrap()
            .push(session.to_owned());
    }
    /// Every idempotency key `refund` was called with, in order.
    pub fn refund_calls(&self) -> Vec<String> {
        self.i().refunds.calls.lock().unwrap().clone()
    }
    /// How many refunds the vendor actually made (distinct keys).
    pub fn refunds_made(&self) -> usize {
        self.i().refunds.made.lock().unwrap().len()
    }
    /// The next refund goes through at the vendor and its reply never arrives.
    pub fn lose_next_refund_reply(&self) {
        self.i()
            .refunds
            .lose_next_reply
            .store(true, Ordering::SeqCst);
    }
}

impl CallbackPay {
    pub fn new(secret: &str) -> Self {
        Self {
            inner: Arc::new(Inner {
                secret: secret.into(),
                refunds: VendorRefunds::default(),
                checkout_prefix: None,
                next: AtomicU64::new(1),
                state: Mutex::new(BTreeMap::new()),
                authorized: Mutex::new(Vec::new()),
                captures: AtomicU64::new(0),
                signup_orders: Mutex::new(Vec::new()),
                payers: Mutex::new(Vec::new()),
                paid: Mutex::new(Vec::new()),
                asked_since: Mutex::new(Vec::new()),
                livemode: Mutex::new(None),
                test_paid: Mutex::new(Vec::new()),
                card_checks: Mutex::new(BTreeMap::new()),
                checkouts: Mutex::new(BTreeMap::new()),
            }),
        }
    }

    /// **A vendor that mints its own checkout URL per order**, `<prefix><handle
    /// id>`, the way Klarna's create-order reply does. Used to prove that the
    /// vendor's answer beats the `checkout_url` template in `monetize.toml`.
    pub fn with_checkout(secret: &str, prefix: &str) -> Self {
        CallbackPay {
            inner: Arc::new(Inner {
                secret: secret.into(),
                refunds: VendorRefunds::default(),
                checkout_prefix: Some(prefix.to_string()),
                next: AtomicU64::new(1),
                state: Mutex::new(BTreeMap::new()),
                authorized: Mutex::new(Vec::new()),
                captures: AtomicU64::new(0),
                signup_orders: Mutex::new(Vec::new()),
                payers: Mutex::new(Vec::new()),
                paid: Mutex::new(Vec::new()),
                asked_since: Mutex::new(Vec::new()),
                livemode: Mutex::new(None),
                test_paid: Mutex::new(Vec::new()),
                card_checks: Mutex::new(BTreeMap::new()),
                checkouts: Mutex::new(BTreeMap::new()),
            }),
        }
    }

    /// **The double's key is a TEST key (`Some(false)`), a LIVE one
    /// (`Some(true)`), or it cannot say (`None`, the default).**
    pub fn set_livemode(&self, mode: Option<bool>) {
        *self.i().livemode.lock().unwrap() = mode;
    }
    /// The test payment method this double answers on a test key.
    pub const TEST_PAYER: &'static str = "pm_card_visa";
    /// Every signup paid with the test payment method.
    pub fn test_paid(&self) -> Vec<monetize_pay::signup::SignupOrder> {
        self.i().test_paid.lock().unwrap().clone()
    }

    /// **The buyer checked a card on the card-check page `session`**, nothing charged: the
    /// vendor's fingerprint of that card is `fingerprint` (the same card, the same value).
    pub fn check_card(&self, session: &str, fingerprint: &str) {
        let mut all = self.i().card_checks.lock().unwrap();
        let (_, c) = all
            .get_mut(session)
            .expect("a card check this double minted");
        c.state = monetize_pay::signup::CardCheckState::Checked;
        c.fingerprint = fingerprint.to_owned();
        c.customer = format!("cus_{fingerprint}");
        c.payment_method = format!("pm_{fingerprint}");
        c.reference = format!("seti_{session}");
    }
    /// **The buyer paid the signup checkout `session`** at the vendor — the vendor says PAID
    /// from now on (no webhook is sent: a test that wants one sends it).
    pub fn pay_checkout(&self, session: &str) {
        let mut all = self.i().checkouts.lock().unwrap();
        let (order, at) = all.get_mut(session).expect("a checkout this double minted");
        *at = monetize_pay::signup::SignupAtVendor::Paid(Box::new(monetize_pay::PaidSignup {
            vendor: "mockpay".into(),
            session: session.to_owned(),
            reference: format!("pi_{session}"),
            email: order.email.clone(),
            username: order.username.clone(),
            product: order.product.clone(),
            plan: order.plan.clone(),
            amount: order.amount,
            tax: monetize_pay::Money {
                minor: 0,
                currency: order.amount.currency,
            },
            paid_at_unix_ms: 1_788_436_800_000,
            customer_country: None,
        }));
    }
    /// **The signup checkout `session` expired unpaid.**
    pub fn expire_checkout(&self, session: &str) {
        self.i()
            .checkouts
            .lock()
            .unwrap()
            .get_mut(session)
            .expect("a checkout this double minted")
            .1 = monetize_pay::signup::SignupAtVendor::Closed;
    }

    /// **The card-check page `session` closed with no card checked.**
    pub fn expire_card_check(&self, session: &str) {
        let mut all = self.i().card_checks.lock().unwrap();
        all.get_mut(session)
            .expect("a card check this double minted")
            .1
            .state = monetize_pay::signup::CardCheckState::Expired;
    }
    /// Every card-check page minted, oldest session first: the order it was minted for.
    pub fn card_check_orders(&self) -> Vec<(String, monetize_pay::signup::SignupOrder)> {
        self.i()
            .card_checks
            .lock()
            .unwrap()
            .iter()
            .map(|(k, (o, _))| (k.clone(), o.clone()))
            .collect()
    }

    /// The customer authorized at the vendor: the next `capture` draws the money.
    pub fn authorize(&self, handle_id: &str) {
        self.i()
            .authorized
            .lock()
            .unwrap()
            .push(handle_id.to_string());
    }

    /// **The customer never paid and the vendor gave up** — a hosted checkout
    /// that EXPIRED: the vendor's own word for the charge is `Released` from
    /// now on, which is what the server reads back before it closes anything.
    pub fn expire(&self, handle_id: &str) {
        self.i()
            .state
            .lock()
            .unwrap()
            .insert(handle_id.to_string(), ChargeStatus::Released);
    }

    /// How many times the server asked the vendor to capture.
    pub fn captures(&self) -> u64 {
        self.i().captures.load(Ordering::SeqCst)
    }

    /// Every signup checkout this vendor was asked to mint, in order.
    pub fn signup_orders(&self) -> Vec<monetize_pay::signup::SignupOrder> {
        self.i().signup_orders.lock().unwrap().clone()
    }

    /// The payer each reservation named, in order.
    pub fn payers(&self) -> Vec<Option<String>> {
        self.i().payers.lock().unwrap().clone()
    }

    /// **The vendor drew the money without us hearing about it.** The state a
    /// dropped callback, a retried webhook or a crash between `capture` and the
    /// ledger write leaves behind — and the one a resume must DISCOVER by asking
    /// rather than act on by capturing again.
    pub fn force_captured(&self, handle_id: &str) {
        self.i().state.lock().unwrap().insert(
            handle_id.to_string(),
            ChargeStatus::Captured {
                at_unix_ms: 1_788_436_800_000,
                vendor_ref: format!("out-of-band-{handle_id}"),
            },
        );
    }

    pub fn vendor_state(&self, handle_id: &str) -> Option<ChargeStatus> {
        self.i().state.lock().unwrap().get(handle_id).cloned()
    }

    /// The body a real vendor would POST for `handle` after `event`
    /// (`"authorized" | "captured" | "released"`), and the header that signs it.
    pub fn callback_body(reference: &str, handle_id: &str, event: &str) -> Vec<u8> {
        serde_json::to_vec(
            &serde_json::json!({ "reference": reference, "handle": handle_id, "event": event }),
        )
        .unwrap()
    }
    /// The body a vendor would POST when money went back or is contested
    /// (`refunded` | `dispute_opened` | `dispute_won` | `dispute_lost`), about
    /// the order `reference` held under `handle_id`.
    pub fn reversal_body(
        reference: &str,
        handle_id: &str,
        event: &str,
        amount_minor: u64,
    ) -> Vec<u8> {
        serde_json::to_vec(&serde_json::json!({ "reference": reference, "handle": handle_id, "event": event, "amount": amount_minor })).unwrap()
    }
    /// The body a vendor whose hosted page sold a SIGNUP would POST. The
    /// `PaidSignup` goes on the wire as its own serde shape, which is what a
    /// real impl builds out of the vendor's session object.
    pub fn signup_body(signup: &monetize_pay::PaidSignup) -> Vec<u8> {
        serde_json::to_vec(&serde_json::json!({ "signup": signup })).unwrap()
    }
    pub fn signed_headers(&self) -> Vec<(String, String)> {
        vec![
            (SIGNATURE_HEADER.into(), self.i().secret.clone()),
            ("content-type".into(), "application/json".into()),
        ]
    }

    /// **The vendor took this money.** From here on `paid_since` answers it to
    /// every caller whose window covers `paid_at_unix_ms` — a fact list, the
    /// way a real vendor answers, never a diff. Nothing else in this double
    /// changes: the state a callback would have reported is a different thing
    /// from what the vendor's ledger says, and a test that wants both sets both.
    pub fn report_paid(&self, fact: monetize_pay::reconcile::PaidAtVendor) {
        self.i().paid.lock().unwrap().push(fact);
    }

    /// Every `since` the reconciler asked this vendor from, in order.
    pub fn asked_since(&self) -> Vec<i64> {
        self.i().asked_since.lock().unwrap().clone()
    }

    /// **A vendor fact in the shape Stripe's Checkout Session gives**, for the
    /// reconcile tests: 125.00 SEK with 25.00 of it tax — the incident's
    /// amount — labelled as `metadata` names it.
    pub fn paid_fact(
        session: &str,
        paid_at_unix_ms: i64,
        metadata: &[(&str, &str)],
    ) -> monetize_pay::reconcile::PaidAtVendor {
        monetize_pay::reconcile::PaidAtVendor {
            vendor: "mockpay".into(),
            session: session.into(),
            reference: format!("pi_{session}"),
            paid_at_unix_ms,
            amount_minor: 12_500,
            tax_minor: 2_500,
            currency: "sek".into(),
            livemode: true,
            metadata: metadata
                .iter()
                .map(|(k, v)| ((*k).to_owned(), (*v).to_owned()))
                .collect(),
            email: "buyer@example.test".into(),
            customer_country: None,
        }
    }
}

impl CallbackPay {
    pub fn vendor(&self) -> &'static str {
        "mockpay"
    }

    pub fn live(&self) -> Option<bool> {
        *self.i().livemode.lock().unwrap()
    }
    pub fn test_payer(&self) -> Option<String> {
        (self.live() == Some(false)).then(|| Self::TEST_PAYER.to_owned())
    }
    /// Pays as a real vendor's test key does, and answers what its webhook would.
    pub fn pay_signup_as_test(
        &self,
        order: &monetize_pay::signup::SignupOrder,
    ) -> Result<Option<monetize_pay::PaidSignup>, PayError> {
        if self.live() != Some(false) {
            return Err(PayError::Declined(
                "not a test key: nothing is paid without a person".into(),
            ));
        }
        self.i().test_paid.lock().unwrap().push(order.clone());
        let n = self.i().next.fetch_add(1, Ordering::SeqCst);
        Ok(Some(monetize_pay::PaidSignup {
            vendor: "mockpay".into(),
            session: format!("pi_test_{n}"),
            reference: format!("pi_test_{n}"),
            email: order.email.clone(),
            username: order.username.clone(),
            product: order.product.clone(),
            plan: order.plan.clone(),
            amount: order.amount,
            tax: monetize_pay::Money {
                minor: 0,
                currency: order.amount.currency,
            },
            paid_at_unix_ms: 1_788_436_800_000,
            customer_country: None,
        }))
    }

    /// The facts [`CallbackPay::report_paid`] put here whose `paid_at` is in
    /// the window. A fact list, answered again on every call — the reconciler
    /// is what makes the second answer a no-op, and this double must not help.
    pub fn paid_from(
        &self,
        since_unix_ms: i64,
    ) -> Result<Vec<monetize_pay::reconcile::PaidAtVendor>, PayError> {
        self.i().asked_since.lock().unwrap().push(since_unix_ms);
        Ok(self
            .i()
            .paid
            .lock()
            .unwrap()
            .iter()
            .filter(|f| f.paid_at_unix_ms >= since_unix_ms)
            .cloned()
            .collect())
    }
    pub fn markets(&self) -> &[Market] {
        &[Market::Se, Market::Fi]
    }
    pub fn reserve(&self, req: &ChargeRequest) -> Result<ChargeHandle, PayError> {
        self.i().payers.lock().unwrap().push(req.payer.clone());
        let id = format!("res-{}", self.i().next.fetch_add(1, Ordering::SeqCst));
        self.i()
            .state
            .lock()
            .unwrap()
            .insert(id.clone(), ChargeStatus::Reserved);
        // The test payment method pays at once, with nobody at a page — on a test key only.
        if req.payer.as_deref() == Some(Self::TEST_PAYER) {
            if *self.i().livemode.lock().unwrap() != Some(false) {
                return Err(PayError::Declined(
                    "a test payment method on a key that is not a test key".into(),
                ));
            }
            self.i().authorized.lock().unwrap().push(id.clone());
        }
        Ok(ChargeHandle {
            vendor: "mockpay".into(),
            id,
        })
    }
    pub fn draw(&self, handle: &ChargeHandle) -> Result<ChargeStatus, PayError> {
        self.i().captures.fetch_add(1, Ordering::SeqCst);
        let mut state = self.i().state.lock().unwrap();
        let Some(current) = state.get(&handle.id).cloned() else {
            return Err(PayError::UnknownHandle(handle.clone()));
        };
        let next = match current {
            ChargeStatus::Reserved if self.i().authorized.lock().unwrap().contains(&handle.id) => {
                ChargeStatus::Captured {
                    at_unix_ms: 1_788_436_800_000,
                    vendor_ref: format!("cap-{}", handle.id),
                }
            }
            // The customer has not authorized yet: the vendor says so, honestly.
            other => other,
        };
        state.insert(handle.id.clone(), next.clone());
        Ok(next)
    }
    pub fn unlock(&self, handle: &ChargeHandle) -> Result<ChargeStatus, PayError> {
        let mut state = self.i().state.lock().unwrap();
        let Some(current) = state.get(&handle.id).cloned() else {
            return Err(PayError::UnknownHandle(handle.clone()));
        };
        let next = match current {
            ChargeStatus::Reserved => ChargeStatus::Released,
            other => other,
        };
        state.insert(handle.id.clone(), next.clone());
        Ok(next)
    }
    pub fn charge_status(&self, handle: &ChargeHandle) -> Result<ChargeStatus, PayError> {
        self.i()
            .state
            .lock()
            .unwrap()
            .get(&handle.id)
            .cloned()
            .ok_or_else(|| PayError::UnknownHandle(handle.clone()))
    }
    /// The callback-driven mock takes nothing and says so.
    pub fn settled(&self, _handle: &ChargeHandle) -> Option<monetize_pay::Settlement> {
        None
    }
    pub fn parse_callback(&self, raw: RawCallback<'_>) -> Result<CallbackEvent, PayError> {
        let sig = raw
            .headers
            .iter()
            .find(|(k, _)| k.eq_ignore_ascii_case(SIGNATURE_HEADER))
            .map(|(_, v)| v.as_str())
            .unwrap_or("");
        if sig.is_empty()
            || !constant_time_eq::constant_time_eq(sig.as_bytes(), self.i().secret.as_bytes())
        {
            return Err(PayError::Unverified(
                "mockpay: signature header missing or wrong".into(),
            ));
        }
        let v: serde_json::Value = serde_json::from_slice(raw.body)
            .map_err(|e| PayError::Unverified(format!("mockpay: body is not JSON: {e}")))?;
        // **A verified event that carries a PAID SIGNUP.** Past the signature
        // check, so it exercises the arm exactly where a real vendor produces
        // it: the body is the vendor's own from here on, and this one describes
        // a customer that does not exist yet rather than an attempt we hold.
        // **A verified event this parser cannot finish reading.** Stripe's thin
        // events carry an id and nothing else, so a real parser has to fetch the
        // object from the vendor — and when that call fails there is a delivery
        // that verified and still could not be turned into a `CallbackEvent`.
        // It is the arm `callback.rs` answers 503 on (our outage, so the vendor
        // retries), and without a double that can produce it that path is
        // unreachable from a test. Keyed on a field no other test sends, past the signature
        // check, exactly where a real vendor produces it.
        if v.get("thin").is_some() {
            return Err(PayError::Unavailable(
                "mockpay: the event is thin and the vendor could not be asked for the rest of it"
                    .into(),
            ));
        }
        // **A verified event whose payload this parser does not read** — the
        // 400 arm, now that a vendor that could not be asked is a 503.
        if v.get("garbled").is_some() {
            return Err(PayError::Declined(
                "mockpay: the payload is not one this parser reads".into(),
            ));
        }
        if let Some(s) = v.get("signup") {
            let signup: monetize_pay::PaidSignup =
                serde_json::from_value(s.clone()).map_err(|e| {
                    PayError::Ignored(format!("mockpay: signup payload not understood: {e}"))
                })?;
            return Err(PayError::Signup(Box::new(signup)));
        }
        let field = |k: &str| {
            v.get(k)
                .and_then(|x| x.as_str())
                .map(str::to_string)
                .ok_or_else(|| PayError::Unverified(format!("mockpay: no {k}")))
        };
        let handle = ChargeHandle {
            vendor: "mockpay".into(),
            id: field("handle")?,
        };
        let status = match field("event")?.as_str() {
            "authorized" => ChargeStatus::Reserved,
            "captured" => ChargeStatus::Captured {
                at_unix_ms: 1_788_436_800_000,
                vendor_ref: format!("cap-{}", handle.id),
            },
            "released" => ChargeStatus::Released,
            // **A refund, a dispute, a chargeback.** Verified, about a charge
            // this deployment really holds, and there is nothing in monetize
            // that acts on it — which is exactly what `PayError::Ignored`
            // means and why `callback.rs` answers it 200. Produced here so the
            // arm is reachable from a test; see
            // `callback_ordering::known_gap_a_refund_after_the_grant_is_
            // answered_200_and_changes_nothing`.
            ev @ ("refunded" | "dispute_opened" | "dispute_won" | "dispute_lost") => {
                let kind = match ev {
                    "refunded" => monetize_pay::ReversalKind::Refunded,
                    "dispute_opened" => monetize_pay::ReversalKind::DisputeOpened,
                    "dispute_won" => monetize_pay::ReversalKind::DisputeWon,
                    _ => monetize_pay::ReversalKind::DisputeLost,
                };
                let amount = v.get("amount").and_then(|a| a.as_u64()).unwrap_or(0);
                let id = if kind == monetize_pay::ReversalKind::Refunded {
                    format!("re-{}-{amount}", handle.id)
                } else {
                    format!("dp-{}", handle.id)
                };
                return Err(PayError::Reversal(Box::new(monetize_pay::Reversal {
                    vendor: "mockpay".into(),
                    handle: handle.clone(),
                    reference: field("reference")?,
                    kind,
                    id,
                    amount: Money {
                        minor: amount,
                        currency: monetize_pay::Currency::Sek,
                    },
                    reason: "mockpay".into(),
                })));
            }
            other => {
                return Err(PayError::Unverified(format!(
                    "mockpay: unknown event {other:?}"
                )))
            }
        };
        Ok(CallbackEvent {
            reference: field("reference")?,
            handle,
            status,
        })
    }
    /// A signup's session ([`CallbackPay::took_signup`]) is refunded once per
    /// key, Stripe's way ([`VendorRefunds`]); a purchase's handle answers its
    /// status, as it always has.
    pub fn give_back(
        &self,
        handle: &ChargeHandle,
        amount: Money,
    ) -> Result<ChargeStatus, PayError> {
        let r = &self.i().refunds;
        let key = format!("{}:refund:{}", handle.id, amount.minor);
        r.calls.lock().unwrap().push(key.clone());
        if !r.taken.lock().unwrap().contains(&handle.id) {
            return self.charge_status(handle);
        }
        r.made.lock().unwrap().insert(key);
        if r.lose_next_reply.swap(false, Ordering::SeqCst) {
            return Err(PayError::Unavailable(
                "mockpay: the refund went through and its reply was lost".into(),
            ));
        }
        Ok(ChargeStatus::Released)
    }
    /// `None` unless the double was built with [`CallbackPay::with_checkout`],
    /// and then a URL that names the handle — a vendor-minted page, not one a
    /// template could have produced.
    pub fn redirect(&self, handle: &ChargeHandle) -> Option<String> {
        let prefix = self.i().checkout_prefix.as_ref()?;
        // Only for a handle this vendor minted, like every real provider.
        self.i().state.lock().unwrap().get(&handle.id)?;
        Some(format!("{prefix}{}", handle.id))
    }

    /// **A signup page, on the same condition the checkout URL has one**: a
    /// double built without [`CallbackPay::with_checkout`] is a vendor that sells
    /// no signups, which is the other half `Signup.Start` has to be provable
    /// against — that is the deployment whose console keeps its old buy link.
    ///
    /// The order is recorded either way, so the refusing path can still be
    /// asserted to have carried the right name.
    pub fn start_signup(
        &self,
        order: &monetize_pay::signup::SignupOrder,
    ) -> Result<Option<monetize_pay::signup::SignupCheckout>, PayError> {
        self.i().signup_orders.lock().unwrap().push(order.clone());
        let Some(prefix) = self.i().checkout_prefix.as_ref() else {
            return Ok(None);
        };
        let session = format!("cs_mock{}", self.i().next.fetch_add(1, Ordering::SeqCst));
        self.i().checkouts.lock().unwrap().insert(
            session.clone(),
            (order.clone(), monetize_pay::signup::SignupAtVendor::Open),
        );
        Ok(Some(monetize_pay::signup::SignupCheckout {
            url: format!("{prefix}{session}"),
            session,
        }))
    }

    /// **A card-check page, on the same condition a signup page has one** (a double built
    /// with [`CallbackPay::with_checkout`]); nothing is charged on it.
    pub fn start_card_check(
        &self,
        order: &monetize_pay::signup::SignupOrder,
    ) -> Result<Option<monetize_pay::signup::SignupCheckout>, PayError> {
        let Some(prefix) = self.i().checkout_prefix.as_ref() else {
            return Ok(None);
        };
        if order.amount.minor != 0 {
            return Err(PayError::Declined(format!(
                "mockpay: a card check charges nothing, and this order is {}",
                order.amount.major()
            )));
        }
        let session = format!("cs_card{}", self.i().next.fetch_add(1, Ordering::SeqCst));
        let open = monetize_pay::signup::CardCheck {
            session: session.clone(),
            state: monetize_pay::signup::CardCheckState::Open,
            fingerprint: String::new(),
            customer: String::new(),
            payment_method: String::new(),
            reference: String::new(),
        };
        self.i()
            .card_checks
            .lock()
            .unwrap()
            .insert(session.clone(), (order.clone(), open));
        Ok(Some(monetize_pay::signup::SignupCheckout {
            url: format!("{prefix}{session}"),
            session,
        }))
    }
    pub fn signup_at_vendor(
        &self,
        session: &str,
    ) -> Result<Option<monetize_pay::signup::SignupAtVendor>, PayError> {
        if self.i().checkout_prefix.is_none() {
            return Ok(None);
        }
        match self.i().checkouts.lock().unwrap().get(session) {
            Some((_, at)) => Ok(Some(at.clone())),
            None => Err(PayError::UnknownHandle(ChargeHandle {
                vendor: "mockpay".into(),
                id: session.to_owned(),
            })),
        }
    }
    pub fn card_check(
        &self,
        session: &str,
    ) -> Result<Option<monetize_pay::signup::CardCheck>, PayError> {
        if self.i().checkout_prefix.is_none() {
            return Ok(None);
        }
        match self.i().card_checks.lock().unwrap().get(session) {
            Some((_, c)) => Ok(Some(c.clone())),
            None => Err(PayError::UnknownHandle(ChargeHandle {
                vendor: "mockpay".into(),
                id: session.to_owned(),
            })),
        }
    }
}
impl monetize_pay::payment::Payment for CallbackPay {
    fn manifest(&self) -> monetize_pay::payment::PayManifest {
        monetize_pay::payment::PayManifest {
            vendor: self.vendor().to_string(),
            markets: self.markets().to_vec(),
            settings: Vec::new(),
            holds: true,
            test_payer: self.test_payer(),
        }
    }
    fn page(
        &self,
        charge: &monetize_pay::payment::Charge<'_>,
        _back: &monetize_pay::payment::Return,
    ) -> Result<monetize_pay::payment::Page, monetize_pay::PayError> {
        Ok(match charge {
            monetize_pay::payment::Charge::Order(req) => monetize_pay::payment::Page::Order {
                handle: self.reserve(req)?,
                url: None,
            },
            monetize_pay::payment::Charge::Signup(o) => {
                monetize_pay::payment::Page::Signup(self.start_signup(o)?)
            }
            monetize_pay::payment::Charge::CardCheck(o) => {
                monetize_pay::payment::Page::CardCheck(self.start_card_check(o)?)
            }
            monetize_pay::payment::Charge::TestSignup(o) => {
                monetize_pay::payment::Page::TestSignup(self.pay_signup_as_test(o)?)
            }
        })
    }
    fn status(
        &self,
        of: &monetize_pay::payment::Of<'_>,
    ) -> Result<monetize_pay::payment::Standing, monetize_pay::PayError> {
        Ok(match of {
            monetize_pay::payment::Of::Charge(h) => {
                monetize_pay::payment::Standing::Charge(self.charge_status(h)?)
            }
            monetize_pay::payment::Of::Page(h) => {
                monetize_pay::payment::Standing::Page(self.redirect(h))
            }
            monetize_pay::payment::Of::Signup { session } => {
                monetize_pay::payment::Standing::Signup(self.signup_at_vendor(session)?)
            }
            monetize_pay::payment::Of::CardCheck { session } => {
                monetize_pay::payment::Standing::CardCheck(self.card_check(session)?)
            }
        })
    }
    fn capture(
        &self,
        h: &monetize_pay::ChargeHandle,
    ) -> Result<monetize_pay::ChargeStatus, monetize_pay::PayError> {
        self.draw(h)
    }
    fn release(
        &self,
        h: &monetize_pay::ChargeHandle,
    ) -> Result<monetize_pay::ChargeStatus, monetize_pay::PayError> {
        self.unlock(h)
    }
    fn refund(
        &self,
        h: &monetize_pay::ChargeHandle,
        amount: monetize_pay::Money,
    ) -> Result<monetize_pay::ChargeStatus, monetize_pay::PayError> {
        self.give_back(h, amount)
    }
    fn callback(
        &self,
        raw: monetize_pay::RawCallback<'_>,
    ) -> Result<monetize_pay::CallbackEvent, monetize_pay::PayError> {
        self.parse_callback(raw)
    }
    fn settlement(&self, h: &monetize_pay::ChargeHandle) -> Option<monetize_pay::Settlement> {
        self.settled(h)
    }
    fn paid_since(
        &self,
        since_unix_ms: i64,
    ) -> Result<Vec<monetize_pay::reconcile::PaidAtVendor>, monetize_pay::PayError> {
        self.paid_from(since_unix_ms)
    }
    fn livemode(&self) -> Option<bool> {
        self.live()
    }
}

/// The real router on `127.0.0.1:0`, inside the current tokio runtime.
pub struct TestServer {
    pub addr: SocketAddr,
    shutdown: Option<tokio::sync::oneshot::Sender<()>>,
    task: Option<tokio::task::JoinHandle<()>>,
}

impl TestServer {
    pub async fn spawn(state: Arc<AppState>, token: &str) -> Self {
        Self::spawn_with_operator(state, token, "").await
    }

    /// The same, with an OPERATOR bearer beside the service one — what
    /// `SignupAdmin.Refund` / `Settle` take (`crate::auth::signup_admin_interceptor`).
    pub async fn spawn_with_operator(state: Arc<AppState>, token: &str, operator: &str) -> Self {
        let listener = tokio::net::TcpListener::bind("127.0.0.1:0")
            .await
            .expect("bind");
        let addr = listener.local_addr().unwrap();
        let (tx, rx) = tokio::sync::oneshot::channel::<()>();
        let token = Arc::new(token.to_string());
        let operator = Arc::new(operator.to_string());
        let task = tokio::spawn(async move {
            crate::serve_on_with(listener, state, token, operator, async {
                let _ = rx.await;
            })
            .await
            .expect("test server serves");
        });
        TestServer {
            addr,
            shutdown: Some(tx),
            task: Some(task),
        }
    }

    pub fn endpoint(&self) -> String {
        format!("http://{}", self.addr)
    }

    pub async fn stop(mut self) {
        if let Some(tx) = self.shutdown.take() {
            let _ = tx.send(());
        }
        if let Some(t) = self.task.take() {
            let _ = t.await;
        }
    }
}

/// One HTTP/1.1 POST over a plain socket. Returns `(status, body)`.
pub fn http_post(
    addr: SocketAddr,
    path: &str,
    headers: &[(String, String)],
    body: &[u8],
) -> std::io::Result<(u16, String)> {
    use std::io::{Read, Write};
    let mut s = std::net::TcpStream::connect(addr)?;
    let mut req = format!(
        "POST {path} HTTP/1.1\r\nHost: {addr}\r\nContent-Length: {}\r\nConnection: close\r\n",
        body.len()
    );
    for (k, v) in headers {
        req.push_str(&format!("{k}: {v}\r\n"));
    }
    req.push_str("\r\n");
    s.write_all(req.as_bytes())?;
    s.write_all(body)?;
    s.flush()?;
    let mut raw = Vec::new();
    s.read_to_end(&mut raw)?;
    let text = String::from_utf8_lossy(&raw).into_owned();
    let status = text
        .split_whitespace()
        .nth(1)
        .and_then(|c| c.parse().ok())
        .unwrap_or(0);
    let body = text
        .split_once("\r\n\r\n")
        .map(|(_, b)| b.to_string())
        .unwrap_or_default();
    Ok((status, body))
}

/// **A mock cloud under a provider name of the test's choosing** — two clouds
/// in one registry need two names, and `monetize::testing::MockCloud` answers
/// `mockcloud` for all of them. Everything but the name forwards.
pub struct NamedCloud(pub &'static str, pub monetize::testing::MockCloud);

impl NamedCloud {
    pub fn provider(&self) -> &'static str {
        self.0
    }
    pub fn zones(&self) -> &[monetize_cloud::Zone] {
        self.1.zones()
    }
    pub fn price_resource(
        &self,
        r: &monetize_cloud::Resource,
    ) -> Result<monetize_cloud::Price, monetize_cloud::CloudError> {
        monetize_cloud::Cloud::price(&self.1, &monetize_cloud::cloud::Priced::Resource(r))
    }
    pub fn price_ipv4(
        &self,
        zone: &monetize_cloud::Zone,
    ) -> Result<Option<monetize_cloud::Price>, monetize_cloud::CloudError> {
        self.1.price_ipv4(zone)
    }
    pub fn ensure(
        &self,
        reference: &str,
        r: &monetize_cloud::Resource,
    ) -> Result<monetize_cloud::Ensured, monetize_cloud::CloudError> {
        self.1.ensure(reference, r)
    }
    pub fn destroy_id(
        &self,
        id: &monetize_cloud::ResourceId,
    ) -> Result<(), monetize_cloud::CloudError> {
        monetize_cloud::ask::destroy_one(&self.1, id)
    }
    pub fn status(
        &self,
        id: &monetize_cloud::ResourceId,
    ) -> Result<monetize_cloud::ResourceStatus, monetize_cloud::CloudError> {
        self.1.status(id)
    }
    pub fn list_owned(&self) -> Result<monetize_cloud::Inventory, monetize_cloud::CloudError> {
        monetize_cloud::ask::account(&self.1)
    }
}
impl monetize_cloud::cloud::Cloud for NamedCloud {
    fn manifest(&self) -> monetize_cloud::cloud::CloudManifest {
        monetize_cloud::cloud::CloudManifest {
            provider: self.provider().to_string(),
            zones: self.zones().to_vec(),
            settings: Vec::new(),
            grow_refused: Some(monetize_cloud::grow::unsupported(self.provider()).to_string()),
        }
    }
    fn catalog(
        &self,
        zone: &monetize_cloud::Zone,
    ) -> Result<monetize_cloud::cloud::Catalog, monetize_cloud::CloudError> {
        Ok(monetize_cloud::cloud::Catalog {
            plans: Vec::new(),
            ipv4: self.price_ipv4(zone)?,
        })
    }
    fn price(
        &self,
        what: &monetize_cloud::cloud::Priced<'_>,
    ) -> Result<monetize_cloud::Price, monetize_cloud::CloudError> {
        match what {
            monetize_cloud::cloud::Priced::Resource(r) => self.price_resource(r),
            monetize_cloud::cloud::Priced::Plan(p) => monetize_cloud::price_estate_by_rows(
                p,
                |r| self.price_resource(r),
                self.price_ipv4(&p.zone)?,
            ),
            monetize_cloud::cloud::Priced::Grow(_set) => {
                Err(monetize_cloud::grow::unsupported(self.provider()))
            }
        }
    }
    fn lay(
        &self,
        _plan: &monetize_cloud::lay::EstatePlan,
        _journal: &monetize_cloud::grow::Journal,
    ) -> Result<monetize_cloud::lay::LaidEstate, monetize_cloud::CloudError> {
        Err(monetize_cloud::lay::unsupported(self.provider()))
    }
    fn change(
        &self,
        change: &monetize_cloud::cloud::Change,
        _journal: &monetize_cloud::grow::Journal,
    ) -> Result<monetize_cloud::cloud::Changed, monetize_cloud::CloudError> {
        Ok(match change {
            monetize_cloud::cloud::Change::Plan {
                server: _,
                plan: _,
                delta: _,
            } => {
                return Err(monetize_cloud::lay::unsupported_verb(
                    self.provider(),
                    "change_plan",
                ))
            }
            monetize_cloud::cloud::Change::Grow { set: _, estate: _ } => {
                return Err(monetize_cloud::grow::unsupported(self.provider()))
            }
            monetize_cloud::cloud::Change::Settle { set: _ } => {
                return Err(monetize_cloud::grow::unsupported(self.provider()))
            }
            monetize_cloud::cloud::Change::ForgetMedia { laid: _ } => {
                monetize_cloud::cloud::Changed::Forgotten(Vec::new())
            }
            monetize_cloud::cloud::Change::Ensure {
                reference,
                resource,
                room: _,
            } => monetize_cloud::cloud::Changed::Ensured(self.ensure(reference, resource)?),
            monetize_cloud::cloud::Change::Retire { reference, machine } => {
                monetize_cloud::cloud::Changed::Retired(monetize_cloud::cloud::retire_by_rows(
                    self, reference, machine,
                )?)
            }
            monetize_cloud::cloud::Change::LabelAdopted { .. } => {
                return Err(monetize_cloud::lay::unsupported_verb(
                    self.provider(),
                    "label_adopted",
                ))
            }
        })
    }
    fn boot(
        &self,
        boot: &monetize_cloud::cloud::Boot,
        _journal: &monetize_cloud::grow::Journal,
    ) -> Result<monetize_cloud::cloud::Booted, monetize_cloud::CloudError> {
        match boot {
            monetize_cloud::cloud::Boot::Keep(_k) => Err(monetize_cloud::lay::unsupported_verb(
                self.provider(),
                "keep_boot",
            )),
            monetize_cloud::cloud::Boot::Power { server: _, on: _ } => Err(
                monetize_cloud::lay::unsupported_verb(self.provider(), "set_power"),
            ),
            monetize_cloud::cloud::Boot::Restore(_r) => Err(monetize_cloud::lay::unsupported_verb(
                self.provider(),
                "restore_from_backup",
            )),
        }
    }
    fn firewall(
        &self,
        _laid: &monetize_cloud::lay::LaidEstate,
    ) -> Result<(), monetize_cloud::CloudError> {
        Err(monetize_cloud::lay::unsupported_verb(
            self.provider(),
            "set_estate_firewall",
        ))
    }
    fn destroy(
        &self,
        what: &monetize_cloud::cloud::Give<'_>,
    ) -> Result<monetize_cloud::Reclaimed, monetize_cloud::CloudError> {
        match what {
            monetize_cloud::cloud::Give::References(refs) => Ok(
                monetize_cloud::release_for_by_rows(self.list_owned()?, refs, |id| {
                    self.destroy_id(id)
                }),
            ),
            monetize_cloud::cloud::Give::Resource(id) => {
                self.destroy_id(id)?;
                Ok(monetize_cloud::Reclaimed {
                    destroyed: vec![(*id).clone()],
                    ..monetize_cloud::Reclaimed::default()
                })
            }
        }
    }
    fn inventory(
        &self,
        what: &monetize_cloud::cloud::Listing<'_>,
    ) -> Result<monetize_cloud::cloud::Listed, monetize_cloud::CloudError> {
        Ok(match what {
            monetize_cloud::cloud::Listing::Account => {
                monetize_cloud::cloud::Listed::Account(self.list_owned()?)
            }
            monetize_cloud::cloud::Listing::Set(_set) => {
                return Err(monetize_cloud::grow::unsupported(self.provider()))
            }
            monetize_cloud::cloud::Listing::Status(id) => {
                monetize_cloud::cloud::Listed::Status(self.status(id)?)
            }
            monetize_cloud::cloud::Listing::Under(refs) => monetize_cloud::cloud::Listed::Under(
                monetize_cloud::cloud::under(self.list_owned()?, refs),
            ),
            monetize_cloud::cloud::Listing::Room { delta: _, room: _ } => {
                monetize_cloud::cloud::Listed::Room(monetize_cloud::cloud::RoomLeft {
                    serve: Ok(()),
                    left_bytes: None,
                    ceilings: Default::default(),
                })
            }
            monetize_cloud::cloud::Listing::Admits(_plan) => {
                monetize_cloud::cloud::Listed::Admitted
            }
        })
    }
}

/// **A ledger holding one `Provisioned` row per `(provider, minor)`**, in EUR —
/// spend attributable to the provider that bought it, for the per-cloud tests.
pub fn provisioned_rows(ledger: &monetize::Ledger, rows: &[(&str, u64)]) {
    use monetize::ledger::Row;
    let rows: Vec<(String, Row)> = rows
        .iter()
        .enumerate()
        .map(|(i, (provider, minor))| {
            let reference = format!("gunnar/t{i}/2026-09-14+pack_bytes=1");
            let step = monetize::Step::Ensured {
                reference: reference.clone(),
                resource: monetize_cloud::Resource::Disk {
                    server: monetize_cloud::ServerId("gunnar-appliance".into()),
                    gib: 10,
                },
                ensured: monetize_cloud::Ensured::Provisioned {
                    id: monetize_cloud::ResourceId {
                        provider: (*provider).into(),
                        id: format!("disk-{i}"),
                    },
                    cost: monetize_cloud::Price {
                        minor_per_month: *minor,
                        currency: monetize_pay::Currency::Eur,
                    },
                },
            };
            (
                reference,
                Row {
                    seq: 1,
                    at_unix_ms: 1_788_436_800_000,
                    step,
                },
            )
        })
        .collect();
    ledger
        .restore(&rows, &[], &[])
        .expect("an empty ledger takes the rows");
}

/// **A product nobody can reach** (`(id, address)`) — every call answers `Unavailable`, the way a
/// real plugin does when its box is down. The double the poller tests use for
/// the "not connected" row, so this crate's tests drive no product plugin the
/// binary does not link (the open server links none).
/// **A product whose main is down**: `(id, address)`; its main has never reported, so every
/// read is unavailable and names the estate.
pub struct DownProduct(pub &'static str, pub &'static str);

impl crate::registry::IntoServed for DownProduct {
    fn into_served(self) -> std::sync::Arc<monetize_product::pulled::Pulled> {
        std::sync::Arc::new(
            monetize_product::pulled::HeldMain::new(self.1)
                .serve(monetize_product::ProductFacts::new(self.0, &[])),
        )
    }
}