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

nordisk/monetize

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

//! ★ **Managing what was bought is shared too** (A16, 2026-10-23): Gunnar's console's doors
//! for an instance (`auth::private` renew, cancel, terminate, restore-front) and for the
//! account's own plan (`api::subscription` offer, renew, history), moved as they are into the
//! relay. Each asks monetize with the product's own ticket, never an e-mail; ending an
//! instance needs a fresh login and its name typed back. Addresses are `*.mock.invalid`.

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

use axum::http::{HeaderMap, StatusCode};
use monetize_proto as pb;
use monetize_site_buy::{Account, BuySite, Buyer, Door, Refusal, Relay, ShopLink, Who};
use serde_json::json;
use tonic::{Request, Response, Status};

/// The tickets the shop heard, each with the door it came through.
type Heard = Arc<Mutex<Vec<(String, Vec<u8>)>>>;

#[derive(Clone, Default)]
struct Shop {
    tickets: Heard,
    terminate: Arc<Mutex<Option<pb::EstateOrderTerminateRequest>>>,
    quote: Arc<Mutex<Option<pb::QuoteRequest>>>,
    renew: Arc<Mutex<Option<pb::StartPurchaseRequest>>>,
}

impl Shop {
    fn heard(&self, door: &str, ticket: &[u8]) {
        self.tickets
            .lock()
            .unwrap()
            .push((door.to_owned(), ticket.to_vec()));
    }
}

