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

nordisk/gunnar-ui

crates/gunnar-ui-server/src/anonymous/private_order.rs

//! **The private-gunnar buy page, driven the way a browser with no session
//! drives it** — against a fake monetize that answers `EstateOrder` and
//! records what it was asked.
//!
//! What is asserted is the thin layer: the page sends what the buyer chose
//! and shows what monetize answers — the price as monetize's words, the
//! estate as monetize's picture (passed through byte for byte), monetize's
//! sentence on a refusal. Nothing here sizes, prices or draws.

use std::sync::{Arc, Mutex};

use crate::mpb;
use crate::mpb::estate_order_server::{EstateOrder, EstateOrderServer};

use super::{send, FakeGunnar};

/// `EstateOrder` as monetize answers it — or, `unsold`, as a monetize with no
/// `[estate_orders]` does: `Sells` false, and every order call refused with
/// its operator's sentence (the one test2 showed a visitor, 2026-10-08).
#[derive(Clone, Default)]
pub(super) struct FakeEstateOrder {
    pub(super) calls: Arc<Mutex<Vec<String>>>,
    pub(super) unsold: bool,
}

/// monetize's refusal where `[estate_orders]` is absent, word for word.
pub(super) const NOT_SOLD_CONFIG: &str = "this monetize sells no private estates ([estate_orders] is absent)";

impl FakeEstateOrder {
    fn refuse_unsold(&self) -> Result<(), tonic::Status> {
        if self.unsold {
            return Err(tonic::Status::failed_precondition(NOT_SOLD_CONFIG));
        }
        Ok(())
    }
}

