crates/gunnar-ui-server/src/anonymous/paid_signup.rs
//! **Paid signup, driven the way a browser with no session drives it.**
//!
//! A child of [`super`] rather than a file of its own, because it needs that
//! module's console harness and a second copy would be a second thing to keep
//! true. What is asserted here is not the parent's question (*which key did the
//! console use*) but the one this lane adds:
//!
//! ```text
//! no session ─▶ /signup ──▶ the page, one field ─▶ 200
//! ├▶ /auth/signup/name?u=… ──▶ free / taken, and nothing else
//! ├▶ /auth/signup/start ──▶ the vendor's URL, or the old buy link
//! └▶ /auth/signup/rename ──▶ the second name, after the money moved
//! ```
//!
//! # What it is here to catch
//!
//! The username was typed on the VENDOR's hosted page, where nothing can
//! validate it, so the first time this deployment saw it was in the webhook —
//! **after the card was charged**. A name already taken ended as a refusal, a
//! customer with no account and a refund by hand. Every test below is about one
//! of the two halves of the fix: settling the name before a card form exists,
//! and settling it again afterwards when somebody else took it in between.
//!
//! The fake monetize is `auth::signup::tests`'s, reused rather than re-written —
//! the same wire, asked a different question.
use std::sync::{Arc, Mutex};
use crate::auth::signup::tests::{FakeMonetize, SESSION};
use crate::mpb::signup_server::SignupServer;
use super::{send, FakeGunnar};
/// A console that CAN sell an account: a passkey door, and a monetize on
/// loopback answering as `fake` says.
///
/// The gunnar behind it is the parent's, untouched — nothing in this lane asks
/// gunnar anything, and that is itself the point of one test below.
async fn selling(
fake: FakeMonetize,
) -> (
axum::Router,
Arc<crate::auth::Webauthn>,
Arc<Mutex<Vec<String>>>,
) {
selling_estates(fake, true).await
}
/// [`selling`], its monetize selling private estates or not (`[estate_orders]`
/// set or absent; `EstateOrder.Sells`).
async fn selling_estates(
fake: FakeMonetize,
estates: bool,
) -> (
axum::Router,
Arc<crate::auth::Webauthn>,
Arc<Mutex<Vec<String>>>,
) {
selling_within(fake, estates, None).await
}
/// [`selling_estates`], its monetize waited on for `deadlines` (`None`: the
/// handle's default).
async fn selling_within(
fake: FakeMonetize,
estates: bool,
deadlines: Option<crate::monetize::Deadlines>,
) -> (
axum::Router,
Arc<crate::auth::Webauthn>,
Arc<Mutex<Vec<String>>>,
) {
let listener = tokio::net::TcpListener::bind("127.0.0.1:0")
.await
.expect("a loopback port");
let addr = listener.local_addr().expect("its address");
let calls = fake.calls.clone();
let orders = super::private_order::FakeEstateOrder { unsold: !estates, ..Default::default() };
tokio::spawn(async move {
tonic::transport::Server::builder()
.add_service(SignupServer::new(fake))
.add_service(crate::mpb::estate_order_server::EstateOrderServer::new(orders))
.serve_with_incoming(tokio_stream::wrappers::TcpListenerStream::new(listener))
.await
});
// The accounts monetize makes for these sessions, as gunnar holds them
// (thin layer L: the registration link is answered for a person at gunnar).
let gunnar = FakeGunnar::new();
for name in ["bob", "bob2"] {
gunnar.principals.lock().expect("the identity table").insert(
name.to_owned(),
crate::pb::Principal {
id: name.to_owned(),
name: name.to_owned(),
kind: crate::pb::PrincipalKind::User as i32,
created_at_unix_ms: 1,
},
);
}
let (router, webauthn, _) =
super::console_waiting(gunnar, None, Some(format!("http://{addr}")), None, None, deadlines, None).await;
(router, webauthn, calls)
}
/// [`selling`]'s console, but SOLD (provided) by `https://gunnar.rs` (one
/// shop, owner 2026-10-07): the same monetize behind it, the same passkey
/// door — and nothing is bought here.
async fn provided(fake: FakeMonetize) -> axum::Router {
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.expect("a loopback port");
let addr = listener.local_addr().expect("its address");
tokio::spawn(async move {
tonic::transport::Server::builder()
.add_service(SignupServer::new(fake))
.serve_with_incoming(tokio_stream::wrappers::TcpListenerStream::new(listener))
.await
});
let (router, _, _) = super::console_provided_by(FakeGunnar::new(), None, Some(format!("http://{addr}")), Some("https://gunnar.rs"), None).await;
router
}
/// [`selling`]'s console, configured as `product` (G15: test2.gunnar.rs is
/// `gunnar-test2` in the monetize that also serves gunnar.rs).
async fn selling_as(fake: FakeMonetize, product: &str) -> (axum::Router, Arc<Mutex<Vec<String>>>) {
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.expect("a loopback port");
let addr = listener.local_addr().expect("its address");
let calls = fake.calls.clone();
tokio::spawn(async move {
tonic::transport::Server::builder()
.add_service(SignupServer::new(fake))
.add_service(crate::mpb::estate_order_server::EstateOrderServer::new(super::private_order::FakeEstateOrder::default()))
.serve_with_incoming(tokio_stream::wrappers::TcpListenerStream::new(listener))
.await
});
let (router, _, _) = super::console_provided_by(FakeGunnar::new(), None, Some(format!("http://{addr}")), None, Some(product)).await;
(router, calls)
}
/// ★ **A console configured as another product sells THAT product** (G15,
/// round 5.4: gunnar.rs and test2.gunnar.rs both said `gunnar` to monetize,
/// a constant in this console). The id its config names is what `Signup.Plans`
/// and `Signup.Start` carry.
#[tokio::test]
async fn a_console_configured_as_another_product_sells_that_product() {
let (router, calls) = selling_as(FakeMonetize::default(), "gunnar-test2").await;
let (status, body) = get(&router, "/auth/signup/ladder?market=se").await;
assert_eq!(status, http::StatusCode::OK, "{body}");
assert!(crate::auth::signup::tests::read_plans(&calls, "gunnar-test2", crate::mpb::Market::Se as i32), "{:?}", calls.lock().unwrap());
assert!(calls.lock().unwrap().iter().all(|c| c.starts_with("Signup.Plans gunnar-test2 ")), "only this product's plans: {:?}", calls.lock().unwrap());
let (status, body) = post_json(&router, "/auth/signup/start", r#"{"username":"olle","email":"olle@example.com","plan":"gunnar-test2/10gb-1m"}"#).await;
assert_eq!(status, http::StatusCode::OK, "{body}");
let line = calls.lock().unwrap().last().cloned().unwrap();
assert!(line.starts_with("Signup.Start olle gunnar-test2 gunnar-test2/10gb-1m "), "{line}");
}
async fn get(app: &axum::Router, uri: &str) -> (http::StatusCode, String) {
send(app, http::Method::GET, uri, None).await
}
/// A POST with no credential of any kind — which is every caller this lane has.
async fn post_json(app: &axum::Router, uri: &str, body: &str) -> (http::StatusCode, String) {
use tower::ServiceExt;
let request = http::Request::builder()
.method(http::Method::POST)
.uri(uri)
.header(http::header::CONTENT_TYPE, "application/json")
.body(axum::body::Body::from(body.to_owned()))
.expect("a request");
let response = app
.clone()
.oneshot(request)
.await
.expect("the console answers");
let status = response.status();
let body = axum::body::to_bytes(response.into_body(), 1 << 20)
.await
.expect("a readable body");
(status, String::from_utf8_lossy(&body).into_owned())
}
// ---------------------------------------------------------------------------
// Half one: the name is settled before any money moves.
// ---------------------------------------------------------------------------
/// ★ **"Choose another username", and no card was charged to find out.**
///
/// The whole feature, from the outside: a visitor with no session asks about a
/// name and is told, before anything has been paid for.
#[tokio::test]
async fn a_taken_name_is_refused_to_a_visitor_with_no_session_and_nothing_is_charged() {
let (router, _, calls) = selling(FakeMonetize::holding(&["bob"])).await;
let (free_status, free) = get(&router, "/auth/signup/name?u=olle").await;
assert_eq!(free_status, http::StatusCode::OK, "{free}");
assert!(free.contains("\"free\":true"), "{free}");
let (taken_status, taken) = get(&router, "/auth/signup/name?u=bob").await;
assert_eq!(taken_status, http::StatusCode::OK, "{taken}");
assert!(taken.contains("\"free\":false"), "{taken}");
assert!(taken.contains("choose another"), "{taken}");
// No checkout was minted for either. Asking costs nothing and commits to
// nothing — a check that reserved the name would let anybody hold every
// good name on the appliance for free.
let log = crate::auth::signup::tests::asks(&calls);
assert_eq!(
log,
vec![
"Signup.CheckName olle gunnar".to_owned(),
"Signup.CheckName bob gunnar".to_owned()
],
);
}
/// **A name that is not a name never leaves the console.**
///
/// The rule is mirrored here so a typo costs a keystroke instead of a round
/// trip — and so `/auth/signup/name` cannot be used to push arbitrary bytes at
/// monetize over this console's credential.
#[tokio::test]
async fn a_malformed_name_gets_the_rule_and_monetize_is_never_dialled() {
let (router, _, calls) = selling(FakeMonetize::default()).await;
for bad in [
"", "a", "Bob", ".hidden", "nordkalott%21", "bob%2Foverthere",
&"x".repeat(33),
] {
let (status, body) = get(&router, &format!("/auth/signup/name?u={bad}")).await;
assert_eq!(status, http::StatusCode::OK, "{bad:?}: {body}");
assert!(body.contains("\"free\":false"), "{bad:?} was accepted: {body}");
// The RULE, and nothing about whether any such name exists: a malformed
// probe learns exactly the same thing whatever it asks about.
assert!(body.contains("2 to 32 characters"), "{bad:?}: {body}");
}
assert!(
calls.lock().expect("the call log").is_empty(),
"a malformed name reached monetize: {:?}",
calls.lock().expect("the call log")
);
}
/// ★ **The checkout carries the name, and the console's own origin comes back
/// to the welcome page.**
///
/// The success URL is the half that used to be a fixed Payment Link setting. It
/// carries `{session}`, which monetize's vendor rewrites into its own spelling
/// — so this console never has to know who is taking the money.
#[tokio::test]
async fn starting_a_signup_hands_monetize_the_name_and_this_consoles_own_return_url() {
let (router, _, calls) = selling(FakeMonetize::default()).await;
let (status, body) = post_json(
&router,
"/auth/signup/start",
r#"{"username":"olle","email":" olle@example.com ","email_confirm":"OLLE@example.com"}"#,
)
.await;
assert_eq!(status, http::StatusCode::OK, "{body}");
assert!(
body.contains("https://checkout.example/pay/"),
"the page has nowhere to send the browser: {body}"
);
let log = calls.lock().expect("the call log").clone();
assert_eq!(
log,
vec![
"Signup.Start olle gunnar https://console.example/welcome?session={session} olle@example.com"
.to_owned()
],
"the name, the product, no plan (the deployment's price list decides), no vendor (A32-15: the shop routes it), \
this console's own return URL, and the address the buyer typed (trimmed)"
);
}
/// ★ **A monetize that does not answer is a sentence within a bound, never a
/// page that says "Taking you to pay …" for ever.**
///
/// test2 2026-09-30 22:23:17, the owner's second live signup: the capacity
/// line, and then nothing — no minted checkout, no access line (it is written
/// when the request completes), nothing at WARN, still at 22:25. The console
/// waited on `Signup.Start` with no deadline of its own. Now it waits at most
/// its deadline, answers 504 with words the buyer can act on (nothing was
/// charged; try again), and says so at WARN with how long it waited.
#[tokio::test]
async fn a_monetize_that_does_not_answer_is_a_clear_refusal_within_the_deadline() {
let short = crate::monetize::Deadlines { mint: std::time::Duration::from_millis(1500), ..crate::monetize::Deadlines::default() };
let (router, _, _) = selling_within(
FakeMonetize { start_delay: Some(std::time::Duration::from_secs(30)), ..FakeMonetize::default() },
true,
Some(short),
)
.await;
let began = std::time::Instant::now();
let (status, body) = post_json(
&router,
"/auth/signup/start",
r#"{"username":"olle","email":"olle@example.com","email_confirm":"olle@example.com"}"#,
)
.await;
let took = began.elapsed();
assert!(took < std::time::Duration::from_secs(5), "the console waited {took:?}");
assert_eq!(status, http::StatusCode::GATEWAY_TIMEOUT, "{body}");
assert!(body.contains("Nothing has been charged"), "{body}");
assert!(body.contains("try again"), "{body}");
}
/// ★ **One console's deadline is that console's** (stinky #21): the wait on
/// a checkout's mint was a process-wide static that a test lowered for
/// itself, so every console in the process — every other test running beside
/// it — waited that long too. It is the monetize handle's own
/// [`crate::monetize::Deadlines::mint`] now: a hurried console answers in
/// words at its deadline while a patient one, at the same moment, waits out
/// a slow but healthy mint.
#[tokio::test]
async fn one_consoles_deadline_is_not_anothers() {
let short = crate::monetize::Deadlines { mint: std::time::Duration::from_millis(1500), ..crate::monetize::Deadlines::default() };
let (hurried, _, _) = selling_within(FakeMonetize { start_delay: Some(std::time::Duration::from_secs(30)), ..FakeMonetize::default() }, true, Some(short)).await;
let (patient, _, _) = selling(FakeMonetize { start_delay: Some(std::time::Duration::from_millis(2500)), ..FakeMonetize::default() }).await;
let body = r#"{"username":"olle","email":"olle@example.com","email_confirm":"olle@example.com"}"#;
let began = std::time::Instant::now();
let ((hurried_status, hurried_body), (patient_status, patient_body)) =
tokio::join!(post_json(&hurried, "/auth/signup/start", body), post_json(&patient, "/auth/signup/start", body));
assert!(began.elapsed() < std::time::Duration::from_secs(10), "waited {:?}", began.elapsed());
assert_eq!(hurried_status, http::StatusCode::GATEWAY_TIMEOUT, "the hurried console: {hurried_body}");
assert!(hurried_body.contains("Nothing has been charged"), "{hurried_body}");
assert_eq!(patient_status, http::StatusCode::OK, "the patient console was cut by the other's deadline: {patient_body}");
}
/// ★ g-currencies (money 2026-09-28): a signup names the customer's market,
/// so monetize sells the plan priced in its currency (DKK for a Dane); a
/// market this estate does not sell in is refused before monetize is asked.
#[tokio::test]
async fn a_signup_names_the_customers_market_and_an_unknown_one_is_refused() {
let (router, _, calls) = selling(FakeMonetize::default()).await;
let (status, body) = post_json(&router, "/auth/signup/start", r#"{"username":"olle","email":"olle@example.com","email_confirm":"olle@example.com","market":"DK"}"#).await;
assert_eq!(status, http::StatusCode::OK, "{body}");
let log = calls.lock().expect("the call log").clone();
assert!(log[0].ends_with(" market=3"), "MARKET_DK reached monetize: {log:?}");
let (status, body) = post_json(&router, "/auth/signup/start", r#"{"username":"olle","email":"olle@example.com","email_confirm":"olle@example.com","market":"US"}"#).await;
assert_eq!(status, http::StatusCode::BAD_REQUEST, "{body}");
assert_eq!(calls.lock().unwrap().len(), 1, "monetize was not asked");
}
/// ★ **A sign-up refusal is the one shape the page reads** (stinky #27):
/// `{"error": "<sentence>"}`, as `/auth/private/*` answers. The page's
/// `getJson` reads `.error`, so a plain-text refusal from `/auth/signup/ladder`
/// reached the visitor as "the server answered 400"; and a market word means
/// the same on the sign-up routes as on every other window (`market_of`).
#[tokio::test]
async fn a_sign_up_refusal_is_the_json_the_page_reads() {
let (router, _, calls) = selling(FakeMonetize::default()).await;
let start = |market: &str| format!(r#"{{"username":"olle","email":"olle@example.com","email_confirm":"olle@example.com","market":"{market}"}}"#);
for (status, body) in [
get(&router, "/auth/signup/ladder?market=xx").await,
post_json(&router, "/auth/signup/start", &start("US")).await,
post_json(&router, "/auth/signup/start", r#"{"username":"olle","email":"olle.example.com","email_confirm":"olle.example.com"}"#).await,
post_json(&router, "/auth/signup/start", r#"{"username":"-olle","email":"olle@example.com","email_confirm":"olle@example.com"}"#).await,
] {
assert_eq!(status, http::StatusCode::BAD_REQUEST, "{body}");
let j: serde_json::Value = serde_json::from_str(&body).unwrap_or_else(|_| panic!("a refusal the page cannot read: {body}"));
assert!(j["error"].as_str().is_some_and(|e| !e.is_empty()), "{body}");
}
let (_, ladder) = get(&router, "/auth/signup/ladder?market=xx").await;
let (_, order) = post_json(&router, "/auth/signup/start", &start("xx")).await;
let increase = crate::api::increase::market_of(Some("xx")).expect_err("xx is no market");
let error = |body: &str| serde_json::from_str::<serde_json::Value>(body).ok().and_then(|j| j["error"].as_str().map(str::to_owned));
assert_eq!(error(&ladder).as_deref(), Some(increase.0.message.as_str()), "one market rule, one sentence");
assert_eq!(error(&order).as_deref(), Some(increase.0.message.as_str()), "one market rule, one sentence");
assert!(calls.lock().unwrap().is_empty(), "a refusal dials nobody");
}
/// ★ **No address, a mistyped one, or two that differ: refused here, before
/// monetize is asked and before any card form** (2026-09-15: the first paid
/// signup was never asked for one).
#[tokio::test]
async fn a_signup_without_a_confirmed_email_is_refused_before_monetize_is_asked() {
let (router, _, calls) = selling(FakeMonetize::default()).await;
for (body, words) in [
(r#"{"username":"olle"}"#, "required"),
(r#"{"username":"olle","email":"olle.example.com","email_confirm":"olle.example.com"}"#, "does not look like"),
(r#"{"username":"olle","email":"olle@example.com","email_confirm":"olle@example.se"}"#, "do not match"),
] {
let (status, answer) = post_json(&router, "/auth/signup/start", body).await;
assert_eq!(status, http::StatusCode::BAD_REQUEST, "{body}: {answer}");
assert!(answer.contains(words), "{body}: {answer}");
}
assert!(calls.lock().expect("the call log").is_empty(), "an unconfirmed address reached monetize");
}
/// The fixed buy link gets the typed address as Stripe's `prefilled_email`.
#[test]
fn the_fallback_buy_link_carries_the_typed_address_encoded() {
assert_eq!(
crate::auth::signup::buy_url_for_test("https://buy.stripe.com/abc", "a.b+c@example.com"),
"https://buy.stripe.com/abc?prefilled_email=a.b%2Bc%40example.com"
);
assert_eq!(
crate::auth::signup::buy_url_for_test("https://buy.stripe.com/abc?x=1", "o@e.se"),
"https://buy.stripe.com/abc?x=1&prefilled_email=o%40e.se"
);
}
/// **A name taken between the check and the checkout is refused HERE**, which is
/// the last moment it can be refused for free. The page shows monetize's own
/// sentence.
#[tokio::test]
async fn a_name_taken_between_the_check_and_the_checkout_is_refused_before_the_card_form() {
let fake = FakeMonetize::default();
let taken = fake.taken.clone();
let (router, _, _) = selling(fake).await;
let (_, free) = get(&router, "/auth/signup/name?u=bob").await;
assert!(free.contains("\"free\":true"), "{free}");
// Somebody else buys it in the seconds that follow.
taken.lock().expect("the name list").push("bob".to_owned());
let (status, body) = post_json(
&router,
"/auth/signup/start",
r#"{"username":"bob","email":"h@example.com","email_confirm":"h@example.com"}"#,
)
.await;
assert_eq!(status, http::StatusCode::CONFLICT, "{body}");
assert!(body.contains("choose another"), "{body}");
}
/// ★ **A monetize that mints no checkout falls back to the operator's fixed buy
/// link**, so a deployment selling through a Payment Link keeps its door.
///
/// This is why `UNIMPLEMENTED` has to be its own gRPC code rather than another
/// refusal: the console turns one into a link and the other into a sentence.
#[tokio::test]
async fn a_monetize_that_mints_no_checkout_falls_back_to_the_configured_buy_link() {
// `GUNNAR_UI_BUY_URL` is read once into a `OnceLock`, so a test that set it
// would decide the value for every other test in this binary. What is
// asserted instead is the shape of the fallback: with no link configured,
// the console says it cannot sell rather than inventing somewhere to send a
// browser — which is the half that must never regress. In the visitor's
// words (round 5.3.1): the setting's name is the operator's, in the journal.
let (router, _, _) = selling(FakeMonetize {
checkout: None,
..FakeMonetize::default()
})
.await;
let (status, body) = post_json(
&router,
"/auth/signup/start",
r#"{"username":"olle","email":"olle@example.com","email_confirm":"olle@example.com"}"#,
)
.await;
assert_eq!(status, http::StatusCode::NOT_IMPLEMENTED, "{body}");
assert!(body.contains(crate::auth::signup::ACCOUNT_NOT_SOLD_HERE), "{body}");
assert!(!body.contains("GUNNAR_UI_BUY_URL") && !body.contains("monetize"), "configuration words reached the visitor: {body}");
}
/// **The page is served to a stranger, and it is the page and not the console
/// shell.** One field, and the sentence it says when a name is taken.
#[tokio::test]
async fn the_signup_page_is_served_without_a_session_and_asks_for_one_thing() {
let (router, _, calls) = selling(FakeMonetize::default()).await;
let (status, body) = get(&router, "/signup").await;
assert_eq!(status, http::StatusCode::OK);
// The ONE sign-up page (2026-10-07): the account's name and its address.
assert!(body.contains(r#"<h1 id="heading">Sign up for an account</h1>"#), "{body}");
assert!(body.contains(r#"id="account""#) && body.contains(r#"id="email""#), "the fields: {body}");
// Serving a page asks monetize nothing.
assert!(calls.lock().expect("the call log").is_empty());
}
/// **An address that scrapes names runs out, and the refusals cost monetize
/// nothing.**
///
/// Every answer this route gives is about somebody else's account, so the budget
/// matters more here than on the status poll — and `/auth/signup/start` spends
/// from the same one, or a scraper would simply ask the more expensive route.
#[tokio::test]
async fn scraping_names_is_rate_limited_before_monetize_is_dialled() {
let (router, _, calls) = selling(FakeMonetize::default()).await;
let mut refused = 0;
for guess in 0..40 {
let (status, _) = get(&router, &format!("/auth/signup/name?u=guess{guess:04}")).await;
if status == http::StatusCode::TOO_MANY_REQUESTS {
refused += 1;
}
}
assert!(refused > 0, "forty scraped names were all answered");
assert_eq!(
crate::auth::signup::tests::asks(&calls).len(),
40 - refused,
"a rate-limited check still reached monetize"
);
}
/// **A console that cannot finish a signup has none of these routes at all.**
///
/// Not a 501, which would be a promise printed at somebody who can do nothing
/// about it, and not a path in the open build's route table. Absence is the
/// honest state and the testable one.
#[tokio::test]
async fn a_console_that_sells_nothing_has_no_signup_page_and_no_name_oracle() {
let (router, _, _) = super::console(FakeGunnar::new(), false).await;
for (method, uri) in [
(http::Method::GET, "/signup"),
(http::Method::GET, "/auth/signup/name?u=olle"),
] {
let (status, body) = send(&router, method, uri, None).await;
assert_ne!(
status,
http::StatusCode::OK,
"{uri} was served by a console that cannot sell an account: {body}"
);
}
let (status, body) = post_json(&router, "/auth/signup/start", r#"{"username":"olle"}"#).await;
assert_ne!(status, http::StatusCode::OK, "{body}");
}
/// The CSP hashes it at startup so `script-src` ships without `'unsafe-inline'`.
/// A second inline script would be silently unhashed if the startup code only
/// took the first — `login.html` and `welcome.html` both carry this guard.
#[test]
fn the_signup_page_carries_exactly_one_inline_script() {
let hashes = crate::security::inline_script_hashes(crate::auth::signup::signup_html());
assert_eq!(hashes.len(), 1);
}
/// **The page's own copy of the username rule is the server's**, or a visitor is
/// refused by one and accepted by the other.
#[test]
fn the_signup_page_mirrors_the_rule_the_server_enforces() {
let html = crate::auth::signup::signup_html();
assert!(
html.contains("/^[a-z0-9][a-z0-9._-]{1,31}$/"),
"the page's rule has drifted from monetize_pay::signup::USERNAME_RULE"
);
// And it follows nothing but an absolute https URL to the vendor.
assert!(html.contains("startsWith('https://')"), "{html}");
}
// ---------------------------------------------------------------------------
// Half two: the race, settled after the money moved.
// ---------------------------------------------------------------------------
/// ★ **A customer whose name was taken while they paid types another one**, and
/// the same money makes the account. No refund, no support mail.
#[tokio::test]
async fn a_taken_name_refusal_is_finished_by_renaming_and_the_invite_names_the_new_one() {
let (router, webauthn, calls) = selling(FakeMonetize::name_taken(
"username \"bob\" is taken on this appliance; choose another name",
))
.await;
// The welcome page polls and is told it was refused OVER THE NAME.
let (status, polled) = get(
&router,
&format!("/auth/signup/status?session={SESSION}"),
)
.await;
assert_eq!(status, http::StatusCode::OK, "{polled}");
assert!(polled.contains("\"state\":\"refused\""), "{polled}");
// **The machine-readable half.** The page offers a field on THIS, never on
// the prose — prose is a thing somebody improves.
assert!(polled.contains("\"renameable\":true"), "{polled}");
assert!(polled.contains("\"invite_url\":null"), "a refusal opened a door: {polled}");
let (status, renamed) = post_json(
&router,
"/auth/signup/rename",
&format!(r#"{{"session":"{SESSION}","username":"bob2"}}"#),
)
.await;
assert_eq!(status, http::StatusCode::OK, "{renamed}");
assert!(renamed.contains("\"state\":\"created\""), "{renamed}");
assert!(renamed.contains("\"username\":\"bob2\""), "{renamed}");
// **The link is for the account MONETIZE returned**, exactly as it is on
// the ordinary path: the registration asks monetize again and gunnar
// registers the passkey on that account (thin layer L).
assert!(
renamed.contains(&format!("\"invite_url\":\"/login?signup={SESSION}&user=bob2\"")),
"the page has nothing to redirect to: {renamed}"
);
// The console keeps no passkey: there is no table of its own to keep one in.
let _ = &webauthn;
let log = calls.lock().expect("the call log").clone();
assert!(
log.contains(&format!("Signup.Rename {SESSION} bob2")),
"{log:?}"
);
}
/// **A refusal about the ORDER offers no field.** A plan nobody sells and an
/// amount that drifted are not things a customer can type their way out of, and
/// a page that asked them to would be wasting their time and then still
/// refusing.
#[tokio::test]
async fn a_refusal_that_is_not_about_the_name_is_not_renameable() {
let (router, _, calls) =
selling(FakeMonetize::refused("plan \"gunnar/1tb-forever\" is not sold here")).await;
let (_, polled) = get(
&router,
&format!("/auth/signup/status?session={SESSION}"),
)
.await;
assert!(polled.contains("\"state\":\"refused\""), "{polled}");
assert!(polled.contains("\"renameable\":false"), "{polled}");
assert!(polled.contains("gunnar/1tb-forever"), "the sentence is monetize's: {polled}");
// And the route refuses to try, rather than leaving it to the page.
let (status, body) = post_json(
&router,
"/auth/signup/rename",
&format!(r#"{{"session":"{SESSION}","username":"bob2"}}"#),
)
.await;
assert_eq!(status, http::StatusCode::CONFLICT, "{body}");
let log = calls.lock().expect("the call log").clone();
assert!(
log.iter().any(|c| c.starts_with("Signup.Rename")),
"the console decided on its own; monetize holds the record and must be the one to \
refuse: {log:?}"
);
}
/// **A rename of a session shape that is not the vendor's costs a 400 and no
/// call**, the rule `/auth/signup/status` already holds on the same session id.
#[tokio::test]
async fn a_rename_of_a_session_that_is_not_one_is_refused_before_monetize_is_dialled() {
let (router, _, calls) = selling(FakeMonetize::name_taken("taken")).await;
for bad in ["", "nope", "cs_", "cs_test_short", "cs_test_a1b2c3d4e5f6/../etc"] {
let (status, body) = post_json(
&router,
"/auth/signup/rename",
&format!(r#"{{"session":"{bad}","username":"bob2"}}"#),
)
.await;
assert_eq!(status, http::StatusCode::BAD_REQUEST, "{bad:?}: {body}");
}
// And a name that is not a name, on a session that is.
let (status, body) = post_json(
&router,
"/auth/signup/rename",
&format!(r#"{{"session":"{SESSION}","username":"Bob!"}}"#),
)
.await;
assert_eq!(status, http::StatusCode::BAD_REQUEST, "{body}");
assert!(
calls.lock().expect("the call log").is_empty(),
"a malformed rename reached monetize: {:?}",
calls.lock().expect("the call log")
);
}
/// **A second name that is also taken is the ordinary case**, and the page is
/// told so in monetize's words and asks again.
#[tokio::test]
async fn renaming_to_a_name_that_is_also_taken_is_refused_and_mints_nothing() {
let fake = FakeMonetize {
..FakeMonetize::name_taken("username \"bob\" is taken on this appliance")
};
fake.taken
.lock()
.expect("the name list")
.push("bob2".to_owned());
let (router, webauthn, _) = selling(fake).await;
let (status, body) = post_json(
&router,
"/auth/signup/rename",
&format!(r#"{{"session":"{SESSION}","username":"bob2"}}"#),
)
.await;
assert_eq!(status, http::StatusCode::CONFLICT, "{body}");
assert!(body.contains("choose another"), "{body}");
assert!(!body.contains("/login?signup="), "a refused rename opened a door: {body}");
let _ = webauthn;
}
/// ★ **The welcome page and the routes it talks to agree** — asked of the
/// router, not of the page's text (stinky #22: a grep that went green with
/// the words in a comment). The page is served at `/welcome`; every field of
/// the status answer its script reads is one the route sends, and the field
/// it offers a second name on is monetize's VALUE (`renameable`), true for a
/// name refusal; the rule it checks a name with before posting accepts
/// exactly what the console's own rule accepts; and the rename it posts, to
/// the path and in the shape it posts it, is answered.
#[tokio::test]
async fn the_welcome_page_is_served_and_speaks_its_routes_language() {
let (router, _, _) = selling(FakeMonetize::name_taken("username \"bob\" is taken on this appliance; choose another name")).await;
let (status, page) = get(&router, "/welcome").await;
assert_eq!(status, http::StatusCode::OK, "{page}");
assert_eq!(page, crate::auth::signup::welcome_html(), "/welcome serves the welcome page");
let script = &page[page.find("<script").expect("the page's script")..];
let (_, polled) = get(&router, &format!("/auth/signup/status?session={SESSION}")).await;
let answer: serde_json::Value = serde_json::from_str(&polled).expect("the status answer is JSON");
let read: std::collections::BTreeSet<&str> = script
.match_indices("body.")
.filter(|(at, _)| !script[..*at].ends_with(|c: char| c.is_ascii_alphanumeric() || c == '_' || c == '.'))
.map(|(at, m)| {
let key = &script[at + m.len()..];
&key[..key.find(|c: char| !(c.is_ascii_alphanumeric() || c == '_')).unwrap_or(key.len())]
})
.collect();
assert!(read.contains("renameable"), "the page offers the field on something else: {read:?}");
for key in &read {
assert!(answer.get(key).is_some(), "the page reads `body.{key}`, which the status route never sends: {polled}");
}
assert_eq!(answer["renameable"], serde_json::Value::Bool(true), "{polled}");
let rule = script.split_once("const RULE = /").and_then(|(_, r)| r.split_once("/;")).expect("the page's name rule").0;
let rule = regex::Regex::new(rule).expect("the page's rule is a regular expression");
let long = "a".repeat(32);
let too_long = "a".repeat(33);
for name in ["bob2", "b", "ab", "9lives", "a.b-c_d", "-ab", ".ab", "_ab", "Ab", "åsa", "bob 2", "a/b", "", long.as_str(), too_long.as_str()] {
assert_eq!(rule.is_match(name), crate::auth::validate::username(name), "the page and the console disagree on {name:?}");
}
// What the page sends, to where: the script's own fetch, driven here.
assert!(script.contains("fetch('/auth/signup/rename'"), "the page posts the rename elsewhere");
assert!(script.contains("JSON.stringify({ session: session, username: name })"), "the page posts another shape");
let (status, renamed) = post_json(&router, "/auth/signup/rename", &format!(r#"{{"session":"{SESSION}","username":"bob2"}}"#)).await;
assert_eq!(status, http::StatusCode::OK, "{renamed}");
assert!(renamed.contains("\"state\":\"created\""), "{renamed}");
}
// ---------------------------------------------------------------------------
// The ONE sign-up page's account half (2026-10-07).
// ---------------------------------------------------------------------------
/// ★ **What a new account costs is monetize's answer, as is** — the plans, as
/// words, in the visitor's market; the page prices nothing.
#[tokio::test]
async fn the_account_plans_are_monetizes_answer_as_is() {
let (router, _, calls) = selling(FakeMonetize::default()).await;
let (status, body) = get(&router, "/auth/signup/ladder?market=se").await;
assert_eq!(status, http::StatusCode::OK, "{body}");
let j: serde_json::Value = serde_json::from_str(&body).unwrap();
assert_eq!(j["plans"][0]["id"], "gunnar/10gb-1m");
assert_eq!(j["plans"][0]["name"], "10 GB");
assert_eq!(j["plans"][0]["price_words"], "100.00 SEK a month");
assert!(crate::auth::signup::tests::read_plans(&calls, "gunnar", crate::mpb::Market::Se as i32), "{:?}", calls.lock().unwrap());
assert_eq!(j["market"], "SE", "the answer names the one market it is");
let (status, _) = get(&router, "/auth/signup/ladder?market=xx").await;
assert_eq!(status, http::StatusCode::BAD_REQUEST, "a market nobody sells in dials nobody");
}
/// A GET with an `Accept-Language`, as a browser sends it.
async fn get_speaking(app: &axum::Router, uri: &str, languages: &str) -> (http::StatusCode, String) {
use tower::ServiceExt;
let request = http::Request::builder().uri(uri).header(http::header::ACCEPT_LANGUAGE, languages).body(axum::body::Body::empty()).expect("a request");
let response = app.clone().oneshot(request).await.expect("the console answers");
let status = response.status();
let body = axum::body::to_bytes(response.into_body(), 1 << 20).await.expect("a readable body");
(status, String::from_utf8_lossy(&body).into_owned())
}
/// ★ **The visitor's market is the server's to decide** (test2, owner
/// 2026-10-08: on /signup?plan=10gb every + changed the CURRENCY — the page
/// named no market for an English browser, and monetize answered all three).
/// The console reads the browser's languages (`Accept-Language`, in its order
/// of preference: a Swede on an English browser still lists `sv`) and asks
/// monetize for THAT market; naming none, monetize's default decides — and
/// either way the answer names the one market it is.
#[tokio::test]
async fn the_visitors_market_is_the_servers_from_the_browsers_languages() {
let (router, _, calls) = selling(FakeMonetize::default()).await;
for (languages, market, said) in [
("da-DK,da;q=0.9,en;q=0.8", crate::mpb::Market::Dk, "DK"),
("fi-FI,fi;q=0.9", crate::mpb::Market::Fi, "FI"),
("en-US,en;q=0.9,sv;q=0.8", crate::mpb::Market::Se, "SE"),
("en;q=0.5,da;q=0.9", crate::mpb::Market::Dk, "DK"),
("en-US,en;q=0.9", crate::mpb::Market::Unspecified, "SE"),
] {
let (status, body) = get_speaking(&router, "/auth/signup/ladder", languages).await;
assert_eq!(status, http::StatusCode::OK, "{languages}: {body}");
// The site keeps each market's ladder (LADDER-2): the market asked was read, once.
assert!(crate::auth::signup::tests::read_plans(&calls, "gunnar", market as i32), "{languages}: {:?}", calls.lock().unwrap());
let j: serde_json::Value = serde_json::from_str(&body).unwrap();
assert_eq!(j["market"], said, "{languages}: {j}");
}
// A market the page names still wins (an older page): FI's kept ladder, not DK's.
let (status, body) = get_speaking(&router, "/auth/signup/ladder?market=fi", "da-DK").await;
assert_eq!(status, http::StatusCode::OK);
assert_eq!(serde_json::from_str::<serde_json::Value>(&body).unwrap()["market"], "FI", "{body}");
assert!(crate::auth::signup::tests::read_plans(&calls, "gunnar", crate::mpb::Market::Fi as i32), "{:?}", calls.lock().unwrap());
}
/// ★ **The address typed ONCE, the plan monetize named** — the page's one
/// e-mail field (the owner's field list) and its − / +'s step; a page that
/// still sends the address twice has the two compared.
#[tokio::test]
async fn an_account_signup_names_monetizes_plan_and_may_type_its_address_once() {
let (router, _, calls) = selling(FakeMonetize::default()).await;
let (status, body) = post_json(&router, "/auth/signup/start", r#"{"username":"olle","email":"olle@example.com","plan":"gunnar/10gb-1m"}"#).await;
assert_eq!(status, http::StatusCode::OK, "{body}");
let line = calls.lock().unwrap().last().cloned().unwrap();
assert!(line.starts_with("Signup.Start olle gunnar gunnar/10gb-1m "), "{line}");
let (status, body) = post_json(&router, "/auth/signup/start", r#"{"username":"olle","email":"olle@example.com","email_confirm":"olle@example.org"}"#).await;
assert_eq!(status, http::StatusCode::BAD_REQUEST, "two addresses that differ: {body}");
let (status, _) = post_json(&router, "/auth/signup/start", r#"{"username":"olle","email":"olle@example.com","plan":"<script>"}"#).await;
assert_eq!(status, http::StatusCode::BAD_REQUEST, "a plan id of no shape monetize hands out");
}
/// ★ **A new visitor walks every priced stop and starts the one chosen**
/// (owner on test2 2026-10-09: "+ / − don't work"). Where monetize prices
/// plans it hands a new account the room rule's stops — the month at 10 GiB,
/// then 12 months from 10 to 1000 GiB — each under a PRICED id; the ladder
/// passes every one through in monetize's order, and `/auth/signup/start`
/// hands monetize exactly the id of the stop the − / + landed on, so the
/// checkout is minted for that room at that price.
#[tokio::test]
async fn a_new_visitors_ladder_walks_every_priced_stop_and_starts_the_chosen_one() {
let (router, _, calls) = selling(FakeMonetize { priced_ladder: true, ..Default::default() }).await;
let (status, body) = get(&router, "/auth/signup/ladder?market=se").await;
assert_eq!(status, http::StatusCode::OK, "{body}");
let j: serde_json::Value = serde_json::from_str(&body).unwrap();
let plans = j["plans"].as_array().expect("plans");
assert!(plans.len() >= 7, "the month and the six 12-month stops: {j}");
let ids: Vec<&str> = plans.iter().map(|p| p["id"].as_str().unwrap()).collect();
assert_eq!(ids.first(), Some(&"gunnar/10gb-1m@11000SEK"), "10 GB · 1 month first: {ids:?}");
assert_eq!(ids.last(), Some(&"gunnar/1000gb-12m@3049000SEK"), "1 TB at the top: {ids:?}");
let prices: std::collections::BTreeSet<&str> = plans.iter().map(|p| p["price_words"].as_str().unwrap()).collect();
assert_eq!(prices.len(), plans.len(), "the price moves with every stop: {prices:?}");
let words = body.to_ascii_lowercase();
for never in ["room left", "\"cap", "capacity"] {
assert!(!words.contains(never), "a visitor never reads {never:?}: {body}");
}
// The stop the − / + landed on is the one started — the top, and one between.
for chosen in [ids[ids.len() - 1], ids[3]] {
let (status, body) = post_json(&router, "/auth/signup/start", &format!(r#"{{"username":"olle","email":"olle@example.com","plan":"{chosen}","market":"SE"}}"#)).await;
assert_eq!(status, http::StatusCode::OK, "{chosen}: {body}");
let line = calls.lock().unwrap().last().cloned().unwrap();
assert!(line.starts_with(&format!("Signup.Start olle gunnar {chosen} ")), "{line}");
let j: serde_json::Value = serde_json::from_str(&body).unwrap();
assert_eq!(j["url"], "https://checkout.example/pay/cs_test_minted", "{j}");
}
}
/// ★ **The front page's two offers lead to the ONE sign-up page**, each with
/// its choice preselected — what `/auth/status` hands the login page — and a
/// console that is no private instance says so.
#[tokio::test]
async fn the_status_points_both_offers_at_the_sign_up_page() {
let (router, _, _) = selling(FakeMonetize::default()).await;
let (status, body) = get(&router, "/auth/status").await;
assert_eq!(status, http::StatusCode::OK, "{body}");
let j: serde_json::Value = serde_json::from_str(&body).unwrap();
assert_eq!(j["buy_url"], "/signup?plan=10gb");
assert_eq!(j["private_url"], "/signup?plan=private");
assert_eq!(j["signup_url"], "/signup", "the first page's ONE paid link (owner 2026-10-08)");
assert!(j["instance"].is_null(), "gunnar.rs is no private instance: {j}");
// ★ Round 5.3.1: what an account is, as monetize sells it (Signup.Plans' first plan).
assert_eq!(j["account_offer"], "10GB", "{j}");
}
/// ★ **No account pill where monetize sells no account, and its configuration words never
/// reach a visitor** (round 5.3.1, wiring tracer B 2026-10-08): Signup.Plans answering no plan
/// hands the page no account offer; a Start monetize refuses for want of a signup sink is said
/// as "An account is not sold here right now."
#[tokio::test]
async fn where_monetize_sells_no_account_the_visitor_reads_plain_words() {
let (router, _, _) = selling(FakeMonetize { no_plans: true, ..FakeMonetize::default() }).await;
let (status, body) = get(&router, "/auth/status").await;
assert_eq!(status, http::StatusCode::OK, "{body}");
let j: serde_json::Value = serde_json::from_str(&body).unwrap();
assert!(j["account_offer"].is_null(), "an account offer monetize does not sell: {j}");
let (router, _, _) = selling(FakeMonetize {
start_refusal: Some((tonic::Code::Unimplemented, "no signup sink is loaded for gunnar: this server creates no gunnar account, so it sells none".into())),
..FakeMonetize::default()
})
.await;
let (status, body) = post_json(&router, "/auth/signup/start", r#"{"username":"olle","email":"olle@example.com","plan":"gunnar/account"}"#).await;
assert_eq!(status, http::StatusCode::NOT_IMPLEMENTED, "{body}");
assert!(body.contains(crate::auth::signup::ACCOUNT_NOT_SOLD_HERE), "{body}");
for leak in ["sink", "[signup]", "monetize"] {
assert!(!body.contains(leak), "{leak:?} reached the visitor: {body}");
}
}
/// ★ **No private door where no private Gunnar is sold** (test2, owner
/// 2026-10-08): a console whose monetize sells no private estates
/// (`[estate_orders]` absent; `EstateOrder.Sells` false) hands the sign-up
/// page no private choice — the account alone — and the first page's paid
/// link still leads to the sign-up page.
#[tokio::test]
async fn a_console_whose_monetize_sells_no_estates_hands_out_no_private_door() {
let (router, _, _) = selling_estates(FakeMonetize::default(), false).await;
let (status, body) = get(&router, "/auth/status").await;
assert_eq!(status, http::StatusCode::OK, "{body}");
let j: serde_json::Value = serde_json::from_str(&body).unwrap();
assert!(j["private_url"].is_null(), "a private door where none is sold: {j}");
assert_eq!(j["buy_url"], "/signup?plan=10gb", "{j}");
assert_eq!(j["signup_url"], "/signup", "{j}");
}
// ---------------------------------------------------------------------------
// One shop (owner 2026-10-07): a SOLD Gunnar offers no purchase.
// ---------------------------------------------------------------------------
/// ★ **A SOLD (provided) Gunnar offers no purchase** (one shop, owner
/// 2026-10-07): only gunnar.rs's monetize sells; a sold estate's own monetize
/// only executes. Its console names no account door, says it does not sell
/// and where its shop is, serves no sign-up page, and registers no route that
/// starts an order — while the reads stay (the window still asks monetize,
/// which offers nothing). The same requests on a console that sells reach
/// their handlers, so every 404 / 405 below is the route's absence and not
/// the rig's.
#[tokio::test]
async fn a_provided_console_offers_no_purchase() {
let sold = provided(FakeMonetize::default()).await;
let (status, body) = get(&sold, "/auth/status").await;
assert_eq!(status, http::StatusCode::OK, "{body}");
let s: serde_json::Value = serde_json::from_str(&body).expect("JSON");
assert!(s["buy_url"].is_null(), "no account door on a sold Gunnar: {s}");
assert_eq!(s["private_url"], "https://gunnar.rs/signup?plan=private", "New instance is the provider's: {s}");
assert_eq!(s["shop_here"], false, "{s}");
assert!(s["shop_line"].as_str().is_some_and(|l| l.contains("gunnar.rs")), "where its shop is: {s}");
assert_eq!(s["provider"], "https://gunnar.rs");
assert_eq!(s["signup_url"], "https://gunnar.rs/signup", "its paid link is its provider's sign-up page: {s}");
let (selling_router, _, _) = selling(FakeMonetize::default()).await;
let (_, mine) = get(&selling_router, "/auth/status").await;
let mine: serde_json::Value = serde_json::from_str(&mine).expect("JSON");
assert_eq!((mine["buy_url"].as_str(), mine["shop_here"].as_bool()), (Some("/signup?plan=10gb"), Some(true)), "a console that sells: {mine}");
// No sign-up page, no welcome, no private order on a sold Gunnar.
for path in ["/signup", "/welcome", "/private", "/auth/signup/ladder"] {
let (on_sold, _) = super::send(&sold, http::Method::GET, path, Some("break-glass")).await;
assert_eq!(on_sold, http::StatusCode::NOT_FOUND, "{path} on a sold Gunnar");
let (on_selling, _) = super::send(&selling_router, http::Method::GET, path, Some("break-glass")).await;
assert_ne!(on_selling, http::StatusCode::NOT_FOUND, "{path} is served where the console sells");
}
// No route that starts an order; the reads stay.
for (path, absent) in [
("/api/account/subscription/renew", http::StatusCode::NOT_FOUND),
("/api/account/subscription/buy-bundle", http::StatusCode::NOT_FOUND),
("/api/namespaces/nordisk/increase/bundle", http::StatusCode::NOT_FOUND),
("/api/namespaces/nordisk/increase", http::StatusCode::METHOD_NOT_ALLOWED),
] {
let (on_sold, body) = post_as_operator(&sold, path).await;
assert_eq!(on_sold, absent, "POST {path} on a sold Gunnar: {body}");
let (on_selling, body) = post_as_operator(&selling_router, path).await;
assert!(on_selling != http::StatusCode::NOT_FOUND && on_selling != http::StatusCode::METHOD_NOT_ALLOWED, "POST {path} reaches its handler where the console sells: {on_selling} {body}");
}
for path in ["/api/namespaces/nordisk/increase/bundles", "/api/account/subscription/history"] {
let (on_sold, body) = super::send(&sold, http::Method::GET, path, Some("break-glass")).await;
assert!(on_sold != http::StatusCode::NOT_FOUND && on_sold != http::StatusCode::METHOD_NOT_ALLOWED, "GET {path} still asks monetize on a sold Gunnar: {on_sold} {body}");
}
}
/// ★ **LOW-PRIV (the Gunnar half): a SOLD private Gunnar says at /signup that it is private**
/// and to ask its admin (its owner bought it at the provider; it sells nothing itself). A
/// console that sells keeps its sign-up page.
#[tokio::test]
async fn a_sold_private_gunnar_says_at_signup_that_it_is_private() {
let sold = provided(FakeMonetize::default()).await;
for page in ["/signup", "/signup?plan=private"] {
let (status, body) = get(&sold, page).await;
assert_eq!((status, body.as_str()), (http::StatusCode::NOT_FOUND, crate::serve::PRIVATE_SIGNUP), "{page}");
}
let (selling_router, _, _) = selling(FakeMonetize::default()).await;
let (status, body) = get(&selling_router, "/signup").await;
assert_eq!(status, http::StatusCode::OK, "a console that sells serves its sign-up page");
assert!(!body.contains("This Gunnar is private"), "{body}");
}
/// A JSON POST carrying the console's break-glass bearer: the operator.
async fn post_as_operator(app: &axum::Router, uri: &str) -> (http::StatusCode, String) {
use tower::ServiceExt;
let request = http::Request::builder()
.method(http::Method::POST)
.uri(uri)
.header(http::header::AUTHORIZATION, "Bearer break-glass")
.header(http::header::CONTENT_TYPE, "application/json")
.body(axum::body::Body::from("{}"))
.expect("a request");
let response = app.clone().oneshot(request).await.expect("the console answers");
let status = response.status();
let body = axum::body::to_bytes(response.into_body(), 1 << 20).await.expect("a readable body");
(status, String::from_utf8_lossy(&body).into_owned())
}
/// ★ **An account here comes back to the private order** (one shop, owner
/// 2026-10-07): a visitor buying a private Gunnar first makes their account
/// with the shop's account plan; the return URL monetize is handed carries
/// where the page sends them after the passkey (`next`, a path on this
/// console, nothing else), and the welcome page monetize answers with is
/// followed like a checkout.
#[tokio::test]
async fn an_account_here_returns_to_the_private_order() {
let (router, _, calls) = selling(FakeMonetize { checkout: Some("https://console.example/welcome?session=acct_0123&next=%2Fsignup%3Fplan%3Dprivate".into()), ..FakeMonetize::default() }).await;
let (status, body) = post_json(&router, "/auth/signup/start", r#"{"username":"olle","email":"olle@example.com","plan":"gunnar/account","next":"/signup?plan=private"}"#).await;
assert_eq!(status, http::StatusCode::OK, "{body}");
assert!(body.contains("https://console.example/welcome?session=acct_0123"), "{body}");
let (status, body) = post_json(&router, "/auth/signup/start", r#"{"username":"olle","email":"olle@example.com","plan":"gunnar/account","next":"//evil.example/x"}"#).await;
assert_eq!(status, http::StatusCode::OK, "{body}");
let log = crate::auth::signup::tests::asks(&calls);
assert_eq!(
log,
vec![
"Signup.Start olle gunnar gunnar/account https://console.example/welcome?session={session}&next=%2Fsignup%3Fplan%3Dprivate olle@example.com".to_owned(),
"Signup.Start olle gunnar gunnar/account https://console.example/welcome?session={session} olle@example.com".to_owned(),
],
"the way back rides the return URL; a path off this console does not"
);
}
/// ★ **A member gate that cannot ask gunnar shuts** (stinky 2026-10-10 #16).
/// `standing()` turned "gunnar did not answer who this is" into "not a
/// member", so a member posting `start` while gunnar was unreachable was
/// handed a checkout for a NEW account. `ghost` is signed in, bound to a
/// principal this console cannot sign as: nobody can say whether they are a
/// member, so nothing is started.
#[tokio::test]
async fn a_signup_start_whose_member_check_cannot_be_answered_starts_nothing() {
use tower::ServiceExt;
let (router, webauthn, calls) = selling(FakeMonetize::default()).await;
webauthn.plant_bound_credential_for_tests("ghost", crate::role::Role::Operator, "ghost");
let cookie = webauthn.make_session("ghost", crate::role::Role::Operator, true);
let request = http::Request::builder()
.method(http::Method::POST)
.uri("/auth/signup/start")
.header(http::header::CONTENT_TYPE, "application/json")
.header(http::header::COOKIE, format!("{}={cookie}", crate::auth::SESSION_COOKIE))
.header(http::header::ORIGIN, "https://console.example")
.body(axum::body::Body::from(r#"{"username":"olle","email":"olle@example.com"}"#))
.expect("a request");
let response = router.oneshot(request).await.expect("the console answers");
let status = response.status();
let body = axum::body::to_bytes(response.into_body(), 1 << 20).await.expect("a body");
assert_eq!(status, http::StatusCode::SERVICE_UNAVAILABLE, "{}", String::from_utf8_lossy(&body));
assert!(
calls.lock().expect("the call log").iter().all(|c| !c.starts_with("Signup.Start")),
"a checkout was started for somebody who may be a member: {:?}",
calls.lock().expect("the call log")
);
}
/// ★ **"Not sold here" is read off monetize's CODE, never its words** (stinky
/// gunnar-ui #7, 5.4): UNIMPLEMENTED in any words is "An account is not sold
/// here right now."; a FAILED_PRECONDITION is a refusal about the order even
/// when its prose sounds like configuration — a reworded sentence must not
/// move a visitor from one answer to the other.
#[tokio::test]
async fn not_sold_here_is_read_off_the_code_and_never_off_the_words() {
let not_sold = "no vendor here can sell a gunnar signup at 10000 SEK in Se: [products.gunnar] names no `vendor`; serving: []";
let (router, _, _) = selling(FakeMonetize { start_refusal: Some((tonic::Code::Unimplemented, not_sold.into())), ..FakeMonetize::default() }).await;
let (status, body) = post_json(&router, "/auth/signup/start", r#"{"username":"olle","email":"olle@example.com","email_confirm":"olle@example.com"}"#).await;
assert_eq!(status, http::StatusCode::NOT_IMPLEMENTED, "{body}");
assert!(body.contains(crate::auth::signup::ACCOUNT_NOT_SOLD_HERE), "{body}");
let (router, _, _) = selling(FakeMonetize { start_refusal: Some((tonic::Code::FailedPrecondition, not_sold.into())), ..FakeMonetize::default() }).await;
let (status, body) = post_json(&router, "/auth/signup/start", r#"{"username":"olle","email":"olle@example.com","email_confirm":"olle@example.com"}"#).await;
assert_ne!(status, http::StatusCode::NOT_IMPLEMENTED, "FAILED_PRECONDITION was re-classified by its prose: {body}");
assert!(!body.contains(crate::auth::signup::ACCOUNT_NOT_SOLD_HERE), "{body}");
let (router, _, _) = selling(FakeMonetize { plans_refusal: Some((tonic::Code::Unimplemented, "no signup sink is loaded for gunnar".into())), ..FakeMonetize::default() }).await;
let (status, body) = get(&router, "/auth/signup/ladder").await;
assert_eq!(status, http::StatusCode::NOT_IMPLEMENTED, "{body}");
let (router, _, _) = selling(FakeMonetize { plans_refusal: Some((tonic::Code::FailedPrecondition, "no signup sink is loaded for gunnar".into())), ..FakeMonetize::default() }).await;
let (status, body) = get(&router, "/auth/signup/ladder").await;
assert_ne!(status, http::StatusCode::NOT_IMPLEMENTED, "FAILED_PRECONDITION was re-classified by its prose: {body}");
}
/// ★ **[Send again] on the welcome page** (owner 2026-10-11): `POST /auth/signup/sign-in-again`
/// `{session, next}` asks monetize to mail the signup's link again — this console's own
/// welcome page for that session (and its way back), never a page the browser names — and
/// answers monetize's sentence (the address masked). A session of the wrong shape costs a
/// 400 and no call; a signup not made yet is not mailed.
#[tokio::test]
async fn send_again_mails_the_link_to_this_consoles_welcome_page() {
let (router, _, calls) = selling(FakeMonetize::created("bob")).await;
let (status, body) = post_json(&router, "/auth/signup/sign-in-again", &format!(r#"{{"session":"{SESSION}","next":"/signup?plan=10gb"}}"#)).await;
assert_eq!(status, http::StatusCode::OK, "{body}");
assert!(body.contains("bo***@example.invalid"), "monetize's masked sentence: {body}");
let log = calls.lock().expect("the call log").clone();
let sent = log.iter().find(|c| c.starts_with("Signup.SendSignInAgain")).unwrap_or_else(|| panic!("monetize was not asked: {log:?}"));
assert!(sent.contains(&format!("gunnar {SESSION} https://")), "{sent}");
assert!(sent.ends_with("/welcome?session={session}&next=%2Fsignup%3Fplan%3D10gb"), "this console's welcome page and its way back: {sent}");
let before = calls.lock().expect("the call log").len();
for bad in ["", "nope", "cs_test_a1b2c3d4e5f6/../etc"] {
let (status, body) = post_json(&router, "/auth/signup/sign-in-again", &format!(r#"{{"session":"{bad}"}}"#)).await;
assert_eq!(status, http::StatusCode::BAD_REQUEST, "{bad:?}: {body}");
}
assert_eq!(calls.lock().expect("the call log").len(), before, "a malformed session reached monetize");
let (router, _, calls) = selling(FakeMonetize::default()).await;
let (status, body) = post_json(&router, "/auth/signup/sign-in-again", &format!(r#"{{"session":"{SESSION}"}}"#)).await;
assert_eq!(status, http::StatusCode::CONFLICT, "not made yet: {body}");
assert!(!calls.lock().expect("the call log").iter().any(|c| c.starts_with("Signup.SendSignInAgain")), "a signup not made yet was mailed");
}
/// ★ **The shop not answering** (owner 2026-10-12): the visitor reads one plain
/// sentence — no code, no internal word — and the last offer this console was
/// given, READ-ONLY (`last_known`; the page draws it with buying off).
#[tokio::test]
async fn a_shop_that_does_not_answer_says_so_plainly_and_shows_the_last_offer_read_only() {
// A good answer first: the console keeps it.
let (router, _, _) = selling(FakeMonetize::default()).await;
let (status, body) = get(&router, "/auth/signup/ladder?market=dk").await;
assert_eq!(status, http::StatusCode::OK, "{body}");
// Then the ONE box stops answering.
let (router, _, _) = selling(FakeMonetize { plans_refusal: Some((tonic::Code::Unavailable, "tcp connect error: Connection refused (os error 111)".into())), ..FakeMonetize::default() }).await;
let (status, body) = get(&router, "/auth/signup/ladder?market=dk").await;
assert_eq!(status, http::StatusCode::SERVICE_UNAVAILABLE, "{body}");
let j: serde_json::Value = serde_json::from_str(&body).expect("json");
assert_eq!(j["error"], "The shop is unavailable right now — try again in a few minutes.", "{j}");
assert_eq!(j["unavailable"], true);
assert!(!body.contains("tcp") && !body.contains("os error") && !body.contains("Unavailable"), "no internal words: {body}");
assert_eq!(j["last_known"]["plans"][0]["price_words"], "100.00 SEK a month", "the last offer, read-only: {j}");
assert!(j["as_of_unix_ms"].as_u64().is_some_and(|t| t > 0), "{j}");
}
/// The page's half: an unavailable shop draws the last offer with buying off,
/// and a failed fetch is the same plain sentence, never `the server answered …`.
#[test]
fn the_page_draws_the_last_offer_read_only_and_says_the_shop_is_down_plainly() {
let html = crate::auth::signup::signup_html();
assert!(html.contains("const SHOP_DOWN = 'The shop is unavailable right now — try again in a few minutes.';"));
assert_eq!(crate::auth::signup::SHOP_UNAVAILABLE, "The shop is unavailable right now — try again in a few minutes.", "one sentence, page and server");
assert!(html.contains("if (!r.ok && j.unavailable) {"), "the account ladder reads the unavailable answer");
assert!(html.contains("total: 'last known price — buying is off', ok: false"), "drawn read-only, labelled");
assert!(!html.contains("'the server answered '"), "no status codes for a visitor");
}
/// ★ **The vendor's way back is the ONE order page** (TASKS-4-v2 T16, A28 M11): `/paid/
/// {reference}` and `/cancel/{reference}` are served by this console as the shared crate's
/// order page script (monetize-site-buy `assets::paid_js`, the one Holger serves too) with
/// Gunnar's words and ways on, in Gunnar's look, as the page's one inline script — hashed into
/// the CSP. Signed out it is still the login door's, and a private order's way back
/// (`/paid/estate/<company>`) is still the shared router's.
#[tokio::test]
async fn the_vendors_way_back_is_the_one_order_page_with_gunnars_words() {
use tower::ServiceExt;
let (router, webauthn, _) = selling(FakeMonetize::default()).await;
let back = monetize_site_buy::assets::Back { again: "/account/storage".into(), account: "/account".into(), login: "/login".into() };
let script = monetize_site_buy::assets::paid_js(&crate::auth::signup::GUNNAR_PAGES, &back);
let cookie = webauthn.make_session("olle", crate::role::Role::Viewer, true);
for path in ["/paid/gunnar/acme/2026-10-26", "/cancel/gunnar/acme/2026-10-26"] {
let request = http::Request::get(path).header(http::header::COOKIE, format!("{}={cookie}", crate::auth::SESSION_COOKIE)).body(axum::body::Body::empty()).unwrap();
let response = router.clone().oneshot(request).await.unwrap();
let status = response.status();
let csp = response.headers().get(http::header::CONTENT_SECURITY_POLICY).map(|v| v.to_str().unwrap().to_owned()).unwrap_or_default();
let page = String::from_utf8(axum::body::to_bytes(response.into_body(), 1 << 20).await.unwrap().to_vec()).unwrap();
assert_eq!(status, http::StatusCode::OK, "{path}: {page}");
assert!(page.contains(&script), "{path}: the shared order page script, with Gunnar's words and ways on");
for id in monetize_site_buy::assets::PAID_IDS {
assert!(page.contains(&format!("id=\"{id}\"")), "{path}: the page carries #{id}");
}
let hashes = crate::security::inline_script_hashes(&page);
assert_eq!(hashes.len(), 1, "{path}: one inline script");
assert!(csp.contains(&hashes[0]), "{path}: its hash is in the CSP: {csp}");
}
// Signed out, a browser's navigation is sent to the login page and back, as before.
let request = http::Request::get("/paid/gunnar/acme/2026-10-26").header(http::header::ACCEPT, "text/html").body(axum::body::Body::empty()).unwrap();
let response = router.clone().oneshot(request).await.unwrap();
assert!(response.status().is_redirection(), "signed out: the login door: {}", response.status());
let to = response.headers().get(http::header::LOCATION).unwrap().to_str().unwrap().to_owned();
assert!(to.starts_with("/login?next="), "{to}");
}