#[tonic::async_trait]
impl pb::estate_order_server::EstateOrder for Shop {
    async fn product_order(
        &self,
        _: tonic::Request<pb::EstateProductOrderRequest>,
    ) -> Result<tonic::Response<pb::EstateProductOrderResponse>, tonic::Status> {
        Err(tonic::Status::unimplemented("product_order"))
    }
    async fn product_order_answer(
        &self,
        _: tonic::Request<pb::EstateProductOrderAnswerRequest>,
    ) -> Result<tonic::Response<pb::EstateProductOrderAnswerResponse>, tonic::Status> {
        Err(tonic::Status::unimplemented("product_order_answer"))
    }
    async fn product_order_kinds(
        &self,
        _: tonic::Request<pb::EstateProductOrderKindsRequest>,
    ) -> Result<tonic::Response<pb::EstateProductOrderKindsResponse>, tonic::Status> {
        Err(tonic::Status::unimplemented("product_order_kinds"))
    }
    async fn ladder(
        &self,
        _: Request<pb::EstateOrderLadderRequest>,
    ) -> Result<Response<pb::EstateOrderLadderResponse>, Status> {
        Err(Status::unimplemented("ladder"))
    }
    async fn start(
        &self,
        _: Request<pb::EstateOrderStartRequest>,
    ) -> Result<Response<pb::EstateOrderStartResponse>, Status> {
        Err(Status::unimplemented("start"))
    }
    async fn status(
        &self,
        _: Request<pb::EstateOrderStatusRequest>,
    ) -> Result<Response<pb::EstateOrderStatusResponse>, Status> {
        Err(Status::unimplemented("status"))
    }
    async fn change_plan(
        &self,
        _: Request<pb::EstateOrderChangePlanRequest>,
    ) -> Result<Response<pb::EstateOrderChangePlanResponse>, Status> {
        Err(Status::unimplemented("change_plan"))
    }
    async fn send_sign_in_again(
        &self,
        _: Request<pb::EstateOrderSignInAgainRequest>,
    ) -> Result<Response<pb::EstateOrderSignInAgainResponse>, Status> {
        Err(Status::unimplemented("send_sign_in_again"))
    }
    async fn purchases(
        &self,
        _: Request<pb::EstateOrderPurchasesRequest>,
    ) -> Result<Response<pb::EstateOrderPurchasesResponse>, Status> {
        Err(Status::unimplemented("purchases"))
    }
    async fn sells(
        &self,
        _: Request<pb::EstateOrderSellsRequest>,
    ) -> Result<Response<pb::EstateOrderSellsResponse>, Status> {
        Err(Status::unimplemented("sells"))
    }
    async fn notice(
        &self,
        _: Request<pb::EstateNoticeRequest>,
    ) -> Result<Response<pb::EstateNoticeResponse>, Status> {
        Err(Status::unimplemented("notice"))
    }
    async fn settle(
        &self,
        _: Request<pb::EstateOrderSettleRequest>,
    ) -> Result<Response<pb::EstateOrderSettleResponse>, Status> {
        Err(Status::unimplemented("settle"))
    }
    async fn first_link(
        &self,
        _: Request<pb::EstateOrderFirstLinkRequest>,
    ) -> Result<Response<pb::EstateOrderFirstLinkResponse>, Status> {
        Err(Status::unimplemented("first_link"))
    }
    async fn refeed(
        &self,
        _: Request<pb::EstateOrderRefeedRequest>,
    ) -> Result<Response<pb::EstateOrderRefeedResponse>, Status> {
        Err(Status::unimplemented("refeed"))
    }
    async fn rebuild_main(
        &self,
        _: Request<pb::EstateRebuildMainRequest>,
    ) -> Result<Response<pb::EstateRebuildMainResponse>, Status> {
        Err(Status::unimplemented("rebuild_main"))
    }
    async fn order_site(
        &self,
        _: Request<pb::EstateOrderSiteRequest>,
    ) -> Result<Response<pb::EstateOrderSiteResponse>, Status> {
        Err(Status::unimplemented("order_site"))
    }
    async fn list_sites(
        &self,
        _: Request<pb::EstateSitesRequest>,
    ) -> Result<Response<pb::EstateSitesResponse>, Status> {
        Err(Status::unimplemented("list_sites"))
    }
    async fn update_estate(
        &self,
        _: Request<pb::EstateUpdateRequest>,
    ) -> Result<Response<pb::EstateUpdateResponse>, Status> {
        Err(Status::unimplemented("update_estate"))
    }
    async fn update_status(
        &self,
        _: Request<pb::EstateUpdateStatusRequest>,
    ) -> Result<Response<pb::EstateUpdateResponse>, Status> {
        Err(Status::unimplemented("update_status"))
    }
    async fn cancel_update(
        &self,
        _: Request<pb::EstateUpdateStatusRequest>,
    ) -> Result<Response<pb::EstateUpdateResponse>, Status> {
        Err(Status::unimplemented("cancel_update"))
    }
    async fn set_live_update(
        &self,
        _: Request<pb::EstateSetLiveUpdateRequest>,
    ) -> Result<Response<pb::EstateUpdateResponse>, Status> {
        Err(Status::unimplemented("set_live_update"))
    }
    async fn adopt_estate(
        &self,
        _: Request<pb::EstateAdoptRequest>,
    ) -> Result<Response<pb::EstateAdoptResponse>, Status> {
        Err(Status::unimplemented("adopt_estate"))
    }
    async fn list_code_red(
        &self,
        _: Request<pb::CodeRedListRequest>,
    ) -> Result<Response<pb::CodeRedList>, Status> {
        Err(Status::unimplemented("list_code_red"))
    }
    async fn ack_code_red(
        &self,
        _: Request<pb::CodeRedAckRequest>,
    ) -> Result<Response<pb::CodeRedList>, Status> {
        Err(Status::unimplemented("ack_code_red"))
    }
    async fn raise_code_red(
        &self,
        _: Request<pb::CodeRed>,
    ) -> Result<Response<pb::CodeRedList>, Status> {
        Err(Status::unimplemented("raise_code_red"))
    }
    async fn clear_code_red(
        &self,
        _: Request<pb::CodeRedClearRequest>,
    ) -> Result<Response<pb::CodeRedList>, Status> {
        Err(Status::unimplemented("clear_code_red"))
    }
    async fn narrate_log(
        &self,
        _: Request<pb::EstateNarrateLogRequest>,
    ) -> Result<Response<pb::EstateNarrateLogResponse>, Status> {
        Err(Status::unimplemented("narrate_log"))
    }
    async fn install_log(
        &self,
        _: Request<pb::EstateInstallLogRequest>,
    ) -> Result<Response<pb::EstateInstallLogResponse>, Status> {
        Err(Status::unimplemented("install_log"))
    }
    async fn renew(
        &self,
        r: Request<pb::EstateOrderRenewRequest>,
    ) -> Result<Response<pb::EstateOrderRenewResponse>, Status> {
        let r = r.into_inner();
        self.heard("renew", &r.actor_ticket);
        Ok(Response::new(pb::EstateOrderRenewResponse {
            reference: format!("estate/{}/renew-1", r.company),
            checkout_url: "https://checkout.mock.invalid/c/pay/cs_test_renew".into(),
            price_words: "385.20 EUR".into(),
            paid_until: "2027-04-23".into(),
            words: "Renew acme for 6 months.".into(),
        }))
    }
    async fn cancel(
        &self,
        r: Request<pb::EstateOrderCancelRequest>,
    ) -> Result<Response<pb::EstateOrderCancelResponse>, Status> {
        let r = r.into_inner();
        self.heard("cancel", &r.actor_ticket);
        Ok(Response::new(pb::EstateOrderCancelResponse {
            words: "acme is not renewed.".into(),
            paid_until: "2027-04-23".into(),
            delete_on: "2027-06-22".into(),
        }))
    }
    async fn terminate(
        &self,
        r: Request<pb::EstateOrderTerminateRequest>,
    ) -> Result<Response<pb::EstateOrderTerminateResponse>, Status> {
        let r = r.into_inner();
        self.heard("terminate", &r.actor_ticket);
        *self.terminate.lock().unwrap() = Some(r);
        Ok(Response::new(pb::EstateOrderTerminateResponse {
            words: "acme is ended.".into(),
            done: true,
        }))
    }
    async fn restore_front(
        &self,
        r: Request<pb::EstateOrderRestoreFrontRequest>,
    ) -> Result<Response<pb::EstateOrderRestoreFrontResponse>, Status> {
        let r = r.into_inner();
        self.heard("restore-front", &r.actor_ticket);
        Ok(Response::new(pb::EstateOrderRestoreFrontResponse {
            words: "The front is back.".into(),
            moved: false,
            public_ipv4: "192.0.2.7".into(),
        }))
    }
}