#[tonic::async_trait]
impl EstateOrder for FakeEstateOrder {
    /// [Rebuild main]: monetize's guardian as the console meets it — a silent main, its
    /// remedy, and a press recorded with the principal it names.
    async fn rebuild_main(&self, r: tonic::Request<mpb::EstateRebuildMainRequest>) -> Result<tonic::Response<mpb::EstateRebuildMainResponse>, tonic::Status> {
        let r = r.into_inner();
        self.calls.lock().unwrap().push(format!("rebuild_main press={} principal={} strong={}", r.press, r.principal, r.strong));
        if r.press && !r.strong {
            return Err(tonic::Status::permission_denied("a button needs a named principal in a fresh, user-verified session"));
        }
        let mut a = mpb::EstateRebuildMainResponse {
            finding: "main_lost".into(),
            words: "the main is silent, and the twin cannot see it either: the main is rebuilt from the twin (never promoted)".into(),
            remedy: "new_server".into(),
            ..Default::default()
        };
        if r.press {
            a.case_id = "acme-1".into();
            a.case_state = "approved".into();
        }
        Ok(tonic::Response::new(a))
    }
    async fn start(&self, r: tonic::Request<mpb::EstateOrderStartRequest>) -> Result<tonic::Response<mpb::EstateOrderStartResponse>, tonic::Status> {
        self.refuse_unsold()?;
        let r = r.into_inner();
        self.calls.lock().unwrap().push(format!("start {} {} {} owner={} new={} welcome={} ticket={}", r.company, r.email, r.domain, r.account, r.new_account, r.welcome_url, r.actor_ticket.len()));
        if r.company == "taken" {
            return Err(tonic::Status::already_exists("taken.gunnar.rs is taken"));
        }
        Ok(tonic::Response::new(mpb::EstateOrderStartResponse {
            reference: format!("estate/{}", r.company),
            checkout_url: "https://checkout.fake-stripe.test/c/pay/cs_test_1".into(),
            state: "waiting_for_payment".into(),
            words: "Waiting for your payment.".into(),
            price_words: "792.00 EUR".into(),
        }))
    }
    async fn status(&self, r: tonic::Request<mpb::EstateOrderStatusRequest>) -> Result<tonic::Response<mpb::EstateOrderStatusResponse>, tonic::Status> {
        let c = r.into_inner().company;
        self.calls.lock().unwrap().push(format!("status {c}"));
        Ok(tonic::Response::new(mpb::EstateOrderStatusResponse {
            company: c.clone(),
            state: "waiting_for_dns".into(),
            words: "Add the two records below.".into(),
            url: String::new(),
            phase: "waiting_dns".into(),
            dns_records: vec![
                mpb::EstateDnsRecord { name: "gunnar.company1.com".into(), kind: "CNAME".into(), value: format!("{c}.gunnar.rs") },
                mpb::EstateDnsRecord { name: "_gunnar-proof.gunnar.company1.com".into(), kind: "TXT".into(), value: "gunnar-proof=ab12".into() },
            ],
            paid_until: String::new(),
            change_status: String::new(),
            change_words: String::new(),
            why: String::new(),
            why_since: String::new(),
            // monetize FIRST-LINK-1 (14b83bb6) added the first-admin door's fields; none here.
            ..Default::default()
        }))
    }
    async fn renew(&self, _r: tonic::Request<mpb::EstateOrderRenewRequest>) -> Result<tonic::Response<mpb::EstateOrderRenewResponse>, tonic::Status> {
        Err(tonic::Status::unimplemented("the buy page does not renew"))
    }
    async fn change_plan(&self, _r: tonic::Request<mpb::EstateOrderChangePlanRequest>) -> Result<tonic::Response<mpb::EstateOrderChangePlanResponse>, tonic::Status> {
        Err(tonic::Status::unimplemented("the buy page does not change plans"))
    }
    async fn notice(&self, _r: tonic::Request<mpb::EstateNoticeRequest>) -> Result<tonic::Response<mpb::EstateNoticeResponse>, tonic::Status> {
        Ok(tonic::Response::new(mpb::EstateNoticeResponse::default()))
    }
    async fn purchases(&self, _r: tonic::Request<mpb::EstateOrderPurchasesRequest>) -> Result<tonic::Response<mpb::EstateOrderPurchasesResponse>, tonic::Status> {
        Err(tonic::Status::unimplemented("the buy page reads no purchases"))
    }
    async fn sells(&self, _r: tonic::Request<mpb::EstateOrderSellsRequest>) -> Result<tonic::Response<mpb::EstateOrderSellsResponse>, tonic::Status> {
        Ok(tonic::Response::new(mpb::EstateOrderSellsResponse { estates: !self.unsold }))
    }
    async fn cancel(&self, _r: tonic::Request<mpb::EstateOrderCancelRequest>) -> Result<tonic::Response<mpb::EstateOrderCancelResponse>, tonic::Status> {
        Err(tonic::Status::unimplemented("the buy page does not cancel"))
    }
    async fn send_sign_in_again(&self, _r: tonic::Request<mpb::EstateOrderSignInAgainRequest>) -> Result<tonic::Response<mpb::EstateOrderSignInAgainResponse>, tonic::Status> {
        Err(tonic::Status::unimplemented("the buy page does not ask for sign-in links"))
    }
    async fn settle(&self, _r: tonic::Request<mpb::EstateOrderSettleRequest>) -> Result<tonic::Response<mpb::EstateOrderSettleResponse>, tonic::Status> {
        Err(tonic::Status::permission_denied("the operator's verb is not the buy page's"))
    }
    async fn ladder(&self, r: tonic::Request<mpb::EstateOrderLadderRequest>) -> Result<tonic::Response<mpb::EstateOrderLadderResponse>, tonic::Status> {
        self.calls.lock().unwrap().push(format!("ladder {:?}", r.into_inner().company));
        self.refuse_unsold()?;
        let step = |name: &str, main: &str, data: u64, monthly: &str, available: bool, reason: &str| mpb::EstateLadderStep {
            name: name.into(),
            sizes: Some(mpb::EstateSizes { front_plan: "STARTER-2xCPU-4GB".into(), main_plan: main.into(), main_data_gib: data, ..Default::default() }),
            twin_plan: "STARTER-1xCPU-4GB".into(),
            machines: vec![mpb::EstateMachineLine { role: "main".into(), plan: main.into(), cores: 4, memory_mib: 8_192, disks_gib: vec![50, data, data, data, data] }],
            resources_words: format!("7 cores · 16 GB RAM · {} GB data", 4 * data),
            monthly_words: monthly.into(),
            total_words: "6 months prepaid: 648.00 EUR".into(),
            pay_words: "648.00 EUR".into(),
            available,
            reason: reason.into(),
        };
        Ok(tonic::Response::new(mpb::EstateOrderLadderResponse {
            steps: vec![
                step("Mini", "STARTER-4xCPU-8GB", 10, "108.00 EUR a month", true, ""),
                step("Gigantic", "20xCPU-128GB", 500, "", false, "the provider does not sell the plan \"20xCPU-128GB\" for main"),
            ],
            start: 0,
            laid: false,
            current: 0,
            floor: 0,
            apex: "gunnar.rs".into(),
            term_months: 6,
        }))
    }
}

