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

nordisk/monetize

crates/monetize-site-buy/tests/one_flow_two_sites.rs

//! ★ **One buy flow, two sites** (owner 2026-10-23: "make integration tests for shared lib").
//!
//! The real relay (axum, [`monetize_site_buy::router`]) runs against the fake ONE box
//! ([`monetize_site_buy::testkit`]: Signup, EstateOrder, Purchase and Entitlement over
//! monetize-proto, with a fake Stripe), once with a Gunnar-shaped provider and once with a
//! Holger-shaped one. Every scenario runs for both (`<scenario>::gunnar`, `<scenario>::holger`):
//! one flow, only the provider differs. Each ends on the owner's buy rules
//! ([`sites::Kit::assert_buy_rules`]): Stripe once per purchase, the e-mail only at account
//! creation, every way back on the site.

mod sites;

use axum::http::StatusCode;
use monetize_site_buy::testkit::{
    self, What, BUNDLE, BUNDLE_BYTES, BUNDLE_WORDS, FREE_ROW, PAID_MINOR, PAID_ROW, PAID_WORDS,
};
use serde_json::json;
use sites::{Kit, Shape};

const NEW: &str = "newbie";
const NEW_MAIL: &str = "newbie@mock.invalid";

fn sizes(company: &str, main_plan: &str, gib: u64) -> serde_json::Value {
    json!({"company": company, "front_plan": "f-mini", "main_plan": main_plan, "main_data_gib": gib})
}

/// The session id of a vendor page (`…/c/pay/<id>`).
fn session_of(url: &str) -> String {
    url.rsplit('/').next().unwrap().to_owned()
}

for_both_sites!(
    a_visitor_makes_a_free_account,
    a_visitor_buys_a_paid_plan,
    a_visitor_buys_a_private_instance_and_is_welcomed,
    a_signed_in_buyer_buys_a_private_instance_without_an_e_mail,
    a_signed_in_buyer_grows_the_account,
    a_signed_in_buyer_grows_the_companys_instance,
    every_way_back_from_stripe_lands_on_the_sites_own_origin,
    a_member_trying_to_buy_is_refused_by_name,
    a_one_box_that_is_down_or_errs_is_answered_in_words,
    the_rate_limit_and_a_bad_name_stop_before_the_shop,
    a_buyer_cancels_an_unpaid_order,
);

/// 1. **A visitor, the free account**: the plans, the name check, the start, then Stripe's 0 kr
///    card check — only here — then the welcome page and its status.
async fn a_visitor_makes_a_free_account(shape: Shape) {
    let k = Kit::new(shape).await;
    let plans = k.ask("GET", "/auth/signup/ladder", None, None).await;
    assert_eq!(plans.status, StatusCode::OK, "{:?}", plans.body);
    assert_eq!(
        plans.body["plans"][0]["id"],
        format!("{}/{FREE_ROW}", shape.product())
    );
    assert_eq!(plans.body["plans"][0]["price_words"], "Free");
    let name = k
        .ask("GET", &format!("/auth/signup/name?u={NEW}"), None, None)
        .await;
    assert_eq!(name.body, json!({"free": true, "reason": ""}));

    let start = k.ask("POST", "/auth/signup/start", None, Some(json!({"username": NEW, "email": NEW_MAIL, "plan": format!("{}/{FREE_ROW}", shape.product()), "next": "/"}))).await;
    assert_eq!(start.status, StatusCode::OK, "{:?}", start.body);
    let url = start.body["url"].as_str().unwrap().to_owned();
    {
        let b = k.fake.book();
        assert_eq!(b.checkouts.len(), 1, "one Stripe page for one account");
        let c = &b.checkouts[0];
        assert!(
            c.setup && c.amount_minor == 0,
            "the free account is Stripe's 0 kr card check: {c:?}"
        );
        assert_eq!(c.url(), url);
        assert_eq!(
            c.success_url,
            format!("{}/welcome?session={{session}}&next=%2F", shape.origin())
        );
        assert_eq!(
            c.cancel_url,
            format!("{}{}", shape.origin(), shape.pages().account)
        );
    }
    let session = session_of(&url);
    let pending = k
        .ask(
            "GET",
            &format!("/auth/signup/status?session={session}"),
            None,
            None,
        )
        .await;
    assert_eq!(
        pending.body["state"], "pending",
        "nothing is made before the card check clears"
    );
    assert!(
        pending.body.get("reason").is_none(),
        "the operator's reason never reaches the page"
    );

    k.pay(&url);
    let welcome = k.fake.book().checkouts[0].return_url();
    assert!(
        welcome.starts_with(&format!("{}/welcome?session={session}", shape.origin())),
        "{welcome}"
    );
    let made = k
        .ask(
            "GET",
            &format!("/auth/signup/status?session={session}"),
            None,
            None,
        )
        .await;
    assert_eq!(made.body["state"], "created", "{:?}", made.body);
    assert_eq!(made.body["username"], NEW);
    assert_eq!(
        made.body["invite_url"],
        format!("/login?signup={session}&user={NEW}")
    );
    let b = k.fake.book();
    assert_eq!(b.accounts[NEW].caps["disk_bytes"], testkit::FREE_BYTES);
    assert_eq!(b.mails.len(), 1, "one sign-in mail, at account creation");
    drop(b);
    k.assert_buy_rules();
}