#[tonic::async_trait]
impl pb::signup_server::Signup for Shop {
    async fn status(
        &self,
        _: Request<pb::SignupStatusRequest>,
    ) -> Result<Response<pb::SignupStatusResponse>, Status> {
        Err(Status::unimplemented("status"))
    }
    async fn check_name(
        &self,
        _: Request<pb::CheckNameRequest>,
    ) -> Result<Response<pb::CheckNameResponse>, Status> {
        Err(Status::unimplemented("check_name"))
    }
    async fn start(
        &self,
        _: Request<pb::StartSignupRequest>,
    ) -> Result<Response<pb::StartSignupResponse>, Status> {
        Err(Status::unimplemented("start"))
    }
    async fn rename(
        &self,
        _: Request<pb::RenameSignupRequest>,
    ) -> Result<Response<pb::RenameSignupResponse>, Status> {
        Err(Status::unimplemented("rename"))
    }
    async fn send_sign_in_again(
        &self,
        _: Request<pb::SignupSignInAgainRequest>,
    ) -> Result<Response<pb::SignupSignInAgainResponse>, Status> {
        Err(Status::unimplemented("send_sign_in_again"))
    }
    async fn plans(
        &self,
        _: Request<pb::SignupPlansRequest>,
    ) -> Result<Response<pb::SignupPlansResponse>, Status> {
        Ok(Response::new(pb::SignupPlansResponse {
            plans: vec![pb::SignupPlanOffer {
                id: "holger/5gb-1m".into(),
                name: "5 GB".into(),
                cap_bytes: 5 << 30,
                period_days: 30,
                ..Default::default()
            }],
            ..Default::default()
        }))
    }
}

/// The shop verifies this product's tenant renewal tickets (Gunnar's `offered`).
#[tonic::async_trait]
impl pb::products_server::Products for Shop {
    async fn list(
        &self,
        _: Request<pb::ListProductsRequest>,
    ) -> Result<Response<pb::ListProductsResponse>, Status> {
        Ok(Response::new(pb::ListProductsResponse {
            products: vec![pb::ProductInfo {
                id: "holger".into(),
                tenant_renewal: true,
                ..Default::default()
            }],
            ..Default::default()
        }))
    }
}