async fn selling() -> (axum::Router, Arc<Mutex<Vec<String>>>) {
    selling_as(FakeEstateOrder::default()).await
}

async fn selling_as(fake: FakeEstateOrder) -> (axum::Router, Arc<Mutex<Vec<String>>>) {
    let (router, calls, _) = selling_watched(fake).await;
    (router, calls)
}

/// [`selling_as`], with the console's [`crate::auth::Webauthn`] kept to look at.
async fn selling_watched(fake: FakeEstateOrder) -> (axum::Router, Arc<Mutex<Vec<String>>>, Arc<crate::auth::Webauthn>) {
    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(EstateOrderServer::new(fake))
            .serve_with_incoming(tokio_stream::wrappers::TcpListenerStream::new(listener))
            .await
    });
    let (router, webauthn, _) = super::console_with(FakeGunnar::new(), None, Some(format!("http://{addr}"))).await;
    (router, calls, webauthn)
}

async fn post_json(app: &axum::Router, uri: &str, body: &str) -> (http::StatusCode, serde_json::Value) {
    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, serde_json::from_slice(&body).unwrap_or(serde_json::Value::Null))
}

/// ★ **The old buy page's address is the ONE sign-up page now** (owner
/// 2026-10-07), its query kept: the vendor's return (`?company=`) and the
/// renewal mail's link (`?renew=`) still land, on the private choice.
#[tokio::test]
async fn the_old_private_address_redirects_to_the_sign_up_page_with_its_query() {
    let (app, calls) = selling().await;
    for (asked, lands) in [
        ("/private", "/signup?plan=private"),
        ("/private?company=acme", "/signup?plan=private&company=acme"),
        ("/private?renew=acme", "/signup?plan=private&renew=acme"),
    ] {
        use tower::ServiceExt;
        let response = app.clone().oneshot(http::Request::builder().uri(asked).body(axum::body::Body::empty()).unwrap()).await.unwrap();
        assert_eq!(response.status(), http::StatusCode::PERMANENT_REDIRECT, "{asked}");
        assert_eq!(response.headers().get(http::header::LOCATION).unwrap(), lands, "{asked}");
    }
    assert!(calls.lock().unwrap().is_empty(), "a redirect dials nobody");
}

/// ★ **Only the fields the sign-up page reads go on, rebuilt** (stinky #23):
/// the old address pasted its raw query after `plan=private`, so a `next=`,
/// a second `plan=` or a `company=` that is no company's rode along to the
/// page. Now `company` and `renew` go on when they are a company's name (the
/// first of each, as the page's `URLSearchParams.get` reads it), and nothing
/// else does.
#[tokio::test]
async fn the_old_private_address_carries_only_the_fields_the_sign_up_page_reads() {
    let (app, _) = selling().await;
    for (asked, lands) in [
        ("/private?company=acme&next=https://elsewhere.example", "/signup?plan=private&company=acme"),
        ("/private?plan=10gb&company=acme", "/signup?plan=private&company=acme"),
        ("/private?company=acme&company=other", "/signup?plan=private&company=acme"),
        ("/private?company=Acme%2F..", "/signup?plan=private"),
        ("/private?company=a%26renew%3Dother", "/signup?plan=private"),
        ("/private?renew=acme&instance=x&sso=passkey", "/signup?plan=private&renew=acme"),
        ("/private?company=acme&renew=acme", "/signup?plan=private&company=acme&renew=acme"),
    ] {
        use tower::ServiceExt;
        let response = app.clone().oneshot(http::Request::builder().uri(asked).body(axum::body::Body::empty()).unwrap()).await.unwrap();
        assert_eq!(response.headers().get(http::header::LOCATION).unwrap(), lands, "{asked}");
    }
}