/// 2. **A visitor, a paid plan**: one Stripe page for the plan's amount (the "Pay <amount>"
///    button's words come with the plan), the account made only when paid, its sign-in mail, and
///    that mail asked for again from the welcome page.
async fn a_visitor_buys_a_paid_plan(shape: Shape) {
    let k = Kit::new(shape).await;
    let plans = k.ask("GET", "/auth/signup/ladder", None, None).await;
    let paid = &plans.body["plans"][1];
    let plan = format!("{}/{PAID_ROW}", shape.product());
    assert_eq!(
        (paid["id"].as_str(), paid["price_words"].as_str()),
        (Some(plan.as_str()), Some(PAID_WORDS)),
        "the plan row carries the amount its button pays"
    );
    assert_eq!(paid["period_days"], 30);

    let start = k.ask("POST", "/auth/signup/start", None, Some(json!({"username": NEW, "email": NEW_MAIL, "email_confirm": NEW_MAIL, "plan": plan, "next": "/"}))).await;
    assert_eq!(start.status, StatusCode::OK, "{:?}", start.body);
    let url = start.body["url"].as_str().unwrap().to_owned();
    {
        let b = k.fake.book();
        assert_eq!(b.checkouts.len(), 1, "Stripe once for the plan");
        assert!(
            !b.checkouts[0].setup && b.checkouts[0].amount_minor == PAID_MINOR,
            "a paid plan pays, no card check first: {:?}",
            b.checkouts[0]
        );
        assert!(
            !b.accounts.contains_key(NEW),
            "the account is made when paid, not before"
        );
        assert_eq!(
            b.emails
                .iter()
                .find(|(c, _)| c == "Signup.Start")
                .map(|(_, e)| e.as_str()),
            Some(NEW_MAIL)
        );
    }
    let session = session_of(&url);
    let early = k
        .ask(
            "POST",
            "/auth/signup/sign-in-again",
            None,
            Some(json!({"session": session, "next": "/"})),
        )
        .await;
    assert_eq!(
        early.status,
        StatusCode::CONFLICT,
        "no mail for an account not made yet"
    );

    k.pay(&url);
    assert!(
        k.fake.book().accounts.contains_key(NEW),
        "paid: the account exists"
    );
    let made = k
        .ask(
            "GET",
            &format!("/auth/signup/status?session={session}"),
            None,
            None,
        )
        .await;
    assert_eq!(
        (made.body["state"].as_str(), made.body["username"].as_str()),
        (Some("created"), Some(NEW))
    );
    let again = k
        .ask(
            "POST",
            "/auth/signup/sign-in-again",
            None,
            Some(json!({"session": session, "next": "/"})),
        )
        .await;
    assert_eq!(again.status, StatusCode::OK, "{:?}", again.body);
    {
        let b = k.fake.book();
        assert_eq!(
            b.mails
                .iter()
                .map(|m| (m.to.as_str(), m.kind.as_str()))
                .collect::<Vec<_>>(),
            [(NEW_MAIL, "sign-in"), (NEW_MAIL, "sign-in-again")]
        );
        assert_eq!(
            b.checkouts.len(),
            1,
            "asking for the mail again buys nothing"
        );
    }
    // Once the passkey is registered, the link is spent.
    k.site.passkeys.lock().unwrap().push(NEW.into());
    let spent = k
        .ask(
            "GET",
            &format!("/auth/signup/status?session={session}"),
            None,
            None,
        )
        .await;
    assert_eq!(spent.body["invite_url"], serde_json::Value::Null);
    k.assert_buy_rules();
}