#[tonic::async_trait]
impl pb::purchase_server::Purchase for Shop {
    async fn bundles(
        &self,
        _: Request<pb::ListBundlesRequest>,
    ) -> Result<Response<pb::ListBundlesResponse>, Status> {
        Err(Status::unimplemented("bundles"))
    }
    async fn status(
        &self,
        _: Request<pb::PurchaseStatusRequest>,
    ) -> Result<Response<pb::PurchaseStatusResponse>, Status> {
        Err(Status::unimplemented("status"))
    }
    async fn abandon(
        &self,
        _: Request<pb::AbandonPurchaseRequest>,
    ) -> Result<Response<pb::AbandonPurchaseResponse>, Status> {
        Err(Status::unimplemented("abandon"))
    }
    async fn cancel(
        &self,
        _: Request<pb::CancelPurchaseRequest>,
    ) -> Result<Response<pb::CancelPurchaseResponse>, Status> {
        Err(Status::unimplemented("cancel"))
    }
    async fn quote(
        &self,
        r: Request<pb::QuoteRequest>,
    ) -> Result<Response<pb::QuoteResponse>, Status> {
        let r = r.into_inner();
        self.heard("quote", &r.actor_ticket);
        let tenant = r.tenant.clone();
        *self.quote.lock().unwrap() = Some(r);
        if tenant == "new-repo" {
            return Err(Status::failed_precondition(
                "nothing to renew for new: this server holds no entitlement for them",
            ));
        }
        Ok(Response::new(pb::QuoteResponse {
            reference: "holger/x14/renew".into(),
            can_serve: true,
            period: "month".into(),
            price_words: "100.00 SEK".into(),
            caps: [("disk_bytes".to_string(), 5u64)].into_iter().collect(),
            ..Default::default()
        }))
    }
    async fn start(
        &self,
        r: Request<pb::StartPurchaseRequest>,
    ) -> Result<Response<pb::StartPurchaseResponse>, Status> {
        let r = r.into_inner();
        self.heard("start", &r.actor_ticket);
        *self.renew.lock().unwrap() = Some(r);
        Ok(Response::new(pb::StartPurchaseResponse {
            reference: "holger/x14/renew-1".into(),
            state: pb::PurchaseState::Pending as i32,
            redirect_url: "https://checkout.mock.invalid/c/pay/cs_test_prolong".into(),
            ..Default::default()
        }))
    }
    async fn history(
        &self,
        r: Request<pb::PurchaseHistoryRequest>,
    ) -> Result<Response<pb::PurchaseHistoryResponse>, Status> {
        let r = r.into_inner();
        self.heard("history", &r.actor_ticket);
        Ok(Response::new(pb::PurchaseHistoryResponse {
            attempts: vec![pb::PurchaseAttempt {
                reference: "holger/x14/p-1".into(),
                outcome: "granted".into(),
                at_unix_ms: 7,
                steps: 6,
                price_words: "100.00 SEK".into(),
                ..Default::default()
            }],
        }))
    }
}

/// `x-test-user`: absent a visitor; `fresh:<a>` signed in with a fresh login; else the account
/// (logged in a while ago). x14's tenant is its repository; `new` holds nothing.
struct Site;

impl BuySite for Site {
    fn origin(&self) -> String {
        "https://holger.mock.invalid".into()
    }
    fn product_word(&self) -> String {
        "Holger".into()
    }
    async fn who(&self, headers: &HeaderMap) -> Result<Who, Refusal> {
        Ok(
            match headers.get("x-test-user").and_then(|v| v.to_str().ok()) {
                None => Who::Visitor,
                Some(a) => {
                    let a = a.trim_start_matches("fresh:");
                    Who::Person(Buyer {
                        account: a.into(),
                        tenant: format!("{a}-repo"),
                        ..Default::default()
                    })
                }
            },
        )
    }
    async fn stepped_up(&self, headers: &HeaderMap) -> Result<(), String> {
        match headers.get("x-test-user").and_then(|v| v.to_str().ok()) {
            Some(u) if u.starts_with("fresh:") => Ok(()),
            _ => Err("Sign in again with your passkey first.".into()),
        }
    }
    async fn ticket(
        &self,
        _: &HeaderMap,
        buyer: &Buyer,
        purpose: &str,
        _: &BTreeMap<String, u64>,
    ) -> Result<Vec<u8>, Refusal> {
        Ok(format!("{purpose}:{}", buyer.account).into_bytes())
    }
    async fn held_caps(
        &self,
        _: &HeaderMap,
        buyer: &Buyer,
    ) -> Result<BTreeMap<String, u64>, Refusal> {
        Ok(if buyer.account == "new" {
            BTreeMap::new()
        } else {
            BTreeMap::from([("disk_bytes".to_string(), 5u64)])
        })
    }
    async fn signup_account(&self, _: &str) -> Result<Account, Refusal> {
        Ok(Account::NoPasskey)
    }
}

async fn relay() -> (Arc<Relay<Site>>, Shop) {
    let shop = Shop::default();
    let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
    let at = listener.local_addr().unwrap();
    tokio::spawn(
        tonic::transport::Server::builder()
            .add_service(pb::estate_order_server::EstateOrderServer::new(
                shop.clone(),
            ))
            .add_service(pb::purchase_server::PurchaseServer::new(shop.clone()))
            .add_service(pb::products_server::ProductsServer::new(shop.clone()))
            .add_service(pb::signup_server::SignupServer::new(shop.clone()))
            .serve_with_incoming(tokio_stream::wrappers::TcpListenerStream::new(listener)),
    );
    let link = ShopLink::connect_lazy(
        &Door {
            url: format!("http://{at}"),
            ..Door::default()
        },
        "site-bearer-for-tests",
        "holger",
    )
    .unwrap();
    (Arc::new(Relay::new(Site, link)), shop)
}

