crates/monetize-site-buy/tests/a17_mint_deadline.rs
//! ★ **A grow or a renewal waits for its payment page as a start does** (A17 F-B-02,
//! 2026-10-23): `EstateOrder.ChangePlan`, `EstateOrder.Renew` and the account's
//! `Purchase.Start{renew}` each mint a vendor page, so they ride the minting channel under the
//! mint deadline and a slow vendor is answered [`START_TOO_SLOW`] (504), as `private_start` is,
//! not the 10 s call channel's "the shop did not answer", nor no deadline at all.
use std::collections::BTreeMap;
use std::sync::Arc;
use std::time::{Duration, Instant};
use axum::http::{HeaderMap, StatusCode};
use monetize_proto as pb;
use monetize_site_buy::link::{Bearer, Deadlines, MINT_GRACE};
use monetize_site_buy::relay::START_TOO_SLOW;
use monetize_site_buy::{Account, Buyer, BuySite, Refusal, Relay, ShopLink, Who};
use serde_json::json;
use tonic::{Request, Response, Status};
/// Far past every deadline: the vendor that never opens a page in time.
const SLOW: Duration = Duration::from_secs(8);
const DEADLINES: Deadlines = Deadlines { connect: Duration::from_secs(3), call: Duration::from_millis(300), mint: Duration::from_millis(900) };
/// A shop that answers its reads at once and mints every page too slowly.
#[derive(Clone, Default)]
struct Shop;
#[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 sells(&self, _: Request<pb::EstateOrderSellsRequest>) -> Result<Response<pb::EstateOrderSellsResponse>, Status> {
Ok(Response::new(pb::EstateOrderSellsResponse { estates: true }))
}
async fn change_plan(&self, _: Request<pb::EstateOrderChangePlanRequest>) -> Result<Response<pb::EstateOrderChangePlanResponse>, Status> {
tokio::time::sleep(SLOW).await;
Err(Status::internal("too late"))
}
async fn renew(&self, _: Request<pb::EstateOrderRenewRequest>) -> Result<Response<pb::EstateOrderRenewResponse>, Status> {
tokio::time::sleep(SLOW).await;
Err(Status::internal("too late"))
}
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 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 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 cancel(&self, _: Request<pb::EstateOrderCancelRequest>) -> Result<Response<pb::EstateOrderCancelResponse>, Status> { Err(Status::unimplemented("cancel")) }
async fn terminate(&self, _: Request<pb::EstateOrderTerminateRequest>) -> Result<Response<pb::EstateOrderTerminateResponse>, Status> { Err(Status::unimplemented("terminate")) }
async fn restore_front(&self, _: Request<pb::EstateOrderRestoreFrontRequest>) -> Result<Response<pb::EstateOrderRestoreFrontResponse>, Status> { Err(Status::unimplemented("restore_front")) }
}
#[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, _: Request<pb::QuoteRequest>) -> Result<Response<pb::QuoteResponse>, Status> { Err(Status::unimplemented("quote")) }
async fn history(&self, _: Request<pb::PurchaseHistoryRequest>) -> Result<Response<pb::PurchaseHistoryResponse>, Status> { Err(Status::unimplemented("history")) }
async fn start(&self, _: Request<pb::StartPurchaseRequest>) -> Result<Response<pb::StartPurchaseResponse>, Status> {
tokio::time::sleep(SLOW).await;
Err(Status::internal("too late"))
}
}
/// Everyone who asks is the account x14, signed in.
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, _: &HeaderMap) -> Result<Who, Refusal> {
Ok(Who::Person(Buyer { account: "x14".into(), tenant: "x14-repo".into(), ..Default::default() }))
}
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) -> Result<BTreeMap<String, u64>, Refusal> {
Ok(BTreeMap::from([("disk_bytes".to_string(), 5u64)]))
}
async fn signup_account(&self, _: &str) -> Result<Account, Refusal> {
Ok(Account::NoPasskey)
}
}
/// The relay over the slow shop, with the channels [`ShopLink::connect_lazy`] builds (the call
/// channel cut at the call deadline, the minting one at the mint deadline and its grace).
async fn relay() -> Arc<Relay<Site>> {
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))
.add_service(pb::purchase_server::PurchaseServer::new(Shop))
.add_service(pb::products_server::ProductsServer::new(Shop))
.serve_with_incoming(tokio_stream::wrappers::TcpListenerStream::new(listener)),
);
let ep = tonic::transport::Endpoint::from_shared(format!("http://{at}")).unwrap().connect_timeout(DEADLINES.connect);
let channel = ep.clone().timeout(DEADLINES.call).connect_lazy();
let minting = ep.timeout(DEADLINES.mint + MINT_GRACE).connect_lazy();
let link = ShopLink::from_channels(channel, minting, Bearer::of("site-bearer-for-tests").unwrap(), "holger", DEADLINES);
Arc::new(Relay::new(Site, link))
}
/// One POST through the router, and how long it took (cut at 6 s: no deadline at all fails).
async fn post(r: &Arc<Relay<Site>>, uri: &str, body: serde_json::Value) -> (StatusCode, serde_json::Value, Duration) {
use tower::ServiceExt;
let req = axum::http::Request::builder().method("POST").uri(uri).header("content-type", "application/json").body(axum::body::Body::from(body.to_string())).unwrap();
let t0 = Instant::now();
let res = tokio::time::timeout(Duration::from_secs(6), monetize_site_buy::router(r.clone()).oneshot(req)).await.unwrap_or_else(|_| panic!("{uri}: no answer in 6 s, so no deadline")).unwrap();
let took = t0.elapsed();
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), took)
}
fn assert_too_slow(door: &str, (status, body, took): (StatusCode, serde_json::Value, Duration)) {
assert_eq!((status, body["error"].as_str()), (StatusCode::GATEWAY_TIMEOUT, Some(START_TOO_SLOW)), "{door}: {body}");
assert!(took >= DEADLINES.mint, "{door}: answered after {took:?}, before the mint deadline {:?}", DEADLINES.mint);
assert!(took < DEADLINES.mint + MINT_GRACE, "{door}: answered after {took:?}, past the mint deadline");
}
#[tokio::test]
async fn a_grow_waits_the_mint_deadline_and_says_the_page_did_not_open() {
let r = relay().await;
assert_too_slow("change", post(&r, "/auth/private/change", json!({"company": "acme", "main_data_gib": 10})).await);
}
#[tokio::test]
async fn an_instance_renewal_waits_the_mint_deadline_and_says_the_page_did_not_open() {
let r = relay().await;
assert_too_slow("renew", post(&r, "/auth/private/renew", json!({"company": "acme"})).await);
}
#[tokio::test]
async fn an_account_renewal_has_a_deadline_and_says_the_page_did_not_open() {
let r = relay().await;
assert_too_slow("account renew", post(&r, "/api/account/subscription/renew", json!({})).await);
}