/// 3. **A visitor, a private instance**: the ladder, one order that names the new account
///    (pay once), the welcome page for that account, then the order's watch until it is laid.
async fn a_visitor_buys_a_private_instance_and_is_welcomed(shape: Shape) {
    let k = Kit::new(shape).await;
    let ladder = k.ask("GET", "/auth/private/ladder", None, None).await;
    assert_eq!(ladder.status, StatusCode::OK, "{:?}", ladder.body);
    assert_eq!(ladder.body["steps"][0]["name"], "Mini");
    let pay = ladder.body["steps"][0]["pay_words"].as_str().unwrap();
    assert!(
        pay.ends_with(" EUR") && !pay.starts_with("Pay "),
        "the amount alone; the page says Pay <amount> once: {:?}",
        ladder.body
    );

    let mut body = sizes("acme", "m-mini", 40);
    body["account"] = json!(NEW);
    body["email"] = json!(NEW_MAIL);
    let start = k.ask("POST", "/auth/private/start", None, Some(body)).await;
    assert_eq!(start.status, StatusCode::OK, "{:?}", start.body);
    assert_eq!(start.body["price_words"], "384.80 EUR");
    let url = start.body["url"].as_str().unwrap().to_owned();
    {
        let b = k.fake.book();
        assert_eq!(
            b.checkouts.len(),
            1,
            "a visitor pays ONCE for the account and the instance"
        );
        let What::Estate {
            new_account,
            account,
            email,
            ..
        } = &b.checkouts[0].what
        else {
            panic!("an estate order")
        };
        assert!(*new_account && account == NEW && email == NEW_MAIL);
        assert_eq!(
            b.checkouts[0].success_url,
            format!(
                "{}/welcome?session={{session}}&next=%2Fsignup%3Fplan%3Dprivate%26company%3Dacme",
                shape.origin()
            )
        );
    }
    k.pay(&url);
    let session = session_of(&url);
    let welcome = k
        .ask(
            "GET",
            &format!("/auth/signup/status?session={session}"),
            None,
            None,
        )
        .await;
    assert_eq!(
        (
            welcome.body["state"].as_str(),
            welcome.body["username"].as_str()
        ),
        (Some("created"), Some(NEW)),
        "{:?}",
        welcome.body
    );
    assert_eq!(
        welcome.body["invite_url"],
        format!("/login?signup={session}&user={NEW}")
    );
    let back = k.ask("GET", "/paid/estate/acme", None, None).await;
    assert_eq!(
        (back.status, back.location.as_deref()),
        (
            StatusCode::SEE_OTHER,
            Some("/signup?plan=private&company=acme")
        )
    );
    let mut state = String::new();
    for _ in 0..5 {
        let s = k
            .ask("GET", "/auth/private/status?company=acme", None, None)
            .await;
        state = s.body["state"].as_str().unwrap().to_owned();
        assert_eq!(s.body["url"], "", "a visitor reads the state word only");
        if state == "ready" {
            break;
        }
    }
    assert_eq!(state, "ready", "followed until it is laid");
    assert_eq!(
        k.fake.book().mails.len(),
        1,
        "one sign-in mail, to the new account"
    );
    k.assert_buy_rules();
}

/// 4. **Signed in, a private instance**: the order is the account's, its `estate` ticket
///    attached, and no e-mail address is passed, whatever the page sent.
async fn a_signed_in_buyer_buys_a_private_instance_without_an_e_mail(shape: Shape) {
    let k = Kit::new(shape).await;
    k.fake.add_account("x14", "x14@mock.invalid", &[]);
    let mut body = sizes("acme", "m-mini", 40);
    body["account"] = json!("somebody-else");
    body["email"] = json!("other@mock.invalid");
    let start = k
        .ask("POST", "/auth/private/start", Some("x14"), Some(body))
        .await;
    assert_eq!(start.status, StatusCode::OK, "{:?}", start.body);
    let url = start.body["url"].as_str().unwrap().to_owned();
    {
        let b = k.fake.book();
        assert_eq!(b.checkouts.len(), 1);
        let What::Estate {
            new_account,
            account,
            email,
            ..
        } = &b.checkouts[0].what
        else {
            panic!("an estate order")
        };
        assert!(!new_account, "no new account for a signed-in buyer");
        assert_eq!(
            account, "x14",
            "the order is the session's account, whatever the form says"
        );
        assert!(
            email.is_empty(),
            "the e-mail is asked once, never in a buy: {email:?}"
        );
        assert!(
            b.emails.iter().all(|(_, e)| e.is_empty()),
            "no address was sent at all: {:?}",
            b.emails
        );
        let ticket = &b
            .tickets
            .iter()
            .find(|(c, _)| c == "EstateOrder.Start")
            .unwrap()
            .1;
        assert_eq!(
            ticket,
            format!("{}:estate:x14:{{}}", shape.product()).as_bytes(),
            "the product vouches with its own estate ticket"
        );
    }
    k.pay(&url);
    let mut last = serde_json::Value::Null;
    for _ in 0..5 {
        last = k
            .ask(
                "GET",
                "/auth/private/status?company=acme",
                Some("x14"),
                None,
            )
            .await
            .body;
        if last["state"] == "ready" {
            break;
        }
    }
    assert_eq!(last["state"], "ready", "{last}");
    assert_eq!(
        last["url"],
        format!("https://acme.{}", testkit::APEX),
        "its account reads the whole order"
    );
    assert!(
        k.fake.book().mails.is_empty(),
        "no mail: the account exists already"
    );
    k.assert_buy_rules();
}