/// ★ **A17: back from Stripe, a private Gunnar's buyer follows THEIR order.**
/// The vendor returns to `/paid/estate/<co>` (the reference `estate/<co>`);
/// that address was the room pane's, which asked the room ledger and showed
/// NOT_FOUND. It is the sign-up page following the order now (`?company=`,
/// `EstateOrder.Status`), and a name that is no company's dials nobody.
#[tokio::test]
async fn back_from_stripe_the_buyer_follows_their_estate_order() {
    let (app, calls) = selling().await;
    for (asked, lands) in [("/paid/estate/acme", "/signup?plan=private&company=acme"), ("/paid/estate/Acme%2F..", "/signup?plan=private")] {
        use tower::ServiceExt;
        let response = app.clone().oneshot(http::Request::builder().uri(asked).body(axum::body::Body::empty()).unwrap()).await.unwrap();
        assert!(response.status().is_redirection(), "{asked}: {}", response.status());
        assert_eq!(response.headers().get(http::header::LOCATION).unwrap(), lands, "{asked}");
    }
    assert!(calls.lock().unwrap().is_empty(), "a redirect dials nobody");
    assert_eq!(monetize_site_buy::urls::paid_estate_to(&crate::auth::private::pages(), "acme"), "/signup?plan=private&company=acme");
}

/// ★ live 2026-10-14: **back from Stripe after a grow** (`estate/<co>/change-<n>`) the
/// buyer lands on the same watch, told it is a change — the page then says the machines
/// restart for a few minutes; anything else goes to the page alone.
#[tokio::test]
async fn back_from_stripe_after_a_grow_the_buyer_is_told_the_machines_restart() {
    let (app, calls) = selling().await;
    for (asked, lands) in [
        ("/paid/estate/acme/change-1", "/signup?plan=private&company=acme&change=1"),
        ("/paid/estate/acme/change-x", "/signup?plan=private"),
        ("/paid/estate/Acme%2F../change-1", "/signup?plan=private"),
    ] {
        use tower::ServiceExt;
        let response = app.clone().oneshot(http::Request::builder().uri(asked).body(axum::body::Body::empty()).unwrap()).await.unwrap();
        assert!(response.status().is_redirection(), "{asked}: {}", response.status());
        assert_eq!(response.headers().get(http::header::LOCATION).unwrap(), lands, "{asked}");
    }
    assert!(calls.lock().unwrap().is_empty(), "a redirect dials nobody");
    let page = crate::auth::signup::signup_html();
    assert!(page.contains(crate::auth::private::CHANGE_RESTARTS), "the page says it in these words");
}

/// ★ **The ladder is monetize's answer, as is**: every step's words, its
/// sizes and machines, the refusal of a step it does not sell — for a NEW
/// estate. An INSTANCE's ladder (`?company=`) is its account's (one shop 4,
/// 2026-10-07): nobody signed in is asked to sign in first, and monetize is
/// not dialled.
#[tokio::test]
async fn the_ladder_is_monetizes_answer_as_is() {
    let (app, calls) = selling().await;
    let (status, body) = send(&app, http::Method::GET, "/auth/private/ladder?company=acme", None).await;
    assert_eq!(status, http::StatusCode::UNAUTHORIZED, "{body}");
    assert!(body.contains("Sign in, or Create your account at console.example first"), "{body}");
    assert!(crate::auth::signup::tests::asks(&calls).is_empty(), "nothing dialled for nobody (beside the kept ladder's own read): {:?}", calls.lock().unwrap());
    let (status, body) = send(&app, http::Method::GET, "/auth/private/ladder", None).await;
    assert_eq!(status, http::StatusCode::OK, "{body}");
    let j: serde_json::Value = serde_json::from_str(&body).unwrap();
    assert!(calls.lock().unwrap().iter().all(|c| c == "ladder \"\""), "a new estate's ladder names no company: {:?}", calls.lock().unwrap());
    assert!(!calls.lock().unwrap().is_empty(), "the ladder is monetize's: it was read");
    let steps = j["steps"].as_array().unwrap();
    assert_eq!(steps.len(), 2);
    assert_eq!(steps[0]["name"], "Mini");
    assert_eq!(steps[0]["resources_words"], "7 cores · 16 GB RAM · 40 GB data");
    assert_eq!(steps[0]["monthly_words"], "108.00 EUR a month");
    assert_eq!(steps[0]["total_words"], "6 months prepaid: 648.00 EUR");
    assert_eq!(steps[0]["pay_words"], "648.00 EUR", "the button's amount, as monetize words it (owner 2026-10-22)");
    assert_eq!(steps[0]["sizes"]["main_data_gib"], 10);
    assert_eq!(steps[0]["machines"][0]["cores"], 4);
    assert_eq!(steps[1]["available"], false);
    assert!(steps[1]["reason"].as_str().unwrap().contains("20xCPU-128GB"), "monetize's sentence, whole");
    assert_eq!((j["start"].as_u64(), j["floor"].as_u64(), j["apex"].as_str()), (Some(0), Some(0), Some("gunnar.rs")));
}

