crates/gunnar-ui-server/src/api/increase.rs
//! **More room for one namespace**, asked for by the namespace that wants it.
//!
//! Compiled only under `metered`, and even then the routes exist only when a
//! monetize bearer is configured. On a community appliance this file is not in
//! the binary; on a metered one without the credential there is no route, not a
//! disabled button and not an error — the console simply has nothing to say
//! about it, exactly as it has nothing to say about anonymous browsing without
//! a guest key.
//!
//! # The rule this module is written under
//!
//! **This console contains no price, no currency, no size and no menu of
//! bundles.** It renders what monetize answers. The same code has to draw
//! nothing at all when monetize is not configured, and a real offer when it is,
//! because the SERVER differs and the console does not. The rule, 2026-09-07:
//! hardcode nothing — monetize fills in both the "what" and the price.
//!
//! What that rule costs, stated plainly because it is a finding and not an
//! oversight: **the price is not sent to the browser at all.**
//! `QuoteResponse.price` is a `Money { minor, Currency }` — minor units and an
//! enum. Turning that into `10 €` needs a division by 100 and a map from
//! `CURRENCY_EUR` to a symbol, which is money arithmetic and a currency
//! literal, and both are the thing being forbidden. So a QUOTE still carries no
//! amount to the browser: [`render_offer`] drops `price` and `cost` on the
//! floor, and what a typed order costs is shown by the vendor on the page the
//! tenant is sent to, which is where the card is anyway.
//!
//! That last sentence used to end *"if a price is ever to appear here, monetize
//! must send it as words"*, and monetize now does — `QuoteResponse.price_words`
//! (2026-09-17) and [`Bundle::price_words`] (2026-09-19), both written by one
//! function upstream so a window, a quote and a receipt cannot be formatted
//! three ways. Words are carriable; `Money { minor, currency }` is not. The
//! rule did not move — this console still composes no amount — the wire did.
//!
//! # What is purchasable comes from monetize, not from a list written here
//!
//! [`sellable`] asks `Products.List` and keeps the meters monetize says are
//! **enforced and priced**: `Enforcement::ENFORCED` (the cap is a wall the
//! tenant actually hits) and a non-empty `MeterInfo.unit` (the price-list unit
//! it is billed by; empty means it costs nothing to promise). Both are the
//! product plugin's own words. A console that carried its own list would sell
//! `repos` — which is `MEASURED` in gunnar today, a limit nothing enforces —
//! and the tenant would have no way to tell.
//!
//! # The increment monetize could not supply, and now can
//!
//! This paragraph used to read: *"there is no message anywhere in `monetize.v1`
//! for '10 GiB for 10 €' as a purchasable thing … so the amount is a number the
//! tenant types."* That was the honest answer to the rule and it produced a
//! debugging form — two free-text boxes asking a customer for a meter name and
//! a count of bytes.
//!
//! `Purchase.Bundles` (vendored 2026-09-19) is the message that was missing. It
//! answers with offers, and every string on one is monetize's: the label it
//! composed from the delta, the amount ALREADY RENDERED as words, the period,
//! and `can_serve` asked through the same quote the order itself will run. So
//! the menu is still not invented here — it is read — and the rule is
//! unchanged: [`bundles`] carries those strings through untouched and
//! [`start_bundle`] echoes back an id it never parses.
//!
//! **The typed form stays.** A deployment that declared no bundles answers with
//! an empty list, and typing a size nobody declared is then the only way to ask
//! for one. Buttons when there are offers, boxes when there are not; the
//! console-side fork is `gunnar_ui_web::offers::room` and it is tested there.
//!
//! # `cost` is the provider's number and never leaves this process
//!
//! `QuoteResponse` carries two amounts. `price` is what the customer pays;
//! `cost` is what Vetra pays UpCloud for the iron. They are the two halves of a
//! margin, and a margin is not a thing a customer is shown. [`Offer`] has no
//! field for either, and `the_offer_carries_no_cost` drives a real request
//! through the router against a monetize that answers with a distinctive one
//! and asserts the number is nowhere in the response body — not the struct's
//! shape, the bytes on the wire.
//!
//! # The order of the three calls, and why `Quote` is not optional
//!
//! `Quote` prices an order and moves nothing. `Start` runs the same order and
//! reserves the money. The console always quotes first and shows what came
//! back, so the button a tenant presses is under the answer to the question
//! they asked. When `can_serve` is false there is no button at all and
//! monetize's `reason` is rendered verbatim — on `gunnar.rs` today that is the
//! usual state, because `Admin.Capacity` says the box is short, and a refusal
//! that reads as deliberate is the difference between an honest wall and a
//! console that looks broken.
//!
//! `Start` answers with a `redirect_url` when the vendor needs the customer —
//! a hosted Stripe Checkout session, since a 3-D Secure card confirmed in place
//! answers `requires_action` with no URL at all and under SCA that is the
//! ordinary European case. So this console never sees a card number, holds no
//! payment form and loads no vendor script: it sends the tenant to a page the
//! vendor hosts. The outcome comes back to MONETIZE by webhook, never to the
//! browser's return URL, so the console polls [`status`] and believes only
//! that.
use crate::api::ApiError;
use crate::mpb;
// ── The shop window ────────────────────────────────────────────────────────
/// **ONE THING THIS DEPLOYMENT WILL SELL, in monetize's own words.**
///
/// Written 2026-09-19 against the panel this module's own docs describe as the
/// thing it could not build:
///
/// > *"What monetize CANNOT supply is the increment. There is no message
/// > anywhere in `monetize.v1` for '10 GiB for 10 €' as a purchasable thing …
/// > so the amount is a number the tenant types."*
///
/// That is no longer true. `Purchase.Bundles` is the message, and every string
/// on this struct is monetize's, carried verbatim:
///
/// * [`Bundle::id`] is **opaque**. It is echoed back on the order and NEVER
/// parsed — an operator who names one `bigger` must break nothing;
/// * [`Bundle::label`] is what is being sold, composed by monetize from the
/// delta so no operator can write a window that disagrees with the order;
/// * [`Bundle::price_words`] is the amount ALREADY RENDERED. `Money { minor,
/// currency }` never reaches this process on this path, which is what makes a
/// price sayable here at all: this console may hold no currency literal and
/// do no money arithmetic, so a number it would have to divide by 100 is a
/// number it cannot show. `price_words` is named for the property rather than
/// for the field upstream, exactly as `Past::price_words` is;
/// * [`Bundle::can_serve`] and [`Bundle::reason`] are the same three questions
/// the order itself will ask, asked before the customer is shown a button.
///
/// Field by field and never `serde` over the proto message — the rule
/// [`render_offer`] was written under, and for the same reason.
pub use monetize_site_buy::purchase::{Bundle, Plan, Window, AWAITING_PROVIDER, NOTHING_FITS, PROVIDER_LATE};
/// The operator's doors' answers, moved to monetize-site-buy with the doors (G-REDS 4a).
pub use monetize_site_buy::purchase::{Offer, Placed, Purchasable, Sellable};
/// `PlanStatus` as the browser sees it.
pub fn render_plan(p: mpb::PlanStatus) -> Plan {
monetize_site_buy::purchase::render_plan(crate::monetize::wire(&p))
}
/// `Bundle` as the browser sees it. One function, used by both consoles.
pub fn render_bundle(b: mpb::Bundle) -> Bundle {
monetize_site_buy::purchase::render_bundle(crate::monetize::wire(&b))
}
/// **monetize's list, reduced to the rows it can serve.**
///
/// One function for both consoles, so the operator's window and the tenant's
/// cannot disagree about what is on sale. Each withheld row goes to the journal
/// with the sentence monetize attached to it — the operator's reason is already
/// in monetize's own journal, on the line that produced the verdict.
pub fn window(bundles: Vec<mpb::Bundle>) -> Window {
monetize_site_buy::purchase::window(bundles.iter().map(crate::monetize::wire).collect())
}
/// **monetize's whole answer to `Purchase.Bundles`, as the browser sees it**:
/// [`window`] over its rows, or — when monetize says the window is still being
/// read — no rows, nothing withheld, and the two lines the panel draws while
/// it asks again. One function for both consoles.
pub fn window_of(answer: mpb::ListBundlesResponse) -> Window {
monetize_site_buy::purchase::window_of(crate::monetize::wire(&answer))
}
/// **A market word to monetize's enum** — the three this estate sells in, and
/// a refusal NAMING anything else before monetize is dialled (a typo is not
/// the default market).
pub fn market_of(word: Option<&str>) -> Result<mpb::Market, ApiError> {
crate::auth::validate::market(word).map_err(|refusal| ApiError::from(tonic::Status::invalid_argument(refusal)))
}
#[cfg(test)]
mod tests {
use super::*;
/// ★ G-REDS 4a: **the operator's orders for a tenant are the shared buy flow's ROUTER**
/// (`monetize_site_buy::router_of`, its `operator` and `operator_orders` groups, mounted
/// with the account's doors): what is for sale, the offer, the typed and the window order,
/// the window and the follow. What this console supplies is `GunnarSite::operator` (an
/// administrator, the open console on loopback, the operator's payer). The tests below drive
/// the console's real router, so they hold wherever the doors are answered.
#[test]
fn the_operator_doors_are_the_shared_routers() {
let here = include_str!("increase.rs");
let serve = include_str!("../serve.rs");
for door in ["purchasable", "offer", "start", "status", "bundles", "start_bundle"] {
assert_eq!(here.matches(&["pub async fn ", door, "("].concat()).count(), 0, "{door} is the shared router's, not a handler here");
assert_eq!(serve.matches(&["api::increase::", door, ")"].concat()).count(), 0, "{door}: no route by hand");
}
assert!(serve.contains("operator: true"), "serve.rs mounts the shared operator doors");
}
use std::sync::{Arc, Mutex};
use tower::ServiceExt as _;
use crate::mpb::products_server::{Products as ProductsService, ProductsServer};
use crate::mpb::purchase_server::{Purchase as PurchaseService, PurchaseServer};
/// **The bearer the fake monetize insists on**, so a test can prove the
/// console presented THIS credential and not one of the other two it holds.
const BEARER: &str = "the-monetize-bearer";
/// **The provider's cost, and it is nothing like the price.**
///
/// 133_700 against a price of 1_000: no digit of one is a prefix of the
/// other, neither is a round multiple of the other, and neither is zero. A
/// leak of `cost` under any name — a field called `price`, a stray
/// serialisation of the whole message — puts these exact digits in the body,
/// and a fixture where the two were equal (or where cost was 0, which is
/// what it is on the box's own iron) would pass against a handler that sent
/// the wrong one.
const COST_MINOR: u64 = 133_700;
const PRICE_MINOR: u64 = 1_000;
/// **The vendor's own checkout page.** A `cs_` session id no template of
/// ours could mint, which is the point: the console must carry the link the
/// vendor invented, byte for byte, and not compose one.
const CHECKOUT_URL: &str = "https://checkout.stripe.com/c/pay/cs_test_a1b2c3d4e5";
/// **One offer, in monetize's words.** Every one of these four is a string
/// the console must carry unchanged and could not have composed: the id is
/// opaque, the label is monetize's rendering of the delta, and the amount is
/// WORDS — this console holds no currency literal and does no arithmetic, so
/// a body missing this exact text is a body it tried to compose one into.
const BUNDLE_ID: &str = "10gb-1m";
const BUNDLE_LABEL: &str = "10.0 GiB more room";
const BUNDLE_PRICE_WORDS: &str = "220.00 SEK";
const BUNDLE_REFUSAL: &str = "this box has no room left to promise right now";
/// A monetize that quotes, and records every call it was asked to make.
#[derive(Clone, Default)]
struct FakeMonetize {
/// Every RPC that reached this server, in order. The authorisation
/// tests assert this is EMPTY: a refusal that still made the call has
/// already told monetize about a tenant it should not have named.
calls: Arc<Mutex<Vec<String>>>,
/// What `Quote` says about serving it.
can_serve: bool,
reason: String,
/// **What every `Purchase.Start` actually carried**: `(bundle, whether
/// it named caps)`. The two fields are mutually exclusive on the wire
/// and a real monetize refuses a request that sets both, so a console
/// that sent the id AND a cap map would be refused in production and
/// pass a test that only looked at the id.
ordered: Arc<Mutex<Vec<(String, bool)>>>,
/// The market every `Purchase.Start` / `Purchase.Bundles` named.
markets: Arc<Mutex<Vec<i32>>>,
/// **What `Purchase.Bundles` lists**, when a test wants something other
/// than the usual two rows (one servable, one not): `Some(vec![])` is a
/// deployment that declared nothing, `Some(all refused)` is a shop with
/// rows and no room.
declared: Option<Vec<mpb::Bundle>>,
/// **`Purchase.Bundles` says the window is still being read**
/// (`ListBundlesResponse.reading`, test2 2026-10-05): no rows.
reading: bool,
/// **The payer every `Purchase.Start` named** (g-receipts): the
/// address Stripe mails the order's receipt to, or empty.
payers: Arc<Mutex<Vec<String>>>,
/// The vendor link `Purchase.Start` answers with.
checkout_url: &'static str,
}
impl FakeMonetize {
fn serving() -> FakeMonetize {
FakeMonetize {
can_serve: true,
checkout_url: CHECKOUT_URL,
..Default::default()
}
}
fn record(
&self,
request: &tonic::Request<impl Sized>,
what: &str,
) -> Result<(), tonic::Status> {
let offered = request
.metadata()
.get("authorization")
.and_then(|v| v.to_str().ok())
.unwrap_or_default()
.to_owned();
if offered != format!("Bearer {BEARER}") {
// What a real monetize does with a caller it cannot name. The
// console holds three credentials and only one of them opens
// this door.
return Err(tonic::Status::unauthenticated(format!(
"monetize was offered {offered:?}"
)));
}
self.calls
.lock()
.expect("the call log")
.push(what.to_owned());
Ok(())
}
}
#[tonic::async_trait]
impl ProductsService for FakeMonetize {
async fn list(
&self,
request: tonic::Request<mpb::ListProductsRequest>,
) -> Result<tonic::Response<mpb::ListProductsResponse>, tonic::Status> {
self.record(&request, "Products.List")?;
Ok(tonic::Response::new(mpb::ListProductsResponse {
products: vec![
mpb::ProductInfo {
id: "gunnar".to_owned(),
meters: vec![
// Enforced AND priced: a wall that costs money.
mpb::MeterInfo {
name: "pack_bytes".to_owned(),
meaning: "objects on disk that are not re-derivable".to_owned(),
enforcement: mpb::Enforcement::Enforced as i32,
scope: mpb::Scope::Tenant as i32,
unit: "disk_gib_month".to_owned(),
},
// Enforced and NOT priced: a budget, free to promise.
mpb::MeterInfo {
name: "cache_bytes".to_owned(),
meaning: "the exploded read cache".to_owned(),
enforcement: mpb::Enforcement::Enforced as i32,
scope: mpb::Scope::Tenant as i32,
unit: String::new(),
},
// Priced but only MEASURED: a limit nothing polices.
// Selling this is selling a promise nobody keeps.
mpb::MeterInfo {
name: "repos".to_owned(),
meaning: "repositories in the namespace".to_owned(),
enforcement: mpb::Enforcement::Measured as i32,
scope: mpb::Scope::Tenant as i32,
unit: "disk_gib_month".to_owned(),
},
],
reachable: None,
// This fake monetize does not verify tickets. The
// operator path below never presents one, so the flag
// decides nothing here — it is `api::subscription`'s
// gate, and its own tests set it both ways.
tenant_renewal: false,
metered_door: None,
},
// Another product's meters must never reach gunnar's console.
mpb::ProductInfo {
id: "holger".to_owned(),
meters: vec![mpb::MeterInfo {
name: "rows".to_owned(),
meaning: "holger's own".to_owned(),
enforcement: mpb::Enforcement::Enforced as i32,
scope: mpb::Scope::Tenant as i32,
unit: "disk_gib_month".to_owned(),
}],
reachable: None,
// This fake monetize does not verify tickets. The
// operator path below never presents one, so the flag
// decides nothing here — it is `api::subscription`'s
// gate, and its own tests set it both ways.
tenant_renewal: false,
metered_door: None,
},
],
..Default::default()
}))
}
}
#[tonic::async_trait]
impl PurchaseService for FakeMonetize {
async fn quote(
&self,
request: tonic::Request<mpb::QuoteRequest>,
) -> Result<tonic::Response<mpb::QuoteResponse>, tonic::Status> {
self.record(&request, "Purchase.Quote")?;
let asked = request.into_inner();
Ok(tonic::Response::new(mpb::QuoteResponse {
words: String::new(),
reference: format!("gunnar/{}/2026-09-07", asked.tenant),
price: Some(mpb::Money {
minor: PRICE_MINOR,
currency: mpb::Currency::Eur as i32,
}),
cost: Some(mpb::Money {
minor: COST_MINOR,
currency: mpb::Currency::Eur as i32,
}),
current: [("pack_bytes".to_owned(), 10_737_418_240u64)].into(),
caps: asked.caps.clone(),
delta: [("pack_bytes".to_owned(), 10_737_418_240u64)].into(),
resources: Vec::new(),
can_serve: self.can_serve,
reason: self.reason.clone(),
period: "month".to_owned(),
// The OPERATOR path renders no amount at all — see
// `the_offer_carries_no_cost` and this module's rule — so a
// non-empty value here would fail that test rather than pass
// unnoticed.
price_words: String::new(),
}))
}
/// **The shop window.** Two rows on purpose: one that can be served
/// and one that cannot, so a single request exercises both halves of
/// the console's per-row fork.
async fn bundles(
&self,
request: tonic::Request<mpb::ListBundlesRequest>,
) -> Result<tonic::Response<mpb::ListBundlesResponse>, tonic::Status> {
self.record(&request, "Purchase.Bundles")?;
self.markets.lock().expect("the market log").push(request.get_ref().market);
if self.reading {
return Ok(tonic::Response::new(mpb::ListBundlesResponse { bundles: Vec::new(), reading: true, ..Default::default() }));
}
if let Some(declared) = &self.declared {
return Ok(tonic::Response::new(mpb::ListBundlesResponse {
bundles: declared.clone(),
reading: false, ..Default::default()
}));
}
Ok(tonic::Response::new(mpb::ListBundlesResponse {
reading: false,
bundles: vec![
mpb::Bundle {
id: BUNDLE_ID.to_owned(),
label: BUNDLE_LABEL.to_owned(),
// WORDS. The console may not divide by 100 or map a
// currency, so this is the only shape an amount can
// reach a browser in, and it must arrive unchanged.
price: BUNDLE_PRICE_WORDS.to_owned(),
period: "month".to_owned(),
delta: [("pack_bytes".to_owned(), 10_737_418_240u64)].into(),
can_serve: true,
reason: String::new(), ..Default::default()
},
mpb::Bundle {
id: "100gb-1m".to_owned(),
label: "100.0 GiB more room".to_owned(),
price: "2200.00 SEK".to_owned(),
period: "month".to_owned(),
delta: [("pack_bytes".to_owned(), 107_374_182_400u64)].into(),
can_serve: false,
reason: BUNDLE_REFUSAL.to_owned(), ..Default::default()
},
], ..Default::default()
}))
}
async fn start(
&self,
request: tonic::Request<mpb::StartPurchaseRequest>,
) -> Result<tonic::Response<mpb::StartPurchaseResponse>, tonic::Status> {
self.record(&request, "Purchase.Start")?;
let asked = request.into_inner();
self.markets.lock().expect("the market log").push(asked.market);
self.payers.lock().expect("the payer log").push(asked.payer.clone());
self.ordered
.lock()
.expect("the order log")
.push((asked.bundle.clone(), !asked.caps.is_empty()));
Ok(tonic::Response::new(mpb::StartPurchaseResponse {
reference: format!("gunnar/{}/2026-09-07", asked.tenant),
state: mpb::PurchaseState::Pending as i32,
handle_vendor: "stripe".to_owned(),
handle_id: "cs_test_a1b2c3d4e5".to_owned(),
redirect_url: self.checkout_url.to_owned(),
reason: "waiting for the customer at the vendor".to_owned(),
fact: None,
// PENDING gives nothing yet, so it says nothing.
words: String::new(),
users: None,
}))
}
async fn status(
&self,
request: tonic::Request<mpb::PurchaseStatusRequest>,
) -> Result<tonic::Response<mpb::PurchaseStatusResponse>, tonic::Status> {
self.record(&request, "Purchase.Status")?;
let asked = request.into_inner();
self.calls.lock().expect("the call log").push(format!(
"scoped {} {} {}",
asked.product, asked.tenant, asked.reference
));
// monetize 3b44e7d: with a tenant named, only that tenant's
// attempt of that product is answered — one uniform NOT_FOUND.
let owner = asked
.reference
.split_once('/')
.and_then(|(_, rest)| rest.rsplit_once('/'))
.map(|(owner, _)| owner.to_owned())
.unwrap_or_default();
if !asked.tenant.is_empty() && owner != asked.tenant {
return Err(tonic::Status::not_found(
"this tenant has no purchase by that reference",
));
}
Err(tonic::Status::unimplemented("not this test's"))
}
async fn abandon(
&self,
request: tonic::Request<mpb::AbandonPurchaseRequest>,
) -> Result<tonic::Response<mpb::AbandonPurchaseResponse>, tonic::Status> {
self.record(&request, "Purchase.Abandon")?;
Err(tonic::Status::unimplemented("not this test's"))
}
async fn cancel(
&self,
request: tonic::Request<mpb::CancelPurchaseRequest>,
) -> Result<tonic::Response<mpb::CancelPurchaseResponse>, tonic::Status> {
self.record(&request, "Purchase.Cancel")?;
Err(tonic::Status::unimplemented("not this test's"))
}
}
/// Stand a monetize up on loopback and point a console at it.
async fn console(fake: FakeMonetize) -> (axum::Router, Arc<Mutex<Vec<String>>>) {
console_receipts_to(fake, None).await
}
/// [`console`], with the operator's receipt address fed (g-receipts).
async fn console_receipts_to(fake: FakeMonetize, receipts_to: Option<&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(ProductsServer::new(fake.clone()))
.add_service(PurchaseServer::new(fake))
.serve_with_incoming(tokio_stream::wrappers::TcpListenerStream::new(listener))
.await
});
let signer = Arc::new(
crate::signer::Signer::new(ssh_key::PrivateKey::from(
ssh_key::private::Ed25519Keypair::from_seed(&[9u8; 32]),
))
.expect("a fixed seed makes a signer"),
);
// A gunnar that is not there. Nothing on these routes talks to gunnar,
// and a handle pointing at a port nobody is listening on is how that is
// held: a route that quietly grew a gunnar call would hang here rather
// than pass.
let gunnar =
crate::grpc::Gunnar::connect_lazy("127.0.0.1:1", signer).expect("a lazy handle");
let store = crate::state::StateStore::open(
tempfile::Builder::new()
.suffix(".redb")
.tempfile()
.expect("a temp file")
.into_temp_path()
.keep()
.expect("keep the path"),
)
.expect("a state store");
let router = crate::serve::router(Arc::new(crate::app::App {
webauthn: None,
admin_token: None,
monetize: Some(
crate::monetize::Monetize::connect_lazy(&format!("http://{addr}"), BEARER, "gunnar")
.expect("a lazy channel")
.with_receipts_to(receipts_to),
),
events: crate::sse::EventHub::detached(),
gunnar: gunnar.clone(),
twin: None,
guest: None,
reviewers: Arc::new(crate::reviewer::ReviewerKeys::new(
"127.0.0.1:1".to_owned(),
store.clone(),
)),
tenants: Arc::new(crate::tenant::TenantKeys::new(
"127.0.0.1:1".to_owned(),
store,
)),
forges: Default::default(),
imports: Default::default(),
forge_apps: Default::default(),
ui_dir: None,
caches: crate::api::Caches::default(),
objects: Default::default(),
readmes: Default::default(),
}));
(router, calls)
}
async fn body_of(router: axum::Router, uri: &str) -> (http::StatusCode, String) {
let response = router
.oneshot(
http::Request::builder()
.uri(uri)
.body(axum::body::Body::empty())
.expect("a request"),
)
.await
.expect("the router answers");
let status = response.status();
let bytes = axum::body::to_bytes(response.into_body(), 1 << 20)
.await
.expect("a body");
(status, String::from_utf8(bytes.to_vec()).expect("utf-8"))
}
/// **The provider's cost must never reach a tenant's browser.**
///
/// `price` is what the customer pays; `cost` is what Vetra pays its cloud
/// for the iron the order buys. Sending both puts the margin on the wire,
/// where anyone who opens the network tab can read it — and a JSON field
/// nobody renders is still a disclosure, because the browser received it.
///
/// Asserted on the BYTES rather than on the struct: a field added to
/// [`Offer`] tomorrow, or a handler that serialises the whole proto message
/// because that was easier, both fail here. The struct's shape is a claim;
/// the body is the evidence.
#[tokio::test]
async fn the_offer_carries_no_cost() {
let (router, calls) = console(FakeMonetize::serving()).await;
let (status, body) = body_of(
router,
"/api/namespaces/alpha/increase/offer?meter=pack_bytes&cap=21474836480",
)
.await;
assert_eq!(status, http::StatusCode::OK, "{body}");
assert!(
!body.contains(&COST_MINOR.to_string()),
"the provider's cost reached the browser. monetize answered cost={COST_MINOR} \
beside price={PRICE_MINOR}, and the console sent it on:\n {body}"
);
assert!(
!body.to_lowercase().contains("cost"),
"the response names a cost:\n {body}"
);
assert_eq!(
*calls.lock().expect("the call log"),
vec!["Purchase.Quote".to_owned()],
"one quote and nothing else; a quote moves no money and must not start anything"
);
}
/// The quote's own words survive: the refusal, and the period.
#[tokio::test]
async fn a_refusal_is_monetizes_sentence_and_not_the_consoles() {
let refusal = "the set cannot grow while gunnar is running and 35 GiB are short";
let (router, _) = console(FakeMonetize {
can_serve: false,
reason: refusal.to_owned(),
..Default::default()
})
.await;
let (status, body) = body_of(
router,
"/api/namespaces/alpha/increase/offer?meter=pack_bytes&cap=21474836480",
)
.await;
assert_eq!(status, http::StatusCode::OK, "{body}");
assert!(
body.contains(refusal),
"the refusal was reworded:\n {body}"
);
assert!(body.contains("\"can_serve\":false"), "{body}");
assert!(body.contains("\"period\":\"month\""), "{body}");
}
/// **What is purchasable is monetize's answer, filtered by monetize's own
/// two words** — and nothing that came from a list written here.
#[tokio::test]
async fn only_enforced_and_priced_meters_of_this_product_are_offered() {
let (router, _) = console(FakeMonetize::serving()).await;
let (status, body) = body_of(router, "/api/namespaces/alpha/increase").await;
assert_eq!(status, http::StatusCode::OK, "{body}");
assert!(body.contains("pack_bytes"), "{body}");
assert!(
!body.contains("cache_bytes"),
"cache_bytes is enforced but not priced: it costs nothing to promise, so it is not \
for sale:\n {body}"
);
assert!(
!body.contains("repos"),
"repos is priced but only MEASURED — gunnar's own plugin says it is not yet a wall. \
Selling it would sell a limit nothing enforces:\n {body}"
);
assert!(
!body.contains("rows"),
"that is holger's meter, on gunnar's console:\n {body}"
);
}
/// **Every word in the window is monetize's, carried unchanged** — for the
/// rows that are IN the window.
///
/// The three strings this asserts on are the three a console could be
/// tempted to compose: the label (it could have built one from the delta),
/// the amount (it could have divided a minor unit by 100 and picked a
/// symbol) and the period. All three arrive as bytes and leave as bytes.
///
/// **Rewritten 2026-09-20, knowingly.** Until then this test held that the
/// row monetize could NOT serve reached the browser too, reason and all —
/// *"so the panel would draw a row with no button and no explanation"*. The
/// owner ruled the opposite after reading exactly that row for five days:
/// a card for a size the box cannot hold is not offered at all. The
/// assertion is inverted here on purpose and
/// [`a_bundle_monetize_cannot_serve_is_not_in_the_window`] carries it.
#[tokio::test]
async fn the_window_is_monetizes_words_and_the_console_composes_none_of_them() {
let (router, calls) = console(FakeMonetize::serving()).await;
let (status, body) = body_of(router, "/api/namespaces/alpha/increase/bundles").await;
assert_eq!(status, http::StatusCode::OK, "{body}");
assert!(body.contains(BUNDLE_ID), "the id was not carried:\n {body}");
assert!(body.contains(BUNDLE_LABEL), "the label was reworded:\n {body}");
assert!(
body.contains(BUNDLE_PRICE_WORDS),
"the amount monetize rendered did not reach the browser; a console that drops it has \
to invent one:\n {body}"
);
assert!(body.contains("\"period\":\"month\""), "{body}");
assert!(
body.contains("\"delta\":{\"pack_bytes\":10737418240}"),
"the delta is passed through as numbers for the console's own formatter:\n {body}"
);
assert_eq!(
*calls.lock().expect("the call log"),
vec!["Purchase.Bundles".to_owned()],
"one call, and no quote behind it: `can_serve` on the row IS the quote"
);
}
/// ★ **A bundle the server would refuse is not rendered.** Not its id, not
/// its label, not its price and not the sentence monetize put under it —
/// nothing of it reaches the browser. The one thing the browser is told is
/// that a row was withheld, so an empty window can be told from an
/// undeclared one. The servable row beside it survives untouched: a fix
/// that hid the dead cards and the live one would be worse than the bug.
#[tokio::test]
async fn a_bundle_monetize_cannot_serve_is_not_in_the_window() {
let (router, _) = console(FakeMonetize::serving()).await;
let (status, body) = body_of(router, "/api/namespaces/alpha/increase/bundles").await;
assert_eq!(status, http::StatusCode::OK, "{body}");
for dead in ["100gb-1m", "100.0 GiB more room", "2200.00 SEK", BUNDLE_REFUSAL] {
assert!(
!body.contains(dead),
"a bundle this box cannot serve reached the browser ({dead:?}):\n {body}"
);
}
assert!(!body.contains("\"can_serve\":false"), "a refused row was sent:\n {body}");
assert!(body.contains("\"withheld\":1"), "the browser was not told a row was left out:\n {body}");
assert!(body.contains(BUNDLE_ID), "the servable row must survive the filter:\n {body}");
assert!(!body.contains("unavailable"), "a window with an offer in it has no closing line:\n {body}");
}
/// ★ **A window monetize is still reading is `pending`, with the line for
/// the spinner and the one for after a minute — never an empty answer.**
///
/// test2, 2026-10-05: the owner's first visit after a night's idle drew
/// the typed order, because `{"offers":[],"withheld":0}` is what an unread
/// window and an undeclared one both looked like. The words are this
/// server's, so the console writes none.
#[tokio::test]
async fn a_window_monetize_is_still_reading_is_pending_with_the_servers_lines() {
let (router, _) = console(FakeMonetize { reading: true, ..FakeMonetize::serving() }).await;
let (status, body) = body_of(router, "/api/namespaces/alpha/increase/bundles").await;
assert_eq!(status, http::StatusCode::OK, "{body}");
let w: serde_json::Value = serde_json::from_str(&body).expect("json");
assert_eq!(w["pending"], AWAITING_PROVIDER, "{body}");
assert_eq!(w["late"], PROVIDER_LATE, "{body}");
assert_eq!(w["offers"], serde_json::json!([]), "{body}");
assert!(w.get("unavailable").is_none(), "an unread window is not \"nothing fits\": {body}");
}
/// And an answer is not pending: the two lines appear ONLY when monetize
/// says the window is still being read.
#[tokio::test]
async fn an_answered_window_carries_no_pending_line() {
let (router, _) = console(FakeMonetize::serving()).await;
let (_, body) = body_of(router, "/api/namespaces/alpha/increase/bundles").await;
assert!(!body.contains("pending") && !body.contains("late"), "{body}");
}
/// ★ **A window whose every row is refused says so in one line** — and the
/// line is not about maintenance. Two cards over a false sentence was the
/// bug; a blank panel that looks like a loading failure would be the next
/// one.
#[tokio::test]
async fn a_window_with_nothing_servable_says_so_in_one_line() {
let refused = mpb::Bundle {
id: "100gb-1m".to_owned(),
label: "100.0 GiB more room".to_owned(),
price: "2200.00 SEK".to_owned(),
period: "month".to_owned(),
delta: [("pack_bytes".to_owned(), 107_374_182_400u64)].into(),
can_serve: false,
reason: "We cannot add more space to your account right now while we carry out \
maintenance. Nothing has been charged — please try again later."
.to_owned(), ..Default::default()
};
let (router, _) = console(FakeMonetize {
can_serve: true,
declared: Some(vec![refused.clone(), mpb::Bundle { id: "50gb-1m".to_owned(), ..refused }]),
..Default::default()
})
.await;
let (status, body) = body_of(router, "/api/namespaces/alpha/increase/bundles").await;
assert_eq!(status, http::StatusCode::OK, "{body}");
assert!(body.contains("\"offers\":[]"), "{body}");
assert!(body.contains("\"withheld\":2"), "{body}");
assert!(body.contains(NOTHING_FITS), "the one line is missing:\n {body}");
assert!(
!body.to_ascii_lowercase().contains("maintenance"),
"monetize's masked sentence leaked past the filter:\n {body}"
);
}
/// A deployment that declared no bundles is not a shop with no room: the
/// window is empty, nothing was withheld, and there is no line — the panel
/// draws the typed order, exactly as before.
#[tokio::test]
async fn a_window_nobody_declared_is_empty_and_says_nothing() {
let (router, _) = console(FakeMonetize {
can_serve: true,
declared: Some(Vec::new()),
..Default::default()
})
.await;
let (status, body) = body_of(router, "/api/namespaces/alpha/increase/bundles").await;
assert_eq!(status, http::StatusCode::OK, "{body}");
assert_eq!(body, "{\"offers\":[],\"withheld\":0}", "{body}");
}
/// ★ **A pressed button is the typed order, one field over.**
///
/// ★ **g-receipts: an order placed by the OPERATOR names the operator's
/// address as its payer, so Stripe mails the order's receipt.** MEASURED by
/// the release-3 proof runner 2026-09-29: every order's checkout asked
/// `receipt_email = None` — this console sent `payer: ""` on every order
/// (here, `start_bundle`, `start`, and the subscription's renew/buy/raise),
/// and monetize's own fallback (`email_of`) knows only a namespace a paid
/// signup created, never the operator's. A signed-in tenant still sends
/// nothing: monetize names the address they signed up with.
#[tokio::test]
async fn an_operators_order_names_the_operators_receipt_address_and_nothing_is_invented() {
let fake = FakeMonetize::serving();
let payers = fake.payers.clone();
let (router, _) = console_receipts_to(fake, Some("operator@example.com")).await;
let (status, body) = request(router, http::Method::POST, "/api/namespaces/alpha/increase/bundle", None, &format!("{{\"bundle\":\"{BUNDLE_ID}\"}}")).await;
assert_eq!(status, http::StatusCode::OK, "{body}");
assert_eq!(*payers.lock().expect("the payer log"), vec!["operator@example.com".to_owned()], "the open console is the operator: the receipt goes to the operator");
// No address fed: nothing invented — monetize's fallback decides.
let fake = FakeMonetize::serving();
let payers = fake.payers.clone();
let (router, _) = console_receipts_to(fake, None).await;
let (status, body) = request(router, http::Method::POST, "/api/namespaces/alpha/increase/bundle", None, &format!("{{\"bundle\":\"{BUNDLE_ID}\"}}")).await;
assert_eq!(status, http::StatusCode::OK, "{body}");
assert_eq!(*payers.lock().expect("the payer log"), vec![String::new()]);
// A fed value that is not an address is not sent to a payment vendor.
let fake = FakeMonetize::serving();
let payers = fake.payers.clone();
let (router, _) = console_receipts_to(fake, Some("not an address")).await;
let _ = request(router, http::Method::POST, "/api/namespaces/alpha/increase/bundle", None, &format!("{{\"bundle\":\"{BUNDLE_ID}\"}}")).await;
assert_eq!(*payers.lock().expect("the payer log"), vec![String::new()]);
}
/// ★ **A vendor link the browser may not be sent to never reaches it**
/// (stinky #19): the operator's order is checked as the tenant's is.
#[tokio::test]
async fn a_checkout_link_that_is_not_https_never_reaches_the_browser() {
for bad in ["http://checkout.example/pay", "javascript:alert(1)", "//checkout.example/pay"] {
let (router, _) = console(FakeMonetize { checkout_url: bad, ..FakeMonetize::serving() }).await;
let (status, body) = request(
router,
http::Method::POST,
"/api/namespaces/alpha/increase/bundle",
None,
&format!("{{\"bundle\":\"{BUNDLE_ID}\"}}"),
)
.await;
assert_eq!(status, http::StatusCode::BAD_GATEWAY, "{bad}: {body}");
assert!(!body.contains(bad), "{bad}: {body}");
}
}
/// `bundle` where `caps` would be and NOTHING ELSE changed: the same
/// `Purchase.Start`, the same reference and the same vendor link. The caps assertion is the load-bearing one — monetize
/// refuses a request naming both rather than letting one win silently, so a
/// console that sent an id beside a cap map would be refused in production
/// while passing any test that only checked the id.
#[tokio::test]
async fn a_pressed_offer_orders_by_id_and_names_no_caps() {
let fake = FakeMonetize::serving();
let ordered = fake.ordered.clone();
let (router, calls) = console(fake).await;
let (status, body) = request(
router.clone(),
http::Method::POST,
"/api/namespaces/alpha/increase/bundle",
None,
&format!("{{\"bundle\":\"{BUNDLE_ID}\"}}"),
)
.await;
assert_eq!(status, http::StatusCode::OK, "{body}");
assert_eq!(
*ordered.lock().expect("the order log"),
vec![(BUNDLE_ID.to_owned(), false)],
"the order must name the bundle and no caps"
);
assert!(
body.contains(CHECKOUT_URL),
"the vendor's own link did not reach the browser:\n {body}"
);
assert_eq!(
*calls.lock().expect("the call log"),
vec!["Purchase.Start".to_owned()],
"no second door and no extra quote"
);
// And it lands in the SAME poll the typed order uses: the reference was
// remembered, so following it is allowed.
let (status, followed) = body_of(
router,
"/api/namespaces/alpha/increase/status?reference=gunnar/alpha/2026-09-07",
)
.await;
assert_ne!(
status,
http::StatusCode::NOT_FOUND,
"an order placed from the window is not one this console admits placing:\n {followed}"
);
}
/// ★ **g-dk-market / g-currencies (owner 2026-09-28): the customer's
/// market reaches monetize** — the window and the order carry `market`
/// (SE, FI, DK), so a Dane is priced and charged in DKK and a Finn in
/// EUR; none named is monetize's default, and a market nobody sells in is
/// refused here by name before monetize is dialled.
#[tokio::test]
async fn the_customers_market_reaches_monetize_and_an_unknown_one_is_refused() {
let fake = FakeMonetize::serving();
let markets = fake.markets.clone();
let (router, _calls) = console(fake).await;
let (status, body) = body_of(router.clone(), "/api/namespaces/alpha/increase/bundles?market=FI").await;
assert_eq!(status, http::StatusCode::OK, "{body}");
let (status, body) = request(router.clone(), http::Method::POST, "/api/namespaces/alpha/increase/bundle", None, &format!("{{\"bundle\":\"{BUNDLE_ID}\",\"market\":\"DK\"}}")).await;
assert_eq!(status, http::StatusCode::OK, "{body}");
let (status, _) = request(router.clone(), http::Method::POST, "/api/namespaces/alpha/increase/bundle", None, &format!("{{\"bundle\":\"{BUNDLE_ID}\"}}")).await;
assert_eq!(status, http::StatusCode::OK);
assert_eq!(*markets.lock().unwrap(), vec![mpb::Market::Fi as i32, mpb::Market::Dk as i32, mpb::Market::Unspecified as i32]);
// A TYPED order names its market too (2026-10-05: a 12-month plan is
// raised by a typed size, in the buyer's market).
let (status, body) = request(router.clone(), http::Method::POST, "/api/namespaces/alpha/increase", None, r#"{"meter":"pack_bytes","cap":21474836480,"market":"FI"}"#).await;
assert_eq!(status, http::StatusCode::OK, "{body}");
assert_eq!(markets.lock().unwrap().last(), Some(&(mpb::Market::Fi as i32)));
let (status, body) = request(router, http::Method::POST, "/api/namespaces/alpha/increase/bundle", None, &format!("{{\"bundle\":\"{BUNDLE_ID}\",\"market\":\"US\"}}")).await;
assert_eq!(status, http::StatusCode::BAD_REQUEST, "{body}");
assert!(body.contains("US"), "{body}");
}
/// ★ **One rule from a browser to a market** (wiring tracer B 2026-10-08): where the page names none,
/// the server reads the browser's languages, q-ranked (the sign-up ladder's rule), and the window says
/// the market it was priced in, so the order echoes it; a page's own word still wins; a browser naming
/// no market is monetize's default and the window names none.
#[tokio::test]
async fn the_browsers_languages_name_the_market_where_the_page_names_none() {
let fake = FakeMonetize::serving();
let markets = fake.markets.clone();
let (router, _calls) = console(fake).await;
let ask = |method: http::Method, uri: &str, languages: &str, body: String| {
let mut b = http::Request::builder().method(method).uri(uri).header(http::header::CONTENT_TYPE, "application/json").header(http::header::ORIGIN, "http://127.0.0.1").header(http::header::ACCEPT_LANGUAGE, languages);
if body.is_empty() {
b = b.header(http::header::CONTENT_LENGTH, "0");
}
b.body(axum::body::Body::from(body)).expect("a request")
};
let read = |r: axum::response::Response| async move {
let status = r.status();
let bytes = axum::body::to_bytes(r.into_body(), 1 << 20).await.expect("a body");
(status, String::from_utf8_lossy(&bytes).to_string())
};
// A Dane on an English browser: `da` is ranked below English and still names the market.
let (status, body) = read(router.clone().oneshot(ask(http::Method::GET, "/api/namespaces/alpha/increase/bundles", "en-US,en;q=0.9,da;q=0.8", String::new())).await.expect("answers")).await;
assert_eq!(status, http::StatusCode::OK, "{body}");
assert!(body.contains(r#""market":"DK""#), "the window names the market it was priced in: {body}");
// The order from that window, from a Swedish-speaking Finn's browser, the page naming none: FI.
let (status, body) = read(router.clone().oneshot(ask(http::Method::POST, "/api/namespaces/alpha/increase/bundle", "sv-FI", format!("{{\"bundle\":\"{BUNDLE_ID}\"}}"))).await.expect("answers")).await;
assert_eq!(status, http::StatusCode::OK, "{body}");
// The page's own word wins over the browser's languages.
let (status, _) = read(router.clone().oneshot(ask(http::Method::POST, "/api/namespaces/alpha/increase/bundle", "da-DK", format!("{{\"bundle\":\"{BUNDLE_ID}\",\"market\":\"SE\"}}"))).await.expect("answers")).await;
assert_eq!(status, http::StatusCode::OK);
// A browser that names no market: monetize's default, and the window names none.
let (status, body) = read(router.clone().oneshot(ask(http::Method::GET, "/api/namespaces/alpha/increase/bundles", "en-GB,de;q=0.5", String::new())).await.expect("answers")).await;
assert_eq!(status, http::StatusCode::OK, "{body}");
assert!(!body.contains(r#""market""#), "{body}");
assert_eq!(*markets.lock().unwrap(), vec![mpb::Market::Dk as i32, mpb::Market::Fi as i32, mpb::Market::Se as i32, mpb::Market::Unspecified as i32]);
}
/// A console with a passkey subsystem, so `require_auth` runs the real
/// ladder instead of the open-on-loopback bypass, and sessions can be
/// minted for it.
async fn gated_console(
fake: FakeMonetize,
) -> (
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();
tokio::spawn(async move {
tonic::transport::Server::builder()
.add_service(ProductsServer::new(fake.clone()))
.add_service(PurchaseServer::new(fake))
.serve_with_incoming(tokio_stream::wrappers::TcpListenerStream::new(listener))
.await
});
let dir = tempfile::tempdir().expect("a temp dir").keep();
let store = crate::state::StateStore::open(dir.join("state.redb")).expect("a store");
let webauthn = Arc::new(
crate::auth::Webauthn::new(
crate::auth::RelyingParty {
rp_id: "console.example".to_owned(),
origin: "https://console.example".to_owned(),
rp_name: "gunnar".to_owned(),
},
store.clone(),
)
.expect("a subsystem"),
);
let signer = Arc::new(
crate::signer::Signer::new(ssh_key::PrivateKey::from(
ssh_key::private::Ed25519Keypair::from_seed(&[9u8; 32]),
))
.expect("a fixed seed makes a signer"),
);
let router = crate::serve::router(Arc::new(crate::app::App {
webauthn: Some(webauthn.clone()),
admin_token: None,
monetize: Some(
crate::monetize::Monetize::connect_lazy(&format!("http://{addr}"), BEARER, "gunnar")
.expect("a lazy channel"),
),
events: crate::sse::EventHub::detached(),
gunnar: crate::grpc::Gunnar::connect_lazy("127.0.0.1:1", signer)
.expect("a lazy handle"),
twin: None,
guest: None,
reviewers: Arc::new(crate::reviewer::ReviewerKeys::new(
"127.0.0.1:1".to_owned(),
store.clone(),
)),
tenants: Arc::new(crate::tenant::TenantKeys::new(
"127.0.0.1:1".to_owned(),
store,
)),
forges: Default::default(),
imports: Default::default(),
forge_apps: Default::default(),
ui_dir: None,
caches: crate::api::Caches::default(),
objects: Default::default(),
readmes: Default::default(),
}));
(router, webauthn, calls)
}
/// One request, with an optional session cookie.
async fn request(
router: axum::Router,
method: http::Method,
uri: &str,
cookie: Option<&str>,
body: &str,
) -> (http::StatusCode, String) {
let mut builder = http::Request::builder()
.method(method)
.uri(uri)
.header(http::header::CONTENT_TYPE, "application/json")
// The CSRF guard reads the origin on a mutation; the console's own
// origin is what a real browser sends.
.header(http::header::ORIGIN, "https://console.example");
if let Some(cookie) = cookie {
builder = builder.header(
http::header::COOKIE,
format!("{}={cookie}", crate::auth::SESSION_COOKIE),
);
}
let response = router
.oneshot(
builder
.body(axum::body::Body::from(body.to_owned()))
.expect("a request"),
)
.await
.expect("the router answers");
let status = response.status();
let bytes = axum::body::to_bytes(response.into_body(), 1 << 20)
.await
.expect("a body");
(status, String::from_utf8_lossy(&bytes).into_owned())
}
/// **An order is an admin's, and the floor is the one every other route
/// uses.**
///
/// monetize's own console floors every money mutation at admin
/// (`monetize-ui-server/src/auth.rs`: "every mutation under `/api/` is an
/// admin's — an order, a refund … and all of them are money"), and this
/// console must not be the softer door to the same transaction.
///
/// The floor is `role_floor`'s and nothing bespoke: `/api/namespaces` is
/// already an ADMIN surface there, so putting the route under it is what
/// buys the rule. **ZERO calls** is the second half and the one that
/// matters: a refusal that had already reached monetize would have named a
/// tenant, and an attempt refused after the fact is still an attempt.
#[tokio::test]
async fn a_viewer_cannot_place_an_order_and_monetize_never_hears_of_it() {
let (router, webauthn, calls) = gated_console(FakeMonetize::serving()).await;
let viewer = webauthn.make_session("viewer-vera", crate::role::Role::Viewer, true);
let (status, body) = request(
router,
http::Method::POST,
"/api/namespaces/alpha/increase",
Some(&viewer),
r#"{"meter":"pack_bytes","cap":21474836480}"#,
)
.await;
assert_eq!(
status,
http::StatusCode::FORBIDDEN,
"a viewer placed an order: {body}"
);
assert!(
body.contains("admin"),
"the refusal must name the role it needs: {body}"
);
assert!(
calls.lock().expect("the call log").is_empty(),
"monetize was told about an order this console had already refused: {:?}",
calls.lock().expect("the call log")
);
}
/// **An OPERATOR is not enough either.** monetize floors every money
/// mutation at admin on its own console; the softer door must not be here.
#[tokio::test]
async fn an_operator_cannot_place_an_order_either() {
let (router, webauthn, calls) = gated_console(FakeMonetize::serving()).await;
let operator = webauthn.make_session("operator-olof", crate::role::Role::Operator, true);
let (status, body) = request(
router,
http::Method::POST,
"/api/namespaces/alpha/increase",
Some(&operator),
r#"{"meter":"pack_bytes","cap":21474836480}"#,
)
.await;
assert_eq!(
status,
http::StatusCode::FORBIDDEN,
"an operator placed an order: {body}"
);
assert!(calls.lock().expect("the call log").is_empty());
}
/// The same door from the other side: an admin gets through, so the test
/// above is a floor and not a wall.
#[tokio::test]
async fn an_admin_places_the_order_and_gets_the_vendors_own_link() {
let (router, webauthn, calls) = gated_console(FakeMonetize::serving()).await;
let admin = webauthn.make_session("admin-alva", crate::role::Role::Admin, true);
let (status, body) = request(
router,
http::Method::POST,
"/api/namespaces/alpha/increase",
Some(&admin),
r#"{"meter":"pack_bytes","cap":21474836480}"#,
)
.await;
assert_eq!(status, http::StatusCode::OK, "{body}");
assert!(
body.contains(CHECKOUT_URL),
"the console must carry the vendor's own link, unaltered: {body}"
);
assert!(body.contains("\"state\":\"pending\""), "{body}");
assert!(
!body.contains(&COST_MINOR.to_string()),
"the provider's cost reached the browser through Start: {body}"
);
assert!(
!body.contains("signature") && !body.contains("fact"),
"the signed entitlement fact is monetize's and gunnar's, not the browser's: {body}"
);
assert_eq!(
*calls.lock().expect("the call log"),
vec!["Purchase.Start".to_owned()]
);
}
/// **A visitor with no session reaches nothing, and monetize is not dialled
/// on their behalf.**
///
/// Both halves matter. The status is the middleware's; the empty call log is
/// the promise that no credential of this console's was spent answering a
/// stranger — the same property `crate::anonymous` holds for the gunnar
/// side, now for the one that costs money.
#[tokio::test]
async fn an_anonymous_visitor_reaches_no_money_route() {
for (method, uri) in [
(http::Method::GET, "/api/namespaces/alpha/increase"),
(
http::Method::GET,
"/api/namespaces/alpha/increase/offer?meter=pack_bytes&cap=1",
),
(
http::Method::GET,
"/api/namespaces/alpha/increase/status?reference=gunnar/alpha/2026-09-07",
),
(http::Method::POST, "/api/namespaces/alpha/increase"),
] {
let (router, _, calls) = gated_console(FakeMonetize::serving()).await;
let (status, body) = request(router, method.clone(), uri, None, "{}").await;
assert_eq!(
status,
http::StatusCode::UNAUTHORIZED,
"{method} {uri} answered a stranger: {body}"
);
assert!(
calls.lock().expect("the call log").is_empty(),
"{method} {uri} reached monetize for a caller with no session"
);
}
}
/// ★ **monetize scopes the follow, and nothing is remembered here** (thin
/// layer, owner 2026-10-04). An order placed before a restart — or by
/// monetize's own console — is asked about with the tenant in the path
/// named, so monetize answers only that tenant's attempt. Red before: an
/// in-memory ring of the orders this console placed was the gate, so this
/// 404'd without asking and `Purchase.Status` went out unscoped.
#[tokio::test]
async fn an_operators_follow_is_scoped_by_monetize_to_the_namespace() {
let (router, webauthn, calls) = gated_console(FakeMonetize::serving()).await;
let admin = webauthn.make_session("admin-alva", crate::role::Role::Admin, true);
let (status, body) = request(
router,
http::Method::GET,
"/api/namespaces/alpha/increase/status?reference=gunnar%2Falpha%2F2019-01-01",
Some(&admin),
"",
)
.await;
// The fake answers an in-scope Status UNIMPLEMENTED: the assertion is
// that the console asked, scoped, and monetize decided.
assert_eq!(status, http::StatusCode::NOT_IMPLEMENTED, "{body}");
assert!(
calls
.lock()
.expect("the call log")
.contains(&"scoped gunnar alpha gunnar/alpha/2019-01-01".to_owned()),
"monetize was not asked with the namespace named: {:?}",
calls.lock().expect("the call log")
);
}
/// And the order it DID place is followed, which is the half that keeps the
/// check from being a route that always refuses.
#[tokio::test]
async fn the_order_this_console_placed_is_followed() {
let (router, webauthn, calls) = gated_console(FakeMonetize::serving()).await;
let admin = webauthn.make_session("admin-alva", crate::role::Role::Admin, true);
let (status, placed) = request(
router.clone(),
http::Method::POST,
"/api/namespaces/alpha/increase",
Some(&admin),
r#"{"meter":"pack_bytes","cap":21474836480}"#,
)
.await;
assert_eq!(status, http::StatusCode::OK, "{placed}");
let reference = serde_json::from_str::<serde_json::Value>(&placed).expect("JSON")
["reference"]
.as_str()
.expect("a reference")
.to_owned();
let (status, body) = request(
router,
http::Method::GET,
&format!(
"/api/namespaces/alpha/increase/status?reference={}",
urlencoding(&reference)
),
Some(&admin),
"",
)
.await;
// The fake does not implement `Status` — it records the call and
// answers Unimplemented — so the assertion is that the console got past
// its OWN gate and asked, which is the thing this commit changed. What
// monetize then says is monetize's.
assert_eq!(status, http::StatusCode::NOT_IMPLEMENTED, "{body}");
assert!(
calls
.lock()
.expect("the call log")
.contains(&"Purchase.Status".to_owned()),
"the order this console placed itself was not followed"
);
}
/// ★ **A namespace's offer, bundle window and order status are an
/// administrator's to READ too** (thin layer, owner 2026-10-04). Red before
/// this row: the three GETs sat at the viewer floor with the tenant taken
/// from the path, so any customer or employee asked monetize — on this
/// console's one bearer — for ANOTHER tenant's caps, the operator's reason
/// and the window priced for them: "a read-only enumeration of the whole
/// customer list", in monetize's own words. A customer's own door is
/// `/api/account/subscription`, which monetize scopes by the appliance's
/// ticket. monetize must hear nothing for a viewer or an operator.
#[tokio::test]
async fn a_viewer_or_operator_reads_no_namespaces_offer_window_or_status() {
let (router, webauthn, calls) = gated_console(FakeMonetize::serving()).await;
let viewer = webauthn.make_session("viewer-vera", crate::role::Role::Viewer, true);
let operator = webauthn.make_session("operator-olof", crate::role::Role::Operator, true);
let admin = webauthn.make_session("admin-alva", crate::role::Role::Admin, true);
let reads = [
"/api/namespaces/alpha/increase/offer?meter=pack_bytes&cap=21474836480",
"/api/namespaces/alpha/increase/bundles",
"/api/namespaces/alpha/increase/status?reference=gunnar%2Falpha%2F2026-10-04",
];
for (who, cookie) in [("viewer", &viewer), ("operator", &operator)] {
for uri in reads {
let (status, body) =
request(router.clone(), http::Method::GET, uri, Some(cookie), "").await;
assert_eq!(status, http::StatusCode::FORBIDDEN, "{who} {uri}: {body}");
}
}
assert!(
calls.lock().expect("the call log").is_empty(),
"monetize was asked on behalf of a viewer or an operator: {:?}",
calls.lock().expect("the call log")
);
let (status, body) = request(router, http::Method::GET, reads[1], Some(&admin), "").await;
assert_eq!(status, http::StatusCode::OK, "the administrator: {body}");
}
/// Percent-encode the slashes in a reference, which is what a browser does.
fn urlencoding(reference: &str) -> String {
reference.replace('/', "%2F")
}
/// **A reference belongs to the namespace in its own name, and monetize
/// will not answer one that does not.**
///
/// A reference is `<product>/<tenant>/<date>` — a guessable string. Since
/// monetize 3b44e7d `Purchase.Status` takes the tenant, and the console
/// names the one in the path: another namespace's attempt is monetize's
/// uniform NOT_FOUND (thin layer, owner 2026-10-04).
#[tokio::test]
async fn a_reference_from_another_namespace_is_not_followed() {
let (router, webauthn, calls) = gated_console(FakeMonetize::serving()).await;
let admin = webauthn.make_session("admin-alva", crate::role::Role::Admin, true);
let (status, body) = request(
router,
http::Method::GET,
"/api/namespaces/alpha/increase/status?reference=gunnar%2Fbeta%2F2026-09-07",
Some(&admin),
"",
)
.await;
// monetize's one uniform refusal: it names neither owner nor
// reference, and the call carried the namespace that scopes it.
assert_eq!(status, http::StatusCode::NOT_FOUND, "{body}");
assert!(!body.contains("beta"), "{body}");
assert!(
calls
.lock()
.expect("the call log")
.contains(&"scoped gunnar alpha gunnar/beta/2026-09-07".to_owned()),
"{:?}",
calls.lock().expect("the call log")
);
}
}