/// 5. **Signed in, grow your account**: the bundles, the buy, its status before and after
///    the vendor's paid event.
async fn a_signed_in_buyer_grows_the_account(shape: Shape) {
    let k = Kit::new(shape).await;
    k.fake
        .add_account("x14", "x14@mock.invalid", &[("disk_bytes", 5 << 30)]);
    let window = k
        .ask(
            "GET",
            "/api/account/subscription/bundles",
            Some("x14"),
            None,
        )
        .await;
    assert_eq!(window.status, StatusCode::OK, "{:?}", window.body);
    assert_eq!(
        window.body["offers"].as_array().unwrap().len(),
        1,
        "a row the box cannot serve is withheld"
    );
    assert_eq!(window.body["offers"][0]["price_words"], BUNDLE_WORDS);
    let buy = k
        .ask(
            "POST",
            "/api/account/subscription/buy-bundle",
            Some("x14"),
            Some(json!({"bundle": BUNDLE})),
        )
        .await;
    assert_eq!(buy.status, StatusCode::OK, "{:?}", buy.body);
    assert_eq!(
        (
            buy.body["before"]["disk_bytes"].as_u64(),
            buy.body["after"]["disk_bytes"].as_u64()
        ),
        (Some(5 << 30), Some((5 << 30) + BUNDLE_BYTES))
    );
    let reference = buy.body["reference"].as_str().unwrap().to_owned();
    {
        let b = k.fake.book();
        assert_eq!(
            b.checkouts_of("bundle").len(),
            1,
            "Stripe once for the grow"
        );
        assert!(b.emails.is_empty(), "a grow names no address");
        let order = k
            .site
            .minted
            .lock()
            .unwrap()
            .iter()
            .find(|(p, _, _)| p == "order")
            .cloned()
            .unwrap();
        assert_eq!(
            order.2["disk_bytes"],
            (5 << 30) + BUNDLE_BYTES,
            "the order ticket covers the exact target"
        );
    }
    let waiting = k
        .ask(
            "GET",
            &format!("/api/account/subscription/status?reference={reference}"),
            Some("x14"),
            None,
        )
        .await;
    assert_eq!(
        (
            waiting.body["state"].as_str(),
            waiting.body["phase"].as_str()
        ),
        (Some("pending"), Some("waiting_for_payment")),
        "{:?}",
        waiting.body
    );
    k.pay(buy.body["url"].as_str().unwrap());
    let done = k
        .ask(
            "GET",
            &format!("/api/account/subscription/status?reference={reference}"),
            Some("x14"),
            None,
        )
        .await;
    assert_eq!(
        (done.body["state"].as_str(), done.body["phase"].as_str()),
        (Some("paid"), Some("done")),
        "{:?}",
        done.body
    );
    assert_eq!(
        done.body["after"]["disk_bytes"].as_u64(),
        Some((5 << 30) + BUNDLE_BYTES)
    );
    let theirs = k
        .ask(
            "GET",
            &format!("/api/account/subscription/status?reference={reference}"),
            Some("y15"),
            None,
        )
        .await;
    assert_eq!(
        theirs.status,
        StatusCode::NOT_FOUND,
        "another account's purchase is not read"
    );
    k.assert_buy_rules();
}

/// 6. **Signed in, grow <company>'s instance**: the ladder at its size, a change of plan, the
///    way back from Stripe, and "Your purchases" listing it at its new size.
async fn a_signed_in_buyer_grows_the_companys_instance(shape: Shape) {
    let k = Kit::new(shape).await;
    k.fake.add_account("x14", "x14@mock.invalid", &[]);
    k.fake.add_estate("acme", "x14", "Mini");
    let ladder = k
        .ask(
            "GET",
            "/auth/private/ladder?company=acme",
            Some("x14"),
            None,
        )
        .await;
    assert_eq!(ladder.status, StatusCode::OK, "{:?}", ladder.body);
    assert_eq!(
        (
            ladder.body["current"].as_u64(),
            ladder.body["laid"].as_bool()
        ),
        (Some(0), Some(true))
    );
    let change = k
        .ask(
            "POST",
            "/auth/private/change",
            Some("x14"),
            Some(sizes("acme", "m-small", 120)),
        )
        .await;
    assert_eq!(change.status, StatusCode::OK, "{:?}", change.body);
    assert_eq!(change.body["reference"], "estate/acme/change-1");
    assert_eq!(
        k.fake.book().checkouts.len(),
        1,
        "Stripe once for the change"
    );
    k.pay(change.body["url"].as_str().unwrap());
    let back = k.ask("GET", "/paid/estate/acme/change-1", None, None).await;
    assert_eq!(
        back.location.as_deref(),
        Some("/signup?plan=private&company=acme&change=1")
    );
    let status = k
        .ask(
            "GET",
            "/auth/private/status?company=acme",
            Some("x14"),
            None,
        )
        .await;
    assert_eq!(status.body["change_status"], "done", "{:?}", status.body);
    let mine = k
        .ask("GET", "/api/account/purchases", Some("x14"), None)
        .await;
    assert_eq!(mine.status, StatusCode::OK, "{:?}", mine.body);
    let acme = &mine.body["estates"][0];
    assert_eq!(
        (acme["company"].as_str(), acme["step"].as_str()),
        (Some("acme"), Some("Small")),
        "{:?}",
        mine.body
    );
    assert_eq!(acme["grow_url"], "/signup?plan=grow&instance=acme");
    assert_eq!(acme["renew_url"], format!("{}acme", shape.pages().manage));
    assert_eq!(
        mine.body["storage_lines"][0]["words"],
        format!("Your {} is acme.{}", shape.word(), testkit::APEX)
    );
    let same = k
        .ask(
            "POST",
            "/auth/private/change",
            Some("x14"),
            Some(sizes("acme", "m-small", 120)),
        )
        .await;
    assert_eq!(
        same.status,
        StatusCode::CONFLICT,
        "the size it has already is not sold again"
    );
    assert_eq!(k.fake.book().checkouts.len(), 1);
    k.assert_buy_rules();
}