async fn call(
    r: &Arc<Relay<Site>>,
    method: &str,
    uri: &str,
    user: Option<&str>,
    body: Option<serde_json::Value>,
) -> (StatusCode, serde_json::Value) {
    use tower::ServiceExt;
    let mut req = axum::http::Request::builder().method(method).uri(uri);
    if let Some(u) = user {
        req = req.header("x-test-user", u);
    }
    let req = match body {
        Some(b) => req
            .header("content-type", "application/json")
            .body(axum::body::Body::from(b.to_string()))
            .unwrap(),
        None => req.body(axum::body::Body::empty()).unwrap(),
    };
    let res = monetize_site_buy::router(r.clone())
        .oneshot(req)
        .await
        .unwrap();
    let status = res.status();
    let bytes = axum::body::to_bytes(res.into_body(), 1 << 20)
        .await
        .unwrap();
    (
        status,
        serde_json::from_slice(&bytes).unwrap_or(serde_json::Value::Null),
    )
}

#[tokio::test]
async fn renew_stop_renewing_and_restore_front_are_the_accounts_own_with_its_estate_ticket() {
    let (r, shop) = relay().await;
    let (st, body) = call(
        &r,
        "POST",
        "/auth/private/renew",
        Some("x14"),
        Some(json!({"company": "Acme"})),
    )
    .await;
    assert_eq!(st, StatusCode::OK, "{body}");
    assert_eq!(
        body,
        json!({"url": "https://checkout.mock.invalid/c/pay/cs_test_renew", "words": "Renew acme for 6 months.", "price_words": "385.20 EUR", "paid_until": "2027-04-23"})
    );
    let (st, body) = call(
        &r,
        "POST",
        "/auth/private/cancel",
        Some("x14"),
        Some(json!({"company": "acme"})),
    )
    .await;
    assert_eq!(st, StatusCode::OK, "{body}");
    assert_eq!(
        body,
        json!({"words": "acme is not renewed.", "paid_until": "2027-04-23", "delete_on": "2027-06-22"})
    );
    let (st, body) = call(
        &r,
        "POST",
        "/auth/private/restore-front",
        Some("x14"),
        Some(json!({"company": "acme"})),
    )
    .await;
    assert_eq!(st, StatusCode::OK, "{body}");
    assert_eq!(
        body,
        json!({"words": "The front is back.", "moved": false, "public_ipv4": "192.0.2.7"})
    );
    for (door, ticket) in shop.tickets.lock().unwrap().iter() {
        assert_eq!(
            ticket, b"estate:x14",
            "{door}: the account's own estate ticket"
        );
    }
    let (st, body) = call(
        &r,
        "POST",
        "/auth/private/renew",
        None,
        Some(json!({"company": "acme"})),
    )
    .await;
    assert_eq!(st, StatusCode::UNAUTHORIZED);
    assert!(
        body["error"]
            .as_str()
            .unwrap()
            .starts_with("A private Holger is bought by your account at holger.mock.invalid"),
        "{body}"
    );
    assert_eq!(
        call(
            &r,
            "POST",
            "/auth/private/cancel",
            Some("x14"),
            Some(json!({"company": "../x"}))
        )
        .await
        .0,
        StatusCode::BAD_REQUEST
    );
}