/// ★ **A visitor pays ONCE** (owner 2026-10-22, `monetize/DESIGN-PRIVATE-PAY-ONCE.md`): with no
/// account here, the private order goes straight to its payment — the account name and the
/// e-mail typed once are the NEW account's, which monetize makes when the order is PAID. The
/// console sends what was typed, no ticket, `new_account`, and its own welcome page (back to this
/// order after the passkey); monetize judges the rest. No name: refused, nothing dialled.
#[tokio::test]
async fn a_visitor_pays_once_and_the_order_names_the_new_account() {
    let (app, calls) = selling().await;
    let (status, j) = post_json(&app, "/auth/private/start", r#"{"company":"bolt","email":"c@example.com","account":"carol"}"#).await;
    assert_eq!(status, http::StatusCode::OK, "{j}");
    assert!(j["url"].as_str().is_some_and(|u| u.starts_with("https://")), "straight to the payment: {j}");
    assert_eq!(
        calls.lock().unwrap().iter().filter(|c| c.starts_with("start")).cloned().collect::<Vec<_>>(),
        ["start bolt c@example.com  owner=carol new=true welcome=https://console.example/welcome?session={session}&next=%2Fsignup%3Fplan%3Dprivate%26company%3Dbolt ticket=0"],
    );
    calls.lock().unwrap().clear();
    for body in [r#"{"company":"colt","email":"d@example.com"}"#, r#"{"company":"colt","account":"dora"}"#] {
        let (status, j) = post_json(&app, "/auth/private/start", body).await;
        assert_eq!(status, http::StatusCode::BAD_REQUEST, "{body}: {j}");
    }
    assert!(calls.lock().unwrap().iter().all(|c| !c.starts_with("start")), "nothing was ordered: {:?}", calls.lock().unwrap());
}

/// **Status shows the records a customer's own domain needs.**
#[tokio::test]
async fn status_shows_the_records_a_customer_domain_needs() {
    let (app, _) = selling().await;
    let (status, body) = send(&app, http::Method::GET, "/auth/private/status?company=acme", None).await;
    assert_eq!(status, http::StatusCode::OK, "{body}");
    let j: serde_json::Value = serde_json::from_str(&body).unwrap();
    assert_eq!(j["state"], "waiting_for_dns");
    assert_eq!(j["records"].as_array().unwrap().len(), 2);
    let (status, _) = send(&app, http::Method::GET, "/auth/private/status?company=Not%20A%20Name!", None).await;
    assert_eq!(status, http::StatusCode::BAD_REQUEST, "a probe that cannot be a name dials nobody");
}

/// **Where nothing is sold, nothing of it exists.**
#[tokio::test]
async fn a_console_that_sells_nothing_has_no_private_page() {
    let (router, _, _) = super::console(FakeGunnar::new(), false).await;
    let (status, body) = send(&router, http::Method::GET, "/private", None).await;
    assert_ne!(status, http::StatusCode::OK, "{body}");
    let (status, _) = post_json(&router, "/auth/private/start", "{}").await;
    assert_ne!(status, http::StatusCode::OK);
}

/// **A60: the old quote door is gone** — the page prices from the ladder, and
/// nothing serves `POST /auth/private/quote` any more (a dead route read as wired).
#[tokio::test]
async fn the_old_quote_door_is_gone() {
    let (app, calls) = selling().await;
    let (status, _) = post_json(&app, "/auth/private/quote", r#"{"company":"acme"}"#).await;
    assert!(matches!(status, http::StatusCode::NOT_FOUND | http::StatusCode::METHOD_NOT_ALLOWED | http::StatusCode::UNAUTHORIZED), "{status}");
    assert!(calls.lock().unwrap().iter().all(|c| !c.starts_with("start")), "nothing was ordered: {:?}", calls.lock().unwrap());
}

/// ★ **A console whose monetize sells no private estates never shows a
/// configuration sentence** (test2, owner 2026-10-08: "this monetize sells no
/// private estates ([estate_orders] is absent)" under "Private Gunnar", and
/// no − / +). Every private route answers in the console's own words — a
/// private Gunnar is not sold here — and orders nothing.
#[tokio::test]
async fn where_no_private_gunnar_is_sold_the_routes_say_so_in_words_not_settings() {
    let (app, calls) = selling_as(FakeEstateOrder { unsold: true, ..FakeEstateOrder::default() }).await;
    let (status, body) = send(&app, http::Method::GET, "/auth/private/ladder", None).await;
    assert_eq!(status, http::StatusCode::NOT_FOUND, "{body}");
    assert!(body.contains("A private Gunnar is not sold here."), "{body}");
    for (method, path, payload) in [("POST", "/auth/private/start", r#"{"company":"acme","email":"a@example.com"}"#)] {
        let (status, j) = post_json(&app, path, payload).await;
        assert_eq!(status, http::StatusCode::NOT_FOUND, "{method} {path}: {j}");
        assert_eq!(j["error"], "A private Gunnar is not sold here.", "{method} {path}");
    }
    let (_, ladder) = send(&app, http::Method::GET, "/auth/private/ladder", None).await;
    for config in ["estate_orders", "monetize", "absent"] {
        assert!(!ladder.contains(config), "a configuration word reached the visitor: {config} in {ladder}");
    }
    assert!(calls.lock().unwrap().iter().all(|c| !c.starts_with("start")), "nothing was ordered: {:?}", calls.lock().unwrap());
}

/// ★ **A private order's fields are judged before they cost anything**
/// (stinky 2026-10-10 #1): `start` keyed the rate limiter by whatever company
/// the body named — two megabytes of it, four thousand times over. A name
/// that cannot be a company's, and any field longer than its rule, is a 400
/// before the limiter holds a byte of it and before monetize is dialled.
#[tokio::test]
async fn a_private_start_judges_its_fields_before_the_limiter_holds_them() {
    let (app, calls, webauthn) = selling_watched(FakeEstateOrder::default()).await;
    let long = "a".repeat(100_000);
    let bodies = [
        serde_json::json!({ "company": long }),
        serde_json::json!({ "company": "Not A Name!" }),
        serde_json::json!({ "company": "acme", "email": format!("{long}@example.com") }),
        serde_json::json!({ "company": "acme", "email": "a@example.com", "domain": format!("{long}.example") }),
        serde_json::json!({ "company": "acme", "email": "a@example.com", "front_plan": long }),
        serde_json::json!({ "company": "acme", "email": "a@example.com", "main_plan": long }),
    ];
    for body in bodies {
        let (status, j) = post_json(&app, "/auth/private/start", &body.to_string()).await;
        assert_eq!(status, http::StatusCode::BAD_REQUEST, "{j}");
    }
    assert!(webauthn.private_limit_key_bytes() < 1024, "the limiter holds {} bytes of keys", webauthn.private_limit_key_bytes());
    assert!(calls.lock().unwrap().is_empty(), "nothing dialled: {:?}", calls.lock().unwrap());
}

/// A request as a signed-in browser sends it: the session cookie, and the
/// console's own Origin on a POST.
async fn as_signed_in(app: &axum::Router, cookie: &str, method: http::Method, uri: &str, body: Option<&str>) -> (http::StatusCode, String) {
    use tower::ServiceExt;
    let mut request = http::Request::builder()
        .method(method)
        .uri(uri)
        .header(http::header::COOKIE, format!("{}={cookie}", crate::auth::SESSION_COOKIE))
        .header(http::header::ORIGIN, "https://console.example");
    if body.is_some() {
        request = request.header(http::header::CONTENT_TYPE, "application/json");
    }
    let request = request.body(axum::body::Body::from(body.unwrap_or_default().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())
}

/// ★ **A gunnar that did not answer is a 503, never "sign in again" and never
/// the anonymous view** (stinky 2026-10-10 #15). `account_ticket` folded
/// "this console could not sign as you" into a 403 that told a signed-in
/// buyer to sign in again, and `status` swallowed every ticket failure and
/// asked monetize as nobody. `ghost` is bound to a principal gunnar refuses
/// to enrol a key on: the console cannot sign as them.
#[tokio::test]
async fn a_gunnar_that_cannot_vouch_is_an_outage_not_a_sign_in_and_not_a_stranger() {
    let (app, calls, webauthn) = selling_watched(FakeEstateOrder::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 (status, body) = as_signed_in(&app, &cookie, http::Method::POST, "/auth/private/start", Some(r#"{"company":"acme","email":"a@example.com"}"#)).await;
    assert_eq!(status, http::StatusCode::SERVICE_UNAVAILABLE, "{body}");
    assert!(!body.contains("sign in again"), "{body}");

    let (status, body) = as_signed_in(&app, &cookie, http::Method::GET, "/auth/private/status?company=acme", None).await;
    assert_eq!(status, http::StatusCode::SERVICE_UNAVAILABLE, "{body}");
    assert!(calls.lock().unwrap().iter().all(|c| !c.starts_with("start") && !c.starts_with("status")), "monetize was asked as nobody: {:?}", calls.lock().unwrap());
}

/// One request as a signed-in person (`role`, a fresh user-verified session).
async fn as_person(app: &axum::Router, keys: &crate::auth::Webauthn, user: &str, role: crate::role::Role, method: http::Method, uri: &str, body: &str) -> (http::StatusCode, serde_json::Value) {
    use tower::ServiceExt;
    let cookie = keys.make_session(user, role, true);
    let request = http::Request::builder()
        .method(method)
        .uri(uri)
        .header(http::header::COOKIE, format!("{}={cookie}", crate::auth::SESSION_COOKIE))
        .header(http::header::CONTENT_TYPE, "application/json")
        .header("origin", "https://console.example")
        .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, serde_json::from_slice(&body).unwrap_or(serde_json::Value::Null))
}

/// ★ **[Rebuild main]** (main rebuild, owner 2026-10-12): the console is a thin layer over
/// monetize's guardian — an admin reads what it finds about the main (monetize's words, word
/// for word) and, typing the confirmation, presses its remedy in their own stepped-up name;
/// a viewer or an operator is refused before monetize is asked; a wrong phrase asks nothing.
#[tokio::test]
async fn an_admin_looks_at_and_presses_the_mains_rebuild_through_monetize() {
    let (app, calls, keys) = selling_watched(FakeEstateOrder::default()).await;
    let get = http::Method::GET;
    let post = http::Method::POST;
    let (status, _) = as_person(&app, &keys, "viewer-vera", crate::role::Role::Viewer, get.clone(), "/api/rebuild", "").await;
    assert_eq!(status, http::StatusCode::FORBIDDEN);
    let (status, _) = as_person(&app, &keys, "operator-olof", crate::role::Role::Operator, post.clone(), "/api/rebuild", r#"{"confirm":"rebuild main"}"#).await;
    assert_eq!(status, http::StatusCode::FORBIDDEN, "an operator does not rebuild the main");
    assert!(calls.lock().unwrap().is_empty(), "nothing was asked of monetize: {:?}", calls.lock().unwrap());

    let (status, look) = as_person(&app, &keys, "alice", crate::role::Role::Admin, get, "/api/rebuild", "").await;
    assert_eq!(status, http::StatusCode::OK, "{look}");
    assert_eq!((look["finding"].as_str(), look["remedy"].as_str(), look["confirm"].as_str()), (Some("main_lost"), Some("new_server"), Some(crate::api::rebuild::CONFIRM)));
    assert!(look["words"].as_str().is_some_and(|w| w.contains("never promoted")), "monetize's words: {look}");

    let (status, _) = as_person(&app, &keys, "alice", crate::role::Role::Admin, post.clone(), "/api/rebuild", r#"{"confirm":"yes"}"#).await;
    assert_eq!(status, http::StatusCode::BAD_REQUEST, "a wrong phrase");
    let (status, pressed) = as_person(&app, &keys, "alice", crate::role::Role::Admin, post, "/api/rebuild", r#"{"confirm":"rebuild main"}"#).await;
    assert_eq!(status, http::StatusCode::OK, "{pressed}");
    assert_eq!((pressed["case_id"].as_str(), pressed["case_state"].as_str(), pressed["confirm"].as_str()), (Some("acme-1"), Some("approved"), Some("")), "{pressed}");
    assert_eq!(crate::auth::signup::tests::asks(&calls), vec!["rebuild_main press=false principal= strong=false".to_string(), "rebuild_main press=true principal=alice strong=true".to_string()]);
}