/// 7. **Every way back from Stripe lands on the site's own origin**: all four buys in one
///    site, then every URL the relay emitted, every URL it gave the shop and every vendor return.
async fn every_way_back_from_stripe_lands_on_the_sites_own_origin(shape: Shape) {
    let k = Kit::new(shape).await;
    k.fake
        .add_account("x14", "x14@mock.invalid", &[("disk_bytes", 1)]);
    k.fake.add_estate("old", "x14", "Mini");
    let a = k.ask("POST", "/auth/signup/start", None, Some(json!({"username": NEW, "email": NEW_MAIL, "plan": format!("{}/{PAID_ROW}", shape.product()), "next": shape.pages().private}))).await;
    let mut visitor = sizes("acme", "m-mini", 40);
    visitor["account"] = json!("newbie2");
    visitor["email"] = json!("newbie2@mock.invalid");
    let b = k
        .ask("POST", "/auth/private/start", None, Some(visitor))
        .await;
    let c = k
        .ask(
            "POST",
            "/auth/private/start",
            Some("x14"),
            Some(sizes("beta", "m-mini", 40)),
        )
        .await;
    let d = k
        .ask(
            "POST",
            "/api/account/subscription/buy-bundle",
            Some("x14"),
            Some(json!({"bundle": BUNDLE})),
        )
        .await;
    let e = k
        .ask(
            "POST",
            "/auth/private/change",
            Some("x14"),
            Some(sizes("old", "m-small", 120)),
        )
        .await;
    for r in [&a, &b, &c, &d, &e] {
        assert_eq!(r.status, StatusCode::OK, "{:?}", r.body);
        k.pay(r.body["url"].as_str().unwrap());
    }
    assert_eq!(
        k.fake.book().checkouts.len(),
        5,
        "five buys, five Stripe pages"
    );
    // Cancel on a sign-up from the private choice returns to the private choice.
    assert_eq!(
        k.fake.book().checkouts[0].cancel_url,
        format!("{}{}", shape.origin(), shape.pages().private)
    );
    for path in [
        "/paid/estate/acme",
        "/paid/estate/old/change-1",
        "/paid/estate/..%2Fx",
        "/paid/estate/old/change-x",
    ] {
        let r = k.ask("GET", path, None, None).await;
        assert_eq!(r.status, StatusCode::SEE_OTHER, "{path}");
        assert!(
            r.location
                .as_deref()
                .unwrap()
                .starts_with(&shape.pages().private),
            "{path} → {:?}",
            r.location
        );
    }
    let offer = k.ask("GET", "/auth/buy/offer", None, None).await;
    assert_eq!(offer.body["account_url"], shape.pages().account);
    let _ = k
        .ask("GET", "/api/account/purchases", Some("x14"), None)
        .await;
    let sessions: Vec<String> = k
        .fake
        .book()
        .checkouts
        .iter()
        .filter(|c| !c.success_url.is_empty())
        .map(|c| c.id.clone())
        .collect();
    for session in sessions {
        let _ = k
            .ask(
                "GET",
                &format!("/auth/signup/status?session={session}"),
                None,
                None,
            )
            .await;
        let _ = k
            .ask(
                "POST",
                "/auth/signup/sign-in-again",
                None,
                Some(json!({"session": session, "next": "/"})),
            )
            .await;
    }
    assert!(!k.emitted.lock().unwrap().is_empty());
    k.assert_buy_rules();
}