#[tokio::test]
async fn terminate_needs_the_name_typed_back_and_a_fresh_login_and_offers_no_backup() {
    let (r, shop) = relay().await;
    let (st, body) = call(
        &r,
        "POST",
        "/auth/private/terminate",
        Some("fresh:x14"),
        Some(json!({"company": "acme", "confirm": "acm"})),
    )
    .await;
    assert_eq!(st, StatusCode::BAD_REQUEST);
    assert_eq!(
        body["error"],
        "Type the instance's name, acme, to terminate it. Nothing was ended."
    );
    let (st, body) = call(
        &r,
        "POST",
        "/auth/private/terminate",
        Some("x14"),
        Some(json!({"company": "acme", "confirm": "acme"})),
    )
    .await;
    assert_eq!(st, StatusCode::FORBIDDEN);
    assert_eq!(
        (body["code"].as_str(), body["error"].as_str()),
        (
            Some("stepup"),
            Some("Sign in again with your passkey first. Nothing was ended.")
        )
    );
    assert!(
        shop.terminate.lock().unwrap().is_none(),
        "the shop is never asked without a fresh login"
    );
    // ★ A32-27: the answer carries no backup word; an older page's `backup` is not read.
    let (st, body) = call(
        &r,
        "POST",
        "/auth/private/terminate",
        Some("fresh:x14"),
        Some(json!({"company": "acme", "confirm": "acme"})),
    )
    .await;
    assert_eq!(st, StatusCode::OK, "{body}");
    assert_eq!(body["done"], true, "{body}");
    assert!(
        body.get("backup_offered").is_none() && body.get("backups").is_none(),
        "{body}"
    );
    let (st, body) = call(
        &r,
        "POST",
        "/auth/private/terminate",
        Some("fresh:x14"),
        Some(json!({"company": "acme", "confirm": " ACME ", "backup": "skip"})),
    )
    .await;
    assert_eq!(
        (st, body["done"].as_bool()),
        (StatusCode::OK, Some(true)),
        "an older page's request still ends it: {body}"
    );
    let t = shop.terminate.lock().unwrap().clone().unwrap();
    assert_eq!(
        (
            t.company.as_str(),
            t.confirm.as_str(),
            t.actor_ticket.as_slice()
        ),
        ("acme", "ACME", b"estate:x14".as_slice())
    );
    assert!(
        monetize_site_buy::relay::typed_back("acme", "acme.holger.rs")
            && !monetize_site_buy::relay::typed_back("acme", "acmex")
    );
}

/// A site that proves no fresh login (the provided default) never ends an instance.
#[tokio::test]
async fn a_site_without_a_step_up_never_terminates() {
    assert!(
        monetize_site_buy::site::STEP_UP_WORDS.starts_with("This needs a fresh touch of your key")
    );
    assert_eq!(monetize_site_buy::site::STEP_UP_CODE, "stepup");
}

#[tokio::test]
async fn the_accounts_prolong_is_quoted_and_started_with_its_renew_ticket_and_its_history_read_with_a_read_ticket(
) {
    let (r, shop) = relay().await;
    let (st, body) = call(&r, "GET", "/api/account/subscription", Some("x14"), None).await;
    assert_eq!(st, StatusCode::OK, "{body}");
    assert_eq!(
        (
            body["can_serve"].as_bool(),
            body["price_words"].as_str(),
            body["period"].as_str(),
            body["nothing_to_renew"].as_bool()
        ),
        (Some(true), Some("100.00 SEK"), Some("month"), Some(false))
    );
    let q = shop.quote.lock().unwrap().clone().unwrap();
    assert!(
        q.renew && q.caps.is_empty() && q.bundle.is_empty() && q.tenant == "x14-repo",
        "a renewal names no caps and no bundle"
    );
    let (st, body) = call(&r, "GET", "/api/account/subscription", Some("new"), None).await;
    assert_eq!(st, StatusCode::OK, "a refusal is an answer: {body}");
    assert_eq!(
        (
            body["can_serve"].as_bool(),
            body["nothing_to_renew"].as_bool()
        ),
        (Some(false), Some(true))
    );
    let (st, body) = call(
        &r,
        "POST",
        "/api/account/subscription/renew",
        Some("x14"),
        None,
    )
    .await;
    assert_eq!(st, StatusCode::OK, "{body}");
    assert_eq!(
        (
            body["url"].as_str(),
            body["action"].as_str(),
            body["state"].as_str()
        ),
        (
            Some("https://checkout.mock.invalid/c/pay/cs_test_prolong"),
            Some("renew"),
            Some("pending")
        )
    );
    assert!(shop.renew.lock().unwrap().clone().unwrap().renew);
    let (st, body) = call(
        &r,
        "GET",
        "/api/account/subscription/history",
        Some("x14"),
        None,
    )
    .await;
    assert_eq!(st, StatusCode::OK, "{body}");
    assert_eq!(
        body,
        json!([{"reference": "holger/x14/p-1", "outcome": "granted", "at_unix_ms": 7, "date_words": "1970-01-01", "caps": {}, "delta": {}, "steps": 6, "price_words": "100.00 SEK"}])
    );
    let tickets: Vec<_> = shop
        .tickets
        .lock()
        .unwrap()
        .iter()
        .map(|(d, t)| format!("{d}={}", String::from_utf8_lossy(t)))
        .collect();
    assert_eq!(
        tickets,
        [
            "quote=renew:x14",
            "quote=renew:new",
            "start=renew:x14",
            "history=read:x14"
        ]
    );
    assert_eq!(
        call(&r, "GET", "/api/account/subscription", None, None)
            .await
            .0,
        StatusCode::UNAUTHORIZED
    );
    assert!(
        monetize_site_buy::relay::is_relay_path("/api/account/subscription")
            && monetize_site_buy::relay::is_relay_path("/auth/private/terminate")
    );
}

