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();
}