/// 8. **A member trying to buy is refused, by name**, at every door that buys, and the shop
///    mints nothing for them.
async fn a_member_trying_to_buy_is_refused_by_name(shape: Shape) {
    let k = Kit::new(shape).await;
    k.fake.add_estate("acme", "boss", "Mini");
    let who = Some("member:acme");
    let signup = k
        .ask(
            "POST",
            "/auth/signup/start",
            who,
            Some(json!({"username": "ann2", "email": "ann@mock.invalid"})),
        )
        .await;
    assert_eq!(signup.status, StatusCode::FORBIDDEN);
    assert_eq!(
        signup.error(),
        "You are a member of acme. A member buys nothing: acme's room is its admin's to buy."
    );
    let private = k
        .ask(
            "POST",
            "/auth/private/start",
            who,
            Some(sizes("newco", "m-mini", 40)),
        )
        .await;
    assert_eq!(private.status, StatusCode::FORBIDDEN);
    assert_eq!(
        private.error(),
        "You are a member of acme. Members don’t buy: boss buys for acme."
    );
    let change = k
        .ask(
            "POST",
            "/auth/private/change",
            who,
            Some(sizes("acme", "m-small", 120)),
        )
        .await;
    assert_eq!(
        (change.status, change.error()),
        (StatusCode::FORBIDDEN, private.error())
    );
    for (method, path, body) in [
        ("GET", "/api/account/subscription/bundles", None),
        (
            "POST",
            "/api/account/subscription/buy-bundle",
            Some(json!({"bundle": BUNDLE})),
        ),
    ] {
        let r = k.ask(method, path, who, body).await;
        assert_eq!(r.status, StatusCode::FORBIDDEN, "{path}");
        assert_eq!(r.error(), signup.error(), "{path}");
    }
    let b = k.fake.book();
    assert!(b.checkouts.is_empty(), "a member is sent to no Stripe page");
    assert_eq!(
        b.count("Signup.Start")
            + b.count("EstateOrder.Start")
            + b.count("EstateOrder.ChangePlan")
            + b.count("Purchase.Start"),
        0,
        "the shop was not asked to sell"
    );
    assert!(
        k.site.minted.lock().unwrap().is_empty(),
        "no ticket is minted for a member"
    );
}

/// 9. **The ONE box is down, or answers an error**: every door answers its status and words,
///    never a panic, never an empty body.
async fn a_one_box_that_is_down_or_errs_is_answered_in_words(shape: Shape) {
    // Down: a link to a port nobody listens on.
    let gone = std::net::TcpListener::bind("127.0.0.1:0")
        .unwrap()
        .local_addr()
        .unwrap();
    let fake = testkit::FakeOneBox::spawn(shape.product(), sites::BEARER).await;
    let dead = monetize_site_buy::ShopLink::connect_lazy(
        &monetize_site_buy::Door {
            url: format!("http://{gone}"),
            ..Default::default()
        },
        sites::BEARER,
        shape.product(),
    )
    .unwrap();
    let down = Kit::over(shape, fake, dead, None);
    let doors: Vec<(&str, String, Option<&str>, Option<serde_json::Value>)> = vec![
        ("GET", "/auth/signup/ladder".into(), None, None),
        ("GET", format!("/auth/signup/name?u={NEW}"), None, None),
        (
            "POST",
            "/auth/signup/start".into(),
            None,
            Some(json!({"username": NEW, "email": NEW_MAIL})),
        ),
        (
            "GET",
            "/auth/signup/status?session=cs_test_abcdefghijkl".into(),
            None,
            None,
        ),
        ("GET", "/auth/private/ladder".into(), None, None),
        (
            "POST",
            "/auth/private/start".into(),
            Some("x14"),
            Some(sizes("acme", "m-mini", 40)),
        ),
        (
            "GET",
            "/auth/private/status?company=acme".into(),
            Some("x14"),
            None,
        ),
        (
            "GET",
            "/api/account/subscription/bundles".into(),
            Some("x14"),
            None,
        ),
        (
            "POST",
            "/api/account/subscription/buy-bundle".into(),
            Some("x14"),
            Some(json!({"bundle": BUNDLE})),
        ),
        (
            "GET",
            "/api/account/subscription/status?reference=p-1".into(),
            Some("x14"),
            None,
        ),
        ("GET", "/api/account/purchases".into(), Some("x14"), None),
    ];
    for (method, path, who, body) in &doors {
        let r = down.ask(method, path, *who, body.clone()).await;
        assert!(
            matches!(r.status.as_u16(), 404 | 502 | 503),
            "{path}: {} {:?}",
            r.status,
            r.body
        );
        assert!(
            !r.error().is_empty(),
            "{path}: the buyer is told in words: {:?}",
            r.body
        );
        assert!(
            !r.error().contains("transport")
                && !r.error().contains("tcp")
                && !r.error().contains("127.0.0.1"),
            "{path}: no internal word reaches the buyer: {}",
            r.error()
        );
    }
    let plans = down.ask("GET", "/auth/signup/ladder", None, None).await;
    assert_eq!(
        (plans.status, plans.error()),
        (
            StatusCode::SERVICE_UNAVAILABLE,
            monetize_site_buy::relay::SHOP_UNAVAILABLE
        )
    );
    let private = down.ask("GET", "/auth/private/ladder", None, None).await;
    assert_eq!(
        (private.status, private.error()),
        (
            StatusCode::NOT_FOUND,
            format!("A private {} is not sold here.", shape.word()).as_str()
        ),
        "a shop that never answered sells no instance"
    );
    let name = down
        .ask("GET", &format!("/auth/signup/name?u={NEW}"), None, None)
        .await;
    assert_eq!(
        (name.status, name.error()),
        (
            StatusCode::BAD_GATEWAY,
            monetize_site_buy::rules::SHOP_DID_NOT_ANSWER
        )
    );

    // Up, and answering errors: its verdict in its own words, an outage in the site's.
    let k = Kit::new(shape).await;
    k.fake.add_account("x14", "x14@mock.invalid", &[]);
    let ok = k.ask("GET", "/auth/private/ladder", None, None).await;
    assert_eq!(
        ok.status,
        StatusCode::OK,
        "the shop's word on selling instances is kept"
    );
    k.fake.fail(Some((
        tonic::Code::FailedPrecondition,
        "Someone is signing up with this account name right now.",
    )));
    let r = k
        .ask(
            "POST",
            "/auth/signup/start",
            None,
            Some(json!({"username": NEW, "email": NEW_MAIL})),
        )
        .await;
    assert_eq!(
        (r.status, r.error()),
        (
            StatusCode::CONFLICT,
            "Someone is signing up with this account name right now."
        )
    );
    let r = k
        .ask(
            "POST",
            "/auth/private/start",
            Some("x14"),
            Some(sizes("acme", "m-mini", 40)),
        )
        .await;
    assert_eq!(
        (r.status, r.error()),
        (
            StatusCode::CONFLICT,
            "Someone is signing up with this account name right now."
        )
    );
    k.fake.fail(Some((
        tonic::Code::Internal,
        "redb: table locked at /var/lib/monetize",
    )));
    // The two ladders are the site's, kept (LADDER-2): a shop that errs now does not take back
    // what they were last read as.
    for (method, path, who, body) in doors.iter().filter(|d| !d.1.ends_with("/ladder")) {
        let r = k.ask(method, path, *who, body.clone()).await;
        assert!(
            r.status.is_client_error() || r.status.is_server_error(),
            "{path}: {}",
            r.status
        );
        assert!(!r.error().is_empty(), "{path}: {:?}", r.body);
        assert!(
            !r.error().contains("redb") && !r.error().contains("/var/lib"),
            "{path}: the shop's internals never reach the buyer: {}",
            r.error()
        );
    }
    k.fake.fail(Some((tonic::Code::Unavailable, "")));
    let r = k
        .ask(
            "GET",
            "/api/account/subscription/bundles",
            Some("x14"),
            None,
        )
        .await;
    assert_eq!(
        (r.status, r.error()),
        (
            StatusCode::BAD_GATEWAY,
            monetize_site_buy::rules::SHOP_DID_NOT_ANSWER
        )
    );
    // The grow asks the shop first whether it checks tickets (A32-9): a shop that is down is said so.
    let r = k
        .ask(
            "POST",
            "/api/account/subscription/buy-bundle",
            Some("x14"),
            Some(json!({"bundle": BUNDLE})),
        )
        .await;
    assert_eq!(
        (r.status, r.error()),
        (
            StatusCode::BAD_GATEWAY,
            monetize_site_buy::rules::SHOP_DID_NOT_ANSWER
        )
    );
    assert!(
        k.fake.book().checkouts.is_empty(),
        "nothing was sold while the box erred"
    );
    // The site itself down: its own words, the shop not asked.
    let before = k.fake.book().count("EstateOrder.Start");
    k.fake.fail(None);
    let r = k
        .ask(
            "POST",
            "/auth/private/start",
            Some("down"),
            Some(sizes("acme", "m-mini", 40)),
        )
        .await;
    assert_eq!(r.status, StatusCode::SERVICE_UNAVAILABLE, "{:?}", r.body);
    assert_eq!(
        r.error(),
        format!(
            "{} cannot be reached right now: try again in a minute.",
            shape.word()
        )
    );
    assert_eq!(
        k.fake.book().count("EstateOrder.Start"),
        before,
        "the shop is not asked for a site that cannot say who asks"
    );
}