/// ★ Gunnar's `/auth/status` `account_offer` (auth/routes.rs:440-444): "Account on <host>" is
/// drawn for a VISITOR only where the shop sells an account here, as monetize names it.
#[tokio::test]
async fn the_offer_names_the_shops_account_for_a_visitor_only() {
    let (r, _) = relay().await;
    let (st, body) = call(&r, "GET", "/auth/buy/offer", None, None).await;
    assert_eq!(st, StatusCode::OK, "{body}");
    assert_eq!(body["account_offer"], "5GB");
    let (_, body) = call(&r, "GET", "/auth/buy/offer", Some("x14"), None).await;
    assert!(
        body["account_offer"].is_null(),
        "signed in: no account pill ({body})"
    );
}

/// ★ A32-29: **the Prolong line is the server's**: the renewal answer carries the price line
/// composed once here (`100.00 SEK / month + VAT (…)`) and the customer's sentence when it
/// cannot be prolonged, so neither console composes them (gunnar-ui-web's `price_line` and
/// `renewal_line`, holger-ui's copies).
#[tokio::test]
async fn the_renewal_answer_carries_its_price_line_and_its_customer_line() {
    let (r, _shop) = relay().await;
    let (_, body) = call(&r, "GET", "/api/account/subscription", Some("x14"), None).await;
    assert_eq!(
        body["price_line"].as_str(),
        Some("100.00 SEK / month + VAT (your country's rate, added at checkout)"),
        "{body}"
    );
    assert_eq!(
        body["line"].as_str(),
        Some(""),
        "it can be prolonged: no refusal line: {body}"
    );
    let (_, body) = call(&r, "GET", "/api/account/subscription", Some("new"), None).await;
    assert_eq!(
        (body["price_line"].as_str(), body["line"].as_str()),
        (Some(""), Some("")),
        "nothing bought, nothing to renew: no line: {body}"
    );
    use monetize_site_buy::purchase::{price_line, refused_renewal, RENEWING_UNAVAILABLE};
    assert_eq!(
        refused_renewal("r", None, false).line,
        RENEWING_UNAVAILABLE,
        "a refusal with no words says the one sentence"
    );
    assert_eq!(
        refused_renewal("r", Some("Ask us.".into()), false).line,
        "Ask us."
    );
    assert_eq!(
        price_line("", "month"),
        "",
        "no amount, no line: a price is never composed from nothing"
    );
    assert_eq!(
        price_line("220.00 SEK", ""),
        "220.00 SEK + VAT (your country's rate, added at checkout)"
    );
}

/// ★ A32-29: **an offer's price line and an order's are the server's too**: the window's
/// bundle and the attempt a bundle starts carry `price_line` (composed once,
/// [`monetize_site_buy::purchase::price_line`]), so a console never composes the amount, the
/// period and the VAT note itself.
#[test]
fn an_offer_and_its_order_carry_their_price_line() {
    use monetize_proto as mpb;
    use monetize_site_buy::purchase::{bundle_attempt, render_bundle};
    let wire = mpb::Bundle {
        id: "b".into(),
        label: "10 GiB".into(),
        price: "100.00 SEK".into(),
        period: "month".into(),
        can_serve: true,
        ..Default::default()
    };
    let line = "100.00 SEK / month + VAT (your country's rate, added at checkout)";
    assert_eq!(render_bundle(wire.clone()).price_line, line);
    let a = bundle_attempt(
        mpb::StartPurchaseResponse::default(),
        &wire,
        &Default::default(),
        &Default::default(),
        String::new(),
    );
    assert_eq!(a.price_line, line);
    assert_eq!(
        render_bundle(mpb::Bundle {
            price: String::new(),
            ..wire
        })
        .price_line,
        "",
        "no amount, no line"
    );
}

/// ★ A32-29: **the window's poll schedule is the server's**: every window answer carries
/// `poll` (ask again every 2 s, past a minute every 10 s, stop at 10 min), so a console keeps
/// no schedule of its own for a window it could read.
#[test]
fn the_window_carries_its_poll_schedule() {
    use monetize_site_buy::purchase::{window_of, POLL};
    let pending = window_of(monetize_proto::ListBundlesResponse {
        reading: true,
        ..Default::default()
    });
    let json = serde_json::to_value(&pending).unwrap();
    assert_eq!(
        json["poll"],
        serde_json::json!({"again_ms": 2000, "late_after_ms": 60000, "late_again_ms": 10000, "give_up_ms": 600000}),
        "{json}"
    );
    assert_eq!(
        (
            POLL.next_ms(0),
            POLL.next_ms(59_999),
            POLL.next_ms(60_000),
            POLL.next_ms(600_000)
        ),
        (Some(2_000), Some(2_000), Some(10_000), None)
    );
    assert!(!POLL.is_late(59_999) && POLL.is_late(60_000));
    let answered = serde_json::to_value(monetize_site_buy::purchase::window(Vec::new())).unwrap();
    assert!(
        answered.get("poll").is_none(),
        "an answered window is not asked again: {answered}"
    );
}

/// ★ A32-29: **a plan's paid-until day and a purchase's day are the server's words**
/// (`date_words`, UTC `YYYY-MM-DD`), so no console carries its own calendar arithmetic.
#[test]
fn the_plan_and_the_past_purchases_say_their_days() {
    use monetize_site_buy::purchase::{date_words, render_plan};
    assert_eq!(
        (
            date_words(0),
            date_words(86_400_000),
            date_words(1_799_000_000_000)
        ),
        (
            String::new(),
            "1970-01-02".to_owned(),
            "2027-01-03".to_owned()
        )
    );
    let plan = render_plan(monetize_proto::PlanStatus {
        state: "paid".into(),
        paid_until_unix_ms: 1_799_000_000_000,
        ..Default::default()
    });
    assert_eq!(plan.paid_until_words, "2027-01-03");
}

/// ★ A32-29: **an offer button's words and an order's paid-until day are the server's**: the
/// window's bundle carries `offer_words` (what it adds in customer words, then monetize's amount
/// and period; monetize's label when it sent no delta), and a paid order's attempt carries
/// `paid_until_words` beside the day it names, so a console composes neither.
#[test]
fn an_offer_says_its_button_and_a_paid_order_its_day() {
    use monetize_proto as mpb;
    use monetize_site_buy::purchase::{render_bundle, status_attempt};
    const GIB: u64 = 1 << 30;
    let words =
        |b: mpb::Bundle| serde_json::to_value(render_bundle(b)).unwrap()["offer_words"].clone();
    let wire = mpb::Bundle {
        id: "b".into(),
        label: "10.0 GiB more room".into(),
        price: "100.00 SEK".into(),
        period: "month".into(),
        delta: [("pack_bytes".to_owned(), 10 * GIB)].into_iter().collect(),
        can_serve: true,
        ..Default::default()
    };
    assert_eq!(
        words(wire.clone()),
        json!("+10 GiB storage \u{2014} 100.00 SEK / month")
    );
    let two = mpb::Bundle {
        delta: [
            ("pack_bytes".to_owned(), 3 * GIB / 2),
            ("lfs_bytes".to_owned(), GIB),
        ]
        .into_iter()
        .collect(),
        period: String::new(),
        ..wire.clone()
    };
    assert_eq!(
        words(two),
        json!("+1 GiB large files, +1.5 GiB storage \u{2014} 100.00 SEK")
    );
    let bare = mpb::Bundle {
        label: " 10 GiB more room ".into(),
        price: String::new(),
        delta: Default::default(),
        ..wire
    };
    assert_eq!(
        words(bare),
        json!("10 GiB more room"),
        "no delta: monetize's label; no amount: no price"
    );

    let at = 1_799_000_000_000u64;
    let fact = Some(mpb::EntitlementFact {
        paid_until_unix_ms: Some(at),
        ..Default::default()
    });
    let paid = status_attempt(
        mpb::PurchaseStatusResponse {
            reference: "r".into(),
            state: mpb::PurchaseState::Paid as i32,
            fact: fact.clone(),
            ..Default::default()
        },
        String::new(),
    );
    assert_eq!(
        serde_json::to_value(&paid).unwrap()["paid_until_words"],
        json!("2027-01-03")
    );
    let pending = status_attempt(
        mpb::PurchaseStatusResponse {
            reference: "r".into(),
            state: mpb::PurchaseState::Pending as i32,
            fact,
            ..Default::default()
        },
        String::new(),
    );
    assert!(
        serde_json::to_value(&pending)
            .unwrap()
            .get("paid_until_words")
            .is_none(),
        "an unpaid order names no day"
    );
}