/// 10. **The rate limit and a bad name**: refused before the shop is asked.
async fn the_rate_limit_and_a_bad_name_stop_before_the_shop(shape: Shape) {
    let k = Kit::with_budget(shape, Some(2)).await;
    for _ in 0..2 {
        assert_eq!(
            k.ask("GET", &format!("/auth/signup/name?u={NEW}"), None, None)
                .await
                .status,
            StatusCode::OK
        );
    }
    let third = k
        .ask("GET", &format!("/auth/signup/name?u={NEW}"), None, None)
        .await;
    assert_eq!(
        (third.status, third.error()),
        (
            StatusCode::TOO_MANY_REQUESTS,
            "too many names checked from here; wait a moment"
        )
    );
    assert_eq!(
        k.fake.book().count("Signup.CheckName"),
        2,
        "the third check never reached the shop"
    );
    for _ in 0..2 {
        let _ = k
            .ask("GET", "/auth/private/status?company=acme", None, None)
            .await;
    }
    let busy = k
        .ask("GET", "/auth/private/status?company=acme", None, None)
        .await;
    assert_eq!(
        (busy.status, busy.error()),
        (
            StatusCode::TOO_MANY_REQUESTS,
            "too many requests from here; wait a moment"
        )
    );

    let k = Kit::new(shape).await;
    let short = k.ask("GET", "/auth/signup/name?u=X", None, None).await;
    assert_eq!(short.body["free"], false);
    assert!(
        short.body["reason"]
            .as_str()
            .unwrap()
            .starts_with("a username is "),
        "{:?}",
        short.body
    );
    let taken = k.ask("GET", "/auth/signup/name?u=taken", None, None).await;
    assert_eq!(
        taken.body,
        json!({"free": false, "reason": "username \"taken\" is taken; choose another"})
    );
    for name in ["Bad Name!", "../etc", "", "x"] {
        let r = k
            .ask(
                "POST",
                "/auth/signup/start",
                None,
                Some(json!({"username": name, "email": NEW_MAIL})),
            )
            .await;
        assert_eq!(r.status, StatusCode::BAD_REQUEST, "{name:?}: {:?}", r.body);
        assert!(
            r.error().starts_with("a username is "),
            "{name:?}: {}",
            r.error()
        );
    }
    let mut bad_co = sizes("../x", "m-mini", 40);
    bad_co["account"] = json!(NEW);
    bad_co["email"] = json!(NEW_MAIL);
    let r = k
        .ask("POST", "/auth/private/start", None, Some(bad_co))
        .await;
    assert_eq!(r.status, StatusCode::BAD_REQUEST);
    let r = k
        .ask(
            "POST",
            "/auth/signup/start",
            None,
            Some(json!({"username": NEW, "email": "not-an-address"})),
        )
        .await;
    assert_eq!(r.status, StatusCode::BAD_REQUEST);
    let r = k
        .ask(
            "POST",
            "/auth/signup/start",
            None,
            Some(json!({"username": NEW, "email": NEW_MAIL, "plan": "../../x"})),
        )
        .await;
    assert_eq!(r.status, StatusCode::BAD_REQUEST);
    let b = k.fake.book();
    assert_eq!(
        b.count("Signup.Start") + b.count("EstateOrder.Start"),
        0,
        "a bad name never reaches the shop"
    );
    assert!(b.checkouts.is_empty());
}

/// 11. **"Cancel order"** (holger-ui's `/cancel/<reference>`, Gunnar's FollowOrder `cancel`): an
///     unpaid grow is cancellable; the buyer cancels it and the ONE box closes it (released,
///     cancelled); asked again it is already closed; the vendor's late paid event changes nothing;
///     another account cancels nothing of it.
async fn a_buyer_cancels_an_unpaid_order(shape: Shape) {
    let k = Kit::new(shape).await;
    k.fake
        .add_account("x14", "x14@mock.invalid", &[("disk_bytes", 5 << 30)]);
    let buy = k
        .ask(
            "POST",
            "/api/account/subscription/buy-bundle",
            Some("x14"),
            Some(json!({"bundle": BUNDLE})),
        )
        .await;
    assert_eq!(buy.status, StatusCode::OK, "{:?}", buy.body);
    let reference = buy.body["reference"].as_str().unwrap().to_owned();
    let status = |who: &'static str| {
        let (k, reference) = (&k, reference.clone());
        async move {
            k.ask(
                "GET",
                &format!("/api/account/subscription/status?reference={reference}"),
                Some(who),
                None,
            )
            .await
        }
    };
    let waiting = status("x14").await;
    assert_eq!(
        (
            waiting.body["phase"].as_str(),
            waiting.body["cancellable"].as_bool()
        ),
        (Some("waiting_for_payment"), Some(true)),
        "an unpaid order is cancellable: {:?}",
        waiting.body
    );
    let theirs = k
        .ask(
            "POST",
            "/api/account/subscription/cancel",
            Some("y15"),
            Some(json!({"reference": reference})),
        )
        .await;
    assert_eq!(
        theirs.status,
        StatusCode::NOT_FOUND,
        "another account cancels nothing: {:?}",
        theirs.body
    );
    let closed = k
        .ask(
            "POST",
            "/api/account/subscription/cancel",
            Some("x14"),
            Some(json!({"reference": reference})),
        )
        .await;
    assert_eq!(closed.status, StatusCode::OK, "{:?}", closed.body);
    assert_eq!(
        (closed.body["state"].as_str(), closed.body["phase"].as_str()),
        (Some("released"), Some("cancelled")),
        "{:?}",
        closed.body
    );
    let after = status("x14").await;
    assert_eq!(
        (
            after.body["state"].as_str(),
            after.body["phase"].as_str(),
            after.body["cancellable"].as_bool()
        ),
        (Some("released"), Some("cancelled"), Some(false)),
        "{:?}",
        after.body
    );
    let again = k
        .ask(
            "POST",
            "/api/account/subscription/cancel",
            Some("x14"),
            Some(json!({"reference": reference})),
        )
        .await;
    assert_eq!(
        (again.status, again.body["state"].as_str()),
        (StatusCode::OK, Some("released")),
        "asked again, already closed: {:?}",
        again.body
    );
    assert!(
        !k.fake.pay(buy.body["url"].as_str().unwrap()),
        "a cancelled order is never paid"
    );
    assert_eq!(
        k.fake.book().accounts["x14"].caps["disk_bytes"],
        5 << 30,
        "nothing was added"
    );
    assert_eq!(k.fake.book().count("Purchase.Cancel"), 3);
    k.assert_buy_rules();
}