crates/gunnar-ui-server/src/api/subscription.rs
//! **A customer renews their own subscription, themselves.**
//!
//! Until this module existed, a lapse was an operator's job: somebody with an
//! admin passkey placed the order on the tenant's behalf, by hand, every time.
//! `Transaction::renewal` and `Purchase.Start{renew}` have worked for a while —
//! the arithmetic, the pricing on the standing cap, the Retention boundary. The
//! only thing missing was that nothing but an operator could call them.
//!
//! # Why this is not a route under `/api/namespaces/{name}/`
//!
//! That is where [`crate::api::increase`] lives, and it is the right home for
//! an order an OPERATOR places on a tenant's behalf. It is the wrong home for
//! this, and the reason is written out in `monetize_site_buy::relay::operator_status` (was `increase::status`):
//!
//! > *"BOTH halves of the comparison are chosen by the caller — the reference
//! > in the query and the namespace in the path — so anybody who could name
//! > `gunnar/beta/…` could also ask for it under `/api/namespaces/beta/`."*
//!
//! A tenant surface that took a namespace in the path would have to compare it
//! against the session on every call, and **every such comparison is a place to
//! leak**: by a refusal that names what it found, by a 404 that differs from a
//! 403, by the time one takes. The rule this lane was given is that every
//! refusal a tenant can trigger must say no more than a stranger is entitled to
//! know.
//!
//! So there is **no namespace anywhere in the request** — not in the path, not
//! in a query, not in a body. It comes from the session's pinned principal and
//! from nowhere else. A tenant cannot *spell* another namespace here, so there
//! is no other-namespace request to make, no enumeration to bound and no
//! differential refusal to tune. The property is structural rather than
//! checked.
//!
//! That is `/api/account`'s own argument, and this module sits under that
//! prefix deliberately: `role_floor` puts it at `Viewer` (the tenant rung), and
//! `auth::middleware` already says why that is safe — *"every handler under
//! this prefix acts on the name in the SESSION, which no path segment, query
//! parameter or body field can change, so there is nothing to escalate to."*
//!
//! # What gunnar verifies, and why the console's word is not enough
//!
//! This console holds a **server-wide admin grant** at gunnar. If it alone
//! decided "this session may only touch namespace X", a routing mistake here
//! would be full admin over every customer on the box.
//!
//! So it does not decide. [`ticket`] asks gunnar to mint one, over a credential
//! signed with **that human's own key** — the per-user key
//! [`crate::tenant::TenantKeys`] enrolled on their own gunnar principal, the
//! same one their reads are already answered with. gunnar checks the namespace
//! against its own grant table before a ticket exists:
//!
//! ```text
//! ctx.require(&caller, Access::namespace(&ns), GrantLevel::Admin, "Billing.MintActorTicket")
//! ```
//!
//! A bug in this crate that passed the wrong namespace would be refused by the
//! appliance, because the appliance is checking the key that signed the call
//! and not the string this process sent. What it does **not** defend against is
//! a wholly compromised console — this process holds every enrolled tenant's
//! private half, as [`crate::tenant`] states plainly — but a compromised
//! console already holds a server-wide admin key, so that is no new reach.
//!
//! # The upgrade hazard, and why this module refuses rather than degrades
//!
//! proto3 drops unknown fields. A NEW console talking to an OLD monetize would
//! send an `actor_ticket` that is thrown away — **and the renewal would succeed
//! anyway, with no tenant check performed**, while this console believed the
//! tenant path was being enforced. The failure is invisible from here and looks
//! exactly like success.
//!
//! So nothing here infers enforcement from a version, a build flag or a call
//! that came back 200. the shared relay's `offered` (monetize-site-buy) reads `ProductInfo.tenant_renewal` — a bool
//! monetize sets only when it can actually verify a ticket — and when it is
//! false **the route answers as though it did not exist**. Not a disabled
//! button, not a degraded path that still charges the card. An old monetize
//! leaves the field false by omission, which is the safe direction.
//!
//! # Following an attempt across a restart
//!
//! [`crate::api::increase`] remembers every reference it mints in a
//! process-global ring, and forgets them all on restart; its `status` then 404s
//! and points the caller at monetize's own console. For an operator that is an
//! inconvenience. For a **customer who has just been charged** it is a dead end,
//! because monetize's console is not a place a tenant can reach.
//!
//! [`status`] therefore remembers nothing. A reference is
//! `<product>/<tenant>/<date>`, and the tenant is this session's own namespace
//! — so the check is re-derived from identity on every call and survives a
//! restart. The comparison `monetize_site_buy::relay::operator_status` (was `increase::status`) calls decorative is **decisive**
//! here, and the difference is exactly one thing: there, both halves came from
//! the caller; here, one half comes from the session's pinned principal, which
//! no part of the request can change.
use axum::extract::State;
use axum::Json;
use serde::Serialize;
use crate::api::{ApiError, ApiResult};
use crate::app::AppState;
use crate::pb;
/// **Whose subscription this request is about, decided before a handler exists
/// to forget.**
///
/// An extractor and not a helper, for the reason [`crate::api::account::Me`]
/// and `buy_site::GunnarSite::operator` (was `increase::Ordering`) are: a helper is something a handler can forget to
/// call, and forgetting here spends somebody else's money on somebody else's
/// account.
///
/// **The namespace is not in the request.** It is the gunnar principal the
/// session's passkey was pinned to at registration
/// (`auth::routes::bind_a_principal`), which is the one moment a name was
/// anybody's to give, and on gunnar a principal's id *is* the namespace it owns
/// (`AuthTable::owns_account`). Nothing a caller can send changes it.
#[derive(Debug, Clone)]
pub struct OwnSubscription {
/// The session's own namespace, which is monetize's `tenant`.
pub namespace: String,
/// The console user, for the audit line.
pub who: String,
/// Whom Stripe mails the receipt (g-receipts; [`crate::monetize::Monetize::payer_for`]).
pub payer: String,
}
impl axum::extract::FromRequestParts<AppState> for OwnSubscription {
type Rejection = ApiError;
async fn from_request_parts(
parts: &mut http::request::Parts,
state: &AppState,
) -> Result<OwnSubscription, ApiError> {
let who = match parts.extensions.get::<crate::auth::middleware::Caller>() {
Some(crate::auth::middleware::Caller::Operator(session)) => session.user.clone(),
// **The break-glass token is not a person.** It is held by whoever
// runs the process, it is named after nobody, and there is no
// principal it could be the owner of — so there is no subscription
// that is "its own". An operator with a token who wants to renew
// somebody's plan has `/api/namespaces/{name}/increase`, which is
// the surface for acting on a tenant's behalf. `api::account::Me`
// refuses it for the same reason.
Some(crate::auth::middleware::Caller::AdminToken) => {
return Err(refuse(
"permissiondenied",
"the break-glass token is not a person and holds no plan of its own",
))
}
// Includes the open-console posture (`trunk serve` on loopback),
// where every request runs as admin and still has no name. An
// unnamed caller has no account, so the honest answer is the same
// refusal rather than a guess.
None => {
return Err(refuse(
"permissiondenied",
"this needs a session that can be named: a subscription belongs to whoever \
is logged in, and nobody is",
))
}
};
let Some(namespace) = state
.webauthn
.as_ref()
.and_then(|webauthn| webauthn.principal_of(&who))
else {
// A passkey bound to no principal: somebody invited to look at
// public repositories, or a credential from before the binding
// existed. They hold no namespace, so there is nothing of theirs to
// renew — and this console must NOT resolve a name to find one,
// which would be GUI-001 living on in a quieter place.
return Err(refuse(
"notfound",
"this sign-in belongs to no account here, so there is no \
plan of its own to renew",
));
};
let caller = parts.extensions.get::<crate::auth::middleware::Caller>();
// **A member buys nothing** (owner 2026-10-07: a customer is an
// ACCOUNT that pays; its members use its space and ask its admin for
// more). gunnar says who is a member (`Access.WhoAmI`), and refuses a
// member's ticket by name besides (`Billing.MintActorTicket`): this
// only says it before a quote, an order or a read is attempted.
let member = crate::auth::middleware::member(state, caller)
.await
.map_err(|why| refuse("unavailable", &why))?;
if let Some(member) = member {
return Err(refuse(
"permissiondenied",
&format!(
"{} A member buys nothing: {}'s room is its admin's to buy.",
member.members_line(),
member.account
),
));
}
let payer = state.monetize.as_ref().map(|m| m.payer_for(caller)).unwrap_or_default();
Ok(OwnSubscription { namespace, who, payer })
}
}
fn refuse(code: &str, message: &str) -> ApiError {
ApiError(crate::grpc::Fail {
code: code.to_owned(),
message: message.to_owned(),
})
}
/// **The caller's OWN control-plane channel**, signed with the key
/// [`crate::tenant::TenantKeys`] enrolled on their own gunnar principal.
///
/// One function because three handlers need it and every one of them would
/// otherwise be a place to reach for `state.gunnar` — this console's
/// server-wide admin channel — by accident. That substitution compiles, passes
/// every test that does not inspect the credential, and undoes the entire
/// design; `api::subscription`'s starred test exists because of it.
///
/// **There is no fallback here and there must never be one.** `api::Reader`
/// says why in the same words: the fallback IS the hole.
async fn tenant_channel(
state: &AppState,
who: &str,
) -> Result<crate::tenant::TenantIdentity, ApiError> {
crate::api::person_identity(state, who)
.await
.map_err(|why| why.to_string())
.and_then(|held| held.ok_or_else(|| "this sign-in holds no key at Gunnar any more".to_owned()))
.map_err(|why| {
tracing::warn!(
target: crate::auth::AUDIT_TARGET,
event = "subscription.no_tenant_credential",
user = %who,
reason = %why,
"this console could not sign as the tenant, so it will not act for them"
);
refuse(
"permissiondenied",
"Gunnar could not act as your account just now; sign in again",
)
})
}
/// What another period would come to.
///
/// **No price and no cost**, for the reason [`crate::api::increase::Offer`]
/// gives: this console holds no currency literal and does no money arithmetic,
/// and the amount is shown by the vendor on the page the card is on.
pub use monetize_site_buy::purchase::Renewal;
/// Where the tenant is sent, and how this console follows what happens there.
pub use monetize_site_buy::purchase::{target_caps, Attempt, RoomLine, UsersLine};
// ── The plan this account is on ────────────────────────────────────────────
/// **What the appliance holds about this account's subscription.**
///
/// Read from GUNNAR and not from monetize, which is the whole point: gunnar
/// holds the last fact monetize pushed, its `Entitlement.Get` is
/// namespace-scoped, and the tenant's own key answers it — so the narrowing
/// happens on gunnar's side of the wire, where a mistake in this crate cannot
/// reach it. Asking monetize would have meant vendoring a client for
/// `Entitlement.Set` beside it and holding the answer on this console's word.
///
/// **No signature and no source.** `source` is `payment:<vendor>:<reference>`
/// — a vendor's name and a ledger key, which is operator vocabulary — and the
/// signature is monetize's and gunnar's business, not a browser's.
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct Plan {
/// monetize's word for the plan, verbatim.
pub plan: String,
/// `free` | `paid` | `grace` | `suspended` | `retention`, gunnar's ladder
/// in lower case — or `none` when gunnar holds no entitlement for the
/// namespace at all (an account that has bought nothing).
pub state: &'static str,
/// When the paid period ends. Absent for a plan with no period.
pub paid_until_unix_ms: Option<u64>,
/// The caps the fact carries, per meter.
pub caps: std::collections::BTreeMap<String, u64>,
}
/// **A Free account cannot push** (owner order 2026-10-18): on a console that
/// sells, a person who buys for their own account (gunnar's standing: an
/// individual or an account's admin, never a member) and whose account has no
/// plan — no fact, or gunnar's `Free` — can read and clone, not push. The
/// queue says so instead of "add your key so you can push", which was wrong
/// for them. Best-effort: anything unanswered says nothing.
pub(crate) async fn free_cannot_push(state: &AppState, caller: Option<&crate::auth::middleware::Caller>) -> bool {
if !state.sells_here() {
return false;
}
let Some(caller) = caller else {
return false;
};
let Ok(Some(standing)) = crate::auth::middleware::standing(state, Some(caller)).await else {
return false;
};
// Nothing a plan could change (the site's admin, a member): gunnar is asked nothing more.
if !free_gates_push(caller.role(), &standing, true) {
return false;
}
let Ok(identity) = tenant_channel(state, caller.name()).await else {
return false;
};
let plan_is_free = match identity
.gunnar
.entitlement()
.get(pb::GetEntitlementRequest { namespace: standing.account.clone() })
.await
{
Ok(answer) => match answer.into_inner().fact {
None => true,
Some(fact) => plan_state(fact.state) == "free",
},
// gunnar holds no fact for an account nothing was ever sold for.
Err(status) => status.code() == tonic::Code::NotFound,
};
free_gates_push(caller.role(), &standing, plan_is_free)
}
/// Pure: ★ ACCT-3 (owner 2026-10-29) — **does a Free plan gate this person's push?** Only a
/// person who buys for their own account (`standing`: an individual or an account's admin)
/// whose account holds no plan or gunnar's Free (`plan_is_free`) — and NEVER the site's admin
/// (`site_role`: gunnar's server-wide Admin grant, as `Access.WhoAmI` said it): a site admin is
/// never gated by an account plan. Every input is gunnar's answer; this only combines them.
pub(crate) fn free_gates_push(site_role: crate::role::Role, standing: &crate::auth::Standing, plan_is_free: bool) -> bool {
site_role != crate::role::Role::Admin && standing.may_buy() && !standing.account.is_empty() && plan_is_free
}
/// The queue's item for [`free_cannot_push`].
pub(crate) fn free_cannot_push_item() -> crate::api::actions::Action {
crate::api::actions::Action {
id: "free-cannot-push",
urgency: crate::api::actions::Urgency::Info,
title: "A Free account cannot push".to_owned(),
detail: "With a Free account you can sign in, read and clone, but not push. \
A plan gives your account room of its own to push to."
.to_owned(),
href: "/account#account-plan".to_owned(),
cta: "Get room".to_owned(),
}
}
/// gunnar's entitlement ladder as one lower-case word. An unrecognised value
/// is `unknown` rather than a guess.
/// **The money-shaped entries in the account's own åtgärder queue.**
///
/// Here rather than in [`crate::api::actions`] because the words are here: this
/// module is the one the open build does not contain, and keeping the sentences
/// about renewal and room inside it is what lets the queue itself stay free of
/// them and therefore stay scanned.
///
/// Best-effort like every other check in that queue: a source that cannot
/// answer contributes nothing. A person's whole console must not go dark
/// because the appliance was slow to answer one read.
///
/// Both thresholds are the owner's, 2026-09-18: warn at 85 % of the cap, and a
/// week before the period ends.
pub(crate) async fn actions_for(
state: &AppState,
user: &str,
items: &mut Vec<crate::api::actions::Action>,
) {
use crate::api::actions::{Action, Urgency};
let Some(namespace) = state
.webauthn
.as_ref()
.and_then(|webauthn| webauthn.principal_of(user))
else {
return;
};
let Ok(identity) = tenant_channel(state, user).await else {
return;
};
// ── the subscription ────────────────────────────────────────────────────
if let Ok(answer) = identity
.gunnar
.entitlement()
.get(pb::GetEntitlementRequest { namespace: namespace.clone() })
.await
{
let fact = answer.into_inner().fact.unwrap_or_default();
let now: u64 = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_millis() as u64)
.unwrap_or_default();
// A namespace nothing was ever sold for has no fact, and `free` is not
// a problem to be solved — a self-hoster and a customer between periods
// must not be told the same thing.
match plan_state(fact.state) {
"grace" => items.push(Action {
id: "plan-in-grace",
urgency: Urgency::Urgent,
title: "Your plan's paid time has ended and it is in its grace period".to_owned(),
detail: "The account still works. When the grace period ends it stops, and renewing now is the whole of what prevents that."
.to_owned(),
href: "/account".to_owned(),
cta: "Renew".to_owned(),
}),
"suspended" | "retention" => items.push(Action {
id: "plan-suspended",
urgency: Urgency::Urgent,
title: "This account is suspended".to_owned(),
detail: "Its data is still held. Prolonging brings it back; there is a limit to how long it is kept, and it is stated on your account page."
.to_owned(),
href: "/account".to_owned(),
cta: "Renew".to_owned(),
}),
"paid" if fact.paid_until_unix_ms.is_some_and(|t| t > 0) => {
const WEEK_MS: u64 = 7 * 24 * 60 * 60 * 1000;
// Saturating: a clock that has run past the paid-until is an
// account already in grace, which the arm above owns. Nothing
// here may underflow into "a very long time left".
let left = fact.paid_until_unix_ms.unwrap_or_default().saturating_sub(now);
if left > 0 && left <= WEEK_MS {
items.push(Action {
id: "plan-due-soon",
urgency: Urgency::Soon,
title: "Your plan's paid time ends within the week".to_owned(),
detail: "Prolonging before it does keeps the account out of its grace period entirely."
.to_owned(),
href: "/account".to_owned(),
cta: "Renew".to_owned(),
});
}
}
_ => {}
}
}
// ── the disk ────────────────────────────────────────────────────────────
//
// Against the CAP and not against the box: a tenant hits their own wall
// first, and being told the appliance is filling up is somebody else's
// problem printed at somebody who cannot act on it.
if let Ok(answer) = identity
.gunnar
.usage()
.read(pb::ReadUsageRequest { namespace: namespace.clone() })
.await
{
let read = answer.into_inner();
if let Some(usage) = read.usage {
// `pack_bytes` is `Option<u64>` on the wire, and its ABSENCE is
// "no cap declared" — never zero. A namespace with no wall has
// nothing to be near.
let cap = read.allowance.as_ref().and_then(|a| a.pack_bytes).unwrap_or(0);
// No cap is not a full disk. An uncapped namespace has no wall to
// be near, and a percentage of zero is not a number.
if let Some(permille) = usage.pack_bytes.saturating_mul(1000).checked_div(cap) {
if permille >= 850 {
items.push(Action {
id: "room-running-out",
urgency: if permille >= 950 { Urgency::Urgent } else { Urgency::Soon },
title: format!("Your account is {} % full", permille / 10),
detail: "A push is refused once the cap is reached. More room can be added from your account page, and what it costs is shown there."
.to_owned(),
href: "/account".to_owned(),
cta: "More room".to_owned(),
});
}
}
}
}
}
fn plan_state(wire: i32) -> &'static str {
match pb::EntitlementState::try_from(wire) {
Ok(pb::EntitlementState::Free) => "free",
Ok(pb::EntitlementState::Paid) => "paid",
Ok(pb::EntitlementState::Grace) => "grace",
Ok(pb::EntitlementState::Suspended) => "suspended",
Ok(pb::EntitlementState::Retention) => "retention",
_ => "unknown",
}
}
/// `GET /api/account/plan` — the subscription state of the caller's own
/// account.
///
/// Signed with the caller's own credential, so gunnar decides. **Not gated on
/// the shared relay's `offered` (monetize-site-buy)**: what plan somebody is on is a fact about their account that
/// gunnar holds whether or not this monetize can sell them another period, and
/// a customer whose renewal path is unavailable still has the right to see that
/// their plan has lapsed — that is precisely when they most need to.
pub async fn plan(own: OwnSubscription, State(state): State<AppState>) -> ApiResult<Plan> {
let identity = tenant_channel(&state, &own.who).await?;
let answer = identity
.gunnar
.entitlement()
.get(pb::GetEntitlementRequest {
namespace: own.namespace.clone(),
})
.await
.map_err(ApiError::from)?
.into_inner();
// **No fact is not an error.** A namespace monetize has never pushed for —
// a self-hoster, or an account created before it was sold anything — has no
// entitlement, and answering 404 would make the pane look broken for the
// most ordinary case there is. Nor is it `unknown` (owner, test2,
// 2026-10-05: "Plan unknown" read like a fault): gunnar SAID there is
// none, and the answer carries that as the word `none`.
let Some(fact) = answer.fact else {
return Ok(Json(Plan {
plan: String::new(),
state: "none",
paid_until_unix_ms: None,
caps: Default::default(),
}));
};
Ok(Json(Plan {
plan: fact.plan,
state: plan_state(fact.state),
paid_until_unix_ms: fact.paid_until_unix_ms,
caps: fact.caps.into_iter().collect(),
}))
}
// ── What this account holds, and room for more ─────────────────────────────
/// `GET /api/account/storage` — what the caller's own namespace holds and is
/// allowed.
///
/// Delegates to [`crate::api::usage::render`] rather than counting anything
/// here: the Room table on the namespace page and this pane must never be able
/// to disagree about what a byte is, and two renderers is how they would.
///
/// **Not gated on the shared relay's `offered` (monetize-site-buy)**, for [`plan`]'s reason: what a tenant stores is
/// gunnar's answer about their own disk, true on a community appliance that
/// sells nothing at all.
pub async fn storage(
own: OwnSubscription,
State(state): State<AppState>,
) -> ApiResult<crate::api::usage::Usage> {
let identity = tenant_channel(&state, &own.who).await?;
let answer = identity
.gunnar
.usage()
.read(pb::ReadUsageRequest {
namespace: own.namespace.clone(),
})
.await
.map_err(ApiError::from)?
.into_inner();
// The same refusal `api::usage::read` makes, and for the same reason:
// rendering an entire namespace as zeroes is the one answer a person
// reading their own disk must never be given by mistake.
let usage = answer.usage.ok_or_else(|| {
refuse(
"internal",
"Gunnar answered without your account's figures, so there is nothing to show \
yet; try again in a moment",
)
})?;
Ok(Json(crate::api::usage::render(
usage,
answer.allowance.unwrap_or_default(),
// gunnar's own `remaining` (thin layer, owner 2026-10-04).
answer.remaining,
// **No box.** `Admin.Capacity` is the appliance's own disk — total,
// free, servable — and not a tenant's business.
Err("the server's own disk is not shown to an account".to_owned()),
)))
}
// ── What this account has already been sold ────────────────────────────────
/// One past purchase, folded to its outcome.
///
/// **No price, and that is this console's rule rather than monetize's.**
/// `PurchaseAttempt.price` is deliberately present on the wire — monetize
/// documents it as what the customer was charged, and a customer's own money is
/// theirs to see. This console still drops it, for the reason
/// [`crate::api::increase`] gives about the quote: `Money { minor, Currency }`
/// is minor units and an enum, and turning that into a rendered amount needs a
/// division by 100 and a map from `CURRENCY_EUR` to a symbol — money arithmetic
/// and a currency literal, which are exactly what this console may not contain.
///
/// So a row says WHAT was bought and WHEN, and the amounts live where the card
/// was. If a price is ever to appear here, monetize must send it as words, the
/// same conclusion `increase` reached.
///
/// **And no `cost`.** That is the provider's margin and is not on the wire at
/// all; `the_providers_cost_never_reaches_a_customer` guards the sibling route
/// on the bytes.
pub use monetize_site_buy::purchase::Past;
/// **Your purchases** (one shop, owner 2026-10-07), moved into the shared buy flow
/// (monetize-site-buy `purchase`, A12): what this account bought at this shop — its private
/// Gunnars first (Grow, Renew), then its room here; for an account WITHOUT room here, the owner's
/// two lines for its Storage page ("Your Gunnar is <host>", "Want space here on <this host> too?
/// Buy 10GB/month") — never an empty bar or "0 B".
pub use monetize_site_buy::purchase::{Instance, Line, Purchases, RoomBought};
#[cfg(test)]
pub(crate) mod tests {
use super::*;
use crate::mpb;
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};
use crate::mpb::estate_order_server::{EstateOrder as EstateOrderService, EstateOrderServer};
use crate::pb::access_server::{Access as AccessService, AccessServer};
use crate::pb::billing_server::{Billing as BillingService, BillingServer};
use crate::pb::entitlement_server::{Entitlement as EntitlementService, EntitlementServer};
/// The bearer the fake monetize insists on.
const BEARER: &str = "the-monetize-bearer";
/// **The seed of the console's OWN key** — the server-wide admin
/// credential. Its fingerprint is what the starred test asserts a ticket
/// request did NOT carry.
const CONSOLE_SEED: [u8; 32] = [7u8; 32];
/// **The provider's cost, and nothing like the price.** A leak of `cost`
/// under any name puts these exact digits in the body.
const COST_MINOR: u64 = 133_700;
const PRICE_MINOR: u64 = 1_000;
/// **monetize's rendered amount**, and deliberately not a formatting of
/// `PRICE_MINOR`. If the console ever formats the number itself the two
/// will differ, and the assertions can tell which one reached the browser.
const PRICE_WORDS: &str = "49.99 EUR";
/// The vendor's own checkout page, a session id no template here could mint.
const CHECKOUT_URL: &str = "https://checkout.stripe.com/c/pay/cs_test_renew_9z8y";
/// One call that reached the fake gunnar: what was asked, and **which key
/// signed it**.
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct Signed {
pub(crate) what: String,
pub(crate) fingerprint: String,
pub(crate) namespace: String,
pub(crate) purpose: String,
/// The caps the ticket was minted to COVER. Empty for a renewal, and
/// for an order this is the thing that stops a blank cheque.
caps: std::collections::BTreeMap<String, u64>,
}
#[derive(Clone, Default)]
pub(crate) struct FakeGunnar {
pub(crate) calls: Arc<Mutex<Vec<Signed>>>,
/// gunnar refuses to vouch, the way it does for a namespace the
/// caller's key does not hold.
refuse_ticket: bool,
/// gunnar holds NO entitlement for the namespace: `Entitlement.Get`
/// answers no fact, as it does for an account that has bought nothing.
no_fact: bool,
/// Set: the namespaces this gunnar holds. A ticket for any other is
/// `NOT_FOUND`, as gunnar answers a sign-in with no account of its own (the
/// kit's or the operator's machine sign-in, i54a run32 A3).
namespaces: Option<Vec<String>>,
}
impl FakeGunnar {
/// The fingerprint out of the `gunnar-sshsig-v2` credential, which is
/// the only way a test can see WHICH of the console's keys made a call.
fn fingerprint_of(request: &tonic::Request<impl Sized>) -> String {
request
.metadata()
.get(crate::signer::CREDENTIAL_METADATA_KEY)
.and_then(|v| v.to_str().ok())
.and_then(|header| header.split_whitespace().nth(1))
.unwrap_or("<none>")
.to_owned()
}
}
#[tonic::async_trait]
impl BillingService for FakeGunnar {
async fn mint_actor_ticket(
&self,
request: tonic::Request<pb::MintActorTicketRequest>,
) -> Result<tonic::Response<pb::MintActorTicketResponse>, tonic::Status> {
let fingerprint = FakeGunnar::fingerprint_of(&request);
let asked = request.into_inner();
self.calls.lock().expect("the call log").push(Signed {
what: "Billing.MintActorTicket".to_owned(),
fingerprint,
namespace: asked.namespace.clone(),
purpose: asked.purpose.clone(),
caps: asked.caps.clone().into_iter().collect(),
});
if self.refuse_ticket {
return Err(tonic::Status::permission_denied(format!(
"principal has no admin grant on namespace:{}",
asked.namespace
)));
}
if self.namespaces.as_ref().is_some_and(|held| !held.contains(&asked.namespace)) {
return Err(tonic::Status::not_found(format!("no namespace {:?} on this server", asked.namespace)));
}
Ok(tonic::Response::new(pb::MintActorTicketResponse {
// Opaque here on purpose: whether these bytes VERIFY is
// monetize's question and is answered by `monetize-embed`'s own
// tests. What this console must get right is which key it asked
// with, which namespace it named, and that it relays the answer
// unaltered.
ticket: b"{\"v\":1,\"a-real-ticket-is-signed-json\":true}".to_vec(),
expires_unix_ms: 0,
}))
}
async fn appliance_identity(
&self,
_request: tonic::Request<pb::ApplianceIdentityRequest>,
) -> Result<tonic::Response<pb::ApplianceIdentityResponse>, tonic::Status> {
Err(tonic::Status::unimplemented("not this test's"))
}
}
/// `Access.AddKey` is the one call `TenantKeys::ensure` makes, and it is
/// made with the CONSOLE's admin key — correctly so: enrolling a key on a
/// principal is administration. The ticket that follows must not be.
/// **What the appliance says this account is allowed today.**
///
/// Two meters, and only one of them is what the bundle raises — see
/// `HELD_CACHE`. A console that minted its ticket from the delta alone
/// would be refused by a real monetize and would pass a one-meter fixture.
#[tonic::async_trait]
impl EntitlementService for FakeGunnar {
async fn get(
&self,
request: tonic::Request<pb::GetEntitlementRequest>,
) -> Result<tonic::Response<pb::GetEntitlementResponse>, tonic::Status> {
let asked = request.into_inner();
if self.no_fact {
return Ok(tonic::Response::new(pb::GetEntitlementResponse { fact: None }));
}
Ok(tonic::Response::new(pb::GetEntitlementResponse {
fact: Some(pb::EntitlementFact {
namespace: asked.namespace,
plan: "standard".to_owned(),
state: pb::EntitlementState::Paid as i32,
caps: [
("pack_bytes".to_owned(), HELD_PACK),
("cache_bytes".to_owned(), HELD_CACHE),
]
.into(),
..Default::default()
}),
}))
}
}
#[tonic::async_trait]
impl AccessService for FakeGunnar {
/// Every tenant here buys for their own account: an individual.
async fn who_am_i(
&self,
_r: tonic::Request<pb::WhoAmIRequest>,
) -> Result<tonic::Response<pb::WhoAmIResponse>, tonic::Status> {
Ok(tonic::Response::new(pb::WhoAmIResponse { account_role: "individual".to_owned(), ..Default::default() }))
}
async fn add_key(
&self,
request: tonic::Request<pb::AddKeyRequest>,
) -> Result<tonic::Response<pb::AddKeyResponse>, tonic::Status> {
let fingerprint = FakeGunnar::fingerprint_of(&request);
let asked = request.into_inner();
self.calls.lock().expect("the call log").push(Signed {
what: "Access.AddKey".to_owned(),
fingerprint,
namespace: asked.principal_id,
purpose: String::new(),
caps: Default::default(),
});
Ok(tonic::Response::new(pb::AddKeyResponse::default()))
}
async fn revoke_key(
&self,
_r: tonic::Request<pb::RevokeKeyRequest>,
) -> Result<tonic::Response<pb::RevokeKeyResponse>, tonic::Status> {
Err(tonic::Status::unimplemented("not this test's"))
}
async fn list_keys(
&self,
_r: tonic::Request<pb::ListKeysRequest>,
) -> Result<tonic::Response<pb::ListKeysResponse>, tonic::Status> {
Err(tonic::Status::unimplemented("not this test's"))
}
async fn add_principal(
&self,
_r: tonic::Request<pb::AddPrincipalRequest>,
) -> Result<tonic::Response<pb::AddPrincipalResponse>, tonic::Status> {
Err(tonic::Status::unimplemented("not this test's"))
}
async fn list_principals(
&self,
_r: tonic::Request<pb::ListPrincipalsRequest>,
) -> Result<tonic::Response<pb::ListPrincipalsResponse>, tonic::Status> {
Err(tonic::Status::unimplemented("not this test's"))
}
async fn remove_principal(
&self,
_r: tonic::Request<pb::RemovePrincipalRequest>,
) -> Result<tonic::Response<pb::RemovePrincipalResponse>, tonic::Status> {
Err(tonic::Status::unimplemented("not this test's"))
}
async fn rename_principal(
&self,
_r: tonic::Request<pb::RenamePrincipalRequest>,
) -> Result<tonic::Response<pb::RenamePrincipalResponse>, tonic::Status> {
Err(tonic::Status::unimplemented("not this test's"))
}
async fn grant(
&self,
_r: tonic::Request<pb::GrantRequest>,
) -> Result<tonic::Response<pb::GrantResponse>, tonic::Status> {
Err(tonic::Status::unimplemented("not this test's"))
}
async fn revoke(
&self,
_r: tonic::Request<pb::RevokeRequest>,
) -> Result<tonic::Response<pb::RevokeResponse>, tonic::Status> {
Err(tonic::Status::unimplemented("not this test's"))
}
async fn list_grants(
&self,
_r: tonic::Request<pb::ListGrantsRequest>,
) -> Result<tonic::Response<pb::ListGrantsResponse>, tonic::Status> {
Err(tonic::Status::unimplemented("not this test's"))
}
async fn add_group_member(
&self,
_r: tonic::Request<pb::AddGroupMemberRequest>,
) -> Result<tonic::Response<pb::AddGroupMemberResponse>, tonic::Status> {
Err(tonic::Status::unimplemented("not this test's"))
}
async fn remove_group_member(
&self,
_r: tonic::Request<pb::RemoveGroupMemberRequest>,
) -> Result<tonic::Response<pb::RemoveGroupMemberResponse>, tonic::Status> {
Err(tonic::Status::unimplemented("not this test's"))
}
async fn list_group_members(
&self,
_r: tonic::Request<pb::ListGroupMembersRequest>,
) -> Result<tonic::Response<pb::ListGroupMembersResponse>, tonic::Status> {
Err(tonic::Status::unimplemented("not this test's"))
}
async fn list_group_membership_events(
&self,
_r: tonic::Request<pb::ListGroupMembershipEventsRequest>,
) -> Result<tonic::Response<pb::ListGroupMembershipEventsResponse>, tonic::Status> {
Err(tonic::Status::unimplemented("not this test's"))
}
}
/// What monetize was asked, so a refusal that had already reached it can be
/// told from one that had not.
#[derive(Debug, Clone, PartialEq, Eq)]
struct Asked {
what: String,
tenant: String,
renew: bool,
ticket_len: usize,
caps_named: bool,
caps: std::collections::BTreeMap<String, u64>,
/// **The bundle id the request carried**, empty for a typed order. On
/// the wire it and `caps` are mutually exclusive and monetize refuses a
/// request that sets both, so `caps_named` beside this is what proves
/// the console sent one shape and not one and a half.
bundle: String,
}
/// Every `Purchase.Start{users}` any fake monetize heard: `(tenant, users)` (tests run at once,
/// so each one reads its own tenant's rows).
static USERS_ASKED: Mutex<Vec<(String, mpb::UsersOrder)>> = Mutex::new(Vec::new());
/// ★ A32-25 (the guard): **this console starts no `Purchase.Start{users}` of its own**; "Add
/// users" is monetize-site-buy's door (`relay::add_users`), called from `account_admin`.
#[test]
fn this_console_starts_no_users_purchase_of_its_own() {
let here = include_str!("subscription.rs");
// Spelled apart, so this test's own text is not what it looks for.
let own = [concat!("users: ", "Some("), concat!("pub(crate) async fn ", "add_users("), concat!("async fn ", "order_ticket(")];
for gone in own {
assert!(!here.contains(gone), "api/subscription.rs still carries `{gone}`: \"Add users\" is the shared buy flow's door");
}
assert!(include_str!("account_admin.rs").contains(concat!("monetize_site_buy::relay::", "add_users(")), "account_admin calls the shared door");
}
/// ★ A32-25: **"Add users" buys through the shared buy flow's door** (monetize-site-buy
/// `relay::add_users`), not a `Purchase.Start` of this console's own: `POST
/// /api/account/employees` on a console that sells room starts monetize's `Purchase.Start`
/// with `users` for the account named, the room they bring, the invite redeemed at this
/// console's origin, under an ORDER ticket this console minted as the person for exactly
/// `users` and `user_room_bytes`; the checkout comes back as `checkout_url`.
#[tokio::test]
async fn add_users_goes_through_the_shared_buy_flows_door() {
let rig = rig(FakeGunnar::default(), FakeMonetize::verifying()).await;
let session = rig.tenant("alice", "alice");
let (status, body) = request_body(rig.router.clone(), http::Method::POST, "/api/account/employees", Some(&session), r#"{"account":"alice","count":2,"room_gib":1}"#).await;
assert_eq!(status, http::StatusCode::OK, "{body}");
let answer: serde_json::Value = serde_json::from_str(&body).expect("json");
assert_eq!((answer["state"].as_str(), answer["reference"].as_str()), (Some("pending"), Some("gunnar/alice/2026-09-17")), "{answer}");
assert!(answer["checkout_url"].as_str().is_some_and(|u| u.starts_with("https://")), "{answer}");
let users: Vec<mpb::UsersOrder> = USERS_ASKED.lock().unwrap().iter().filter(|(t, _)| t == "alice").map(|(_, u)| u.clone()).collect();
assert_eq!(users, vec![mpb::UsersOrder { count: 2, room_bytes: 1 << 30, name: String::new(), email: String::new(), invite_origin: "https://console.example".to_owned() }]);
let minted = rig.minted_for();
let order = minted.iter().find(|m| m.purpose == "order").unwrap_or_else(|| panic!("no order ticket: {minted:?}"));
assert_eq!(order.caps, std::collections::BTreeMap::from([("users".to_owned(), 2), ("user_room_bytes".to_owned(), 1 << 30)]));
assert_ne!(order.fingerprint, rig.console_fingerprint, "minted as the person, never with the console's admin key");
}
#[derive(Clone)]
struct FakeMonetize {
asked: Arc<Mutex<Vec<Asked>>>,
/// `ProductInfo.tenant_renewal` — whether this monetize verifies
/// tickets at all. **False is an OLD monetize**, and the whole point of
/// the flag.
tenant_renewal: bool,
/// What `Purchase.Quote` refuses with, when it refuses. `None` quotes.
quote_refuses: Option<(tonic::Code, String)>,
/// The customer's sentence monetize puts beside a refusal
/// (`monetize-customer-words-bin`), when it sends one.
quote_refusal_words: Option<String>,
/// `Purchase.Status` answers a PAID PROLONG: its `Reserved` row the
/// way monetize serialises a plan purchase (`purchase.plan.verb`,
/// an empty `delta`) and the granted fact's new end.
prolonged: bool,
/// `Purchase.Status` answers PENDING with no `Captured` row (an order
/// nobody has paid) instead of PAID. The customer's cancel case.
unpaid: bool,
/// Every `Purchase.Abandon`: `(reference, reason)`.
abandoned: Arc<Mutex<Vec<(String, String)>>>,
/// Every `Purchase.Cancel` monetize ACCEPTED: `(reference, reason)`.
cancelled: Arc<Mutex<Vec<(String, String)>>>,
/// `Purchase.Bundles` says the window is still being read (no rows).
reading: bool,
/// `Purchase.Bundles` answers as a deployment that sells PLANS
/// (2026-10-05): the gauge, a prolong row (pressable or past its
/// horizon) and a 12-month upgrade.
plans: Option<bool>,
/// `EstateOrder.Purchases` answers a room line beside the instance
/// (an account that bought room here too); else none (an account
/// without room here, one shop 2026-10-07).
has_room: bool,
/// The vendor link `Purchase.Start` and an unpaid `Purchase.Status`
/// answer with.
checkout_url: &'static str,
}
/// **The window's one offer.** The id is opaque and deliberately not a
/// description of the delta, so a console that parsed it would get the size
/// wrong rather than merely be rude.
const BUNDLE_ID: &str = "the-usual";
/// monetize's sentence on a prolong past its horizon (`plan::words::ahead_limit`).
const AHEAD_WORDS: &str = "This account is already paid 3 months ahead, the most a 1-month plan can be prolonged. For longer, choose a 12-month plan. Nothing has been charged.";
const BUNDLE_LABEL: &str = "10.0 GiB more room";
/// The end a paid prolong's granted fact carries: 2026-12-05.
const PROLONGED_UNTIL: u64 = 1_796_428_800_000;
const BUNDLE_PRICE_WORDS: &str = "220.00 SEK";
/// The window's one row monetize CANNOT serve, and the sentence it carried
/// on 2026-09-20 (`monetize::transaction::words::CANNOT_ADD_ROOM`).
const DEAD_BUNDLE_ID: &str = "100gb";
const DEAD_BUNDLE_WORDS: &str = "We cannot add more space to your account right now while we carry \
out maintenance. Nothing has been charged — please try again later.";
/// What the offer RAISES, and what the account already HOLDS.
///
/// `held_cache` is the load-bearing fixture: it is a meter the bundle does
/// not touch, and monetize's own resolution keeps it in the target caps. A
/// ticket minted without it is one `verify_ticket` refuses, because that
/// comparison is `!=` over the whole map.
const BUNDLE_DELTA: u64 = 10_737_418_240;
pub(crate) const HELD_PACK: u64 = 10_737_418_240;
const HELD_CACHE: u64 = 1_073_741_824;
impl FakeMonetize {
/// monetize 3b44e7d's tenant scope: with a tenant named, only that
/// tenant's attempt is answered — one uniform NOT_FOUND otherwise.
fn owned_by(reference: &str, tenant: &str) -> Result<(), tonic::Status> {
let owner = reference
.split_once('/')
.and_then(|(_, rest)| rest.rsplit_once('/'))
.map(|(owner, _)| owner)
.unwrap_or_default();
if !tenant.is_empty() && owner != tenant {
return Err(tonic::Status::not_found(
"this tenant has no purchase by that reference",
));
}
Ok(())
}
fn verifying() -> FakeMonetize {
FakeMonetize {
asked: Default::default(),
tenant_renewal: true,
quote_refuses: None,
quote_refusal_words: None,
prolonged: false,
unpaid: false,
abandoned: Default::default(),
cancelled: Default::default(),
reading: false,
plans: None,
has_room: false,
checkout_url: CHECKOUT_URL,
}
}
/// A monetize that REFUSES the quote, with the code and words it would
/// really use. `failed_precondition` is what every `TxError::Refused`
/// becomes in `monetize_server::services::purchase`.
fn refusing_the_quote(code: tonic::Code, why: &str) -> FakeMonetize {
FakeMonetize { quote_refuses: Some((code, why.to_owned())), ..FakeMonetize::verifying() }
}
/// A refusal the way monetize's `refused_status` sends one: the
/// operator's reason in the message, the customer's sentence beside it.
fn refusing_the_quote_with_words(code: tonic::Code, why: &str, words: &str) -> FakeMonetize {
FakeMonetize { quote_refusal_words: Some(words.to_owned()), ..FakeMonetize::refusing_the_quote(code, why) }
}
fn old() -> FakeMonetize {
FakeMonetize {
asked: Default::default(),
tenant_renewal: false,
quote_refuses: None,
quote_refusal_words: None,
prolonged: false,
unpaid: false,
abandoned: Default::default(),
cancelled: Default::default(),
reading: false,
plans: None,
has_room: false,
checkout_url: CHECKOUT_URL,
}
}
fn bearer_ok(&self, request: &tonic::Request<impl Sized>) -> Result<(), tonic::Status> {
let offered = request
.metadata()
.get("authorization")
.and_then(|v| v.to_str().ok())
.unwrap_or_default();
if offered != format!("Bearer {BEARER}") {
return Err(tonic::Status::unauthenticated(format!(
"monetize was offered {offered:?}"
)));
}
Ok(())
}
}
/// **`EstateOrder`, as far as a private Gunnar's order goes** (one shop,
/// 2026-10-07): `Start` is recorded — the account it names, the ticket's
/// length (the bytes are the fake gunnar's constant), the company in
/// `bundle` — and answers where to pay, or monetize's sentence for a
/// taken name. The rest is not these tests'.
#[tonic::async_trait]
impl EstateOrderService for FakeMonetize {
async fn start(&self, request: tonic::Request<mpb::EstateOrderStartRequest>) -> Result<tonic::Response<mpb::EstateOrderStartResponse>, tonic::Status> {
self.bearer_ok(&request)?;
let r = request.into_inner();
self.asked.lock().expect("the ask log").push(Asked {
what: "EstateOrder.Start".to_owned(),
tenant: r.account.clone(),
renew: false,
ticket_len: r.actor_ticket.len(),
caps_named: false,
caps: Default::default(),
bundle: r.company.clone(),
});
if r.company == "taken" {
return Err(tonic::Status::already_exists("taken.gunnar.rs is taken"));
}
Ok(tonic::Response::new(mpb::EstateOrderStartResponse {
reference: format!("estate/{}", r.company),
checkout_url: "https://checkout.fake-stripe.test/c/pay/cs_test_estate".to_owned(),
state: "waiting_for_payment".to_owned(),
words: "Waiting for your payment.".to_owned(),
price_words: "792.00 EUR".to_owned(),
}))
}
async fn status(&self, _r: tonic::Request<mpb::EstateOrderStatusRequest>) -> Result<tonic::Response<mpb::EstateOrderStatusResponse>, tonic::Status> {
Err(tonic::Status::unimplemented("not these tests'"))
}
async fn renew(&self, _r: tonic::Request<mpb::EstateOrderRenewRequest>) -> Result<tonic::Response<mpb::EstateOrderRenewResponse>, tonic::Status> {
Err(tonic::Status::unimplemented("not these tests'"))
}
/// One shop 4: a step for an instance, on its account's ticket — recorded
/// (the company in `bundle`, main's data in `caps`); someone else's
/// instance (`bolt`) is monetize's one sentence.
async fn change_plan(&self, request: tonic::Request<mpb::EstateOrderChangePlanRequest>) -> Result<tonic::Response<mpb::EstateOrderChangePlanResponse>, tonic::Status> {
self.bearer_ok(&request)?;
let r = request.into_inner();
let sizes = r.sizes.unwrap_or_default();
self.asked.lock().expect("the ask log").push(Asked {
what: "EstateOrder.ChangePlan".to_owned(),
tenant: String::new(),
renew: false,
ticket_len: r.actor_ticket.len(),
caps_named: false,
caps: std::collections::BTreeMap::from([("main_data_gib".to_owned(), sizes.main_data_gib)]),
bundle: r.company.clone(),
});
if r.company == "bolt" {
return Err(tonic::Status::unauthenticated("that instance is not your account's"));
}
Ok(tonic::Response::new(mpb::EstateOrderChangePlanResponse {
reference: format!("estate/{}/change-1", r.company),
checkout_url: "https://checkout.fake-stripe.test/c/pay/cs_test_change".to_owned(),
status: "waiting_for_payment".to_owned(),
words: "main's data disks grow from 10 to 30 GB each and the twin's to 120 GB, once paid. 44.00 EUR is held now.".to_owned(),
price_words: "44.00 EUR".to_owned(),
twin_plan: "STARTER-1xCPU-4GB".to_owned(),
}))
}
/// One shop 4: an instance's ladder, on its account's ticket.
async fn ladder(&self, request: tonic::Request<mpb::EstateOrderLadderRequest>) -> Result<tonic::Response<mpb::EstateOrderLadderResponse>, tonic::Status> {
self.bearer_ok(&request)?;
let r = request.into_inner();
self.asked.lock().expect("the ask log").push(Asked {
what: "EstateOrder.Ladder".to_owned(),
tenant: String::new(),
renew: false,
ticket_len: r.actor_ticket.len(),
caps_named: false,
caps: Default::default(),
bundle: r.company.clone(),
});
let step = |name: &str, data: u64| mpb::EstateLadderStep {
name: name.into(),
sizes: Some(mpb::EstateSizes { front_plan: "STARTER-2xCPU-4GB".into(), main_plan: "STARTER-4xCPU-8GB".into(), main_data_gib: data, ..Default::default() }),
resources_words: format!("7 cores · 16 GB RAM · {} GB data", 4 * data),
monthly_words: "132.00 EUR a month".into(),
available: true,
..Default::default()
};
Ok(tonic::Response::new(mpb::EstateOrderLadderResponse { steps: vec![step("Mini", 10), step("Small", 30)], start: 0, laid: true, current: 0, floor: 0, apex: "gunnar.rs".into(), term_months: 6 }))
}
async fn notice(&self, _r: tonic::Request<mpb::EstateNoticeRequest>) -> Result<tonic::Response<mpb::EstateNoticeResponse>, tonic::Status> {
Ok(tonic::Response::new(mpb::EstateNoticeResponse::default()))
}
async fn sells(&self, _r: tonic::Request<mpb::EstateOrderSellsRequest>) -> Result<tonic::Response<mpb::EstateOrderSellsResponse>, tonic::Status> {
Ok(tonic::Response::new(mpb::EstateOrderSellsResponse { estates: true }))
}
async fn cancel(&self, _r: tonic::Request<mpb::EstateOrderCancelRequest>) -> Result<tonic::Response<mpb::EstateOrderCancelResponse>, tonic::Status> {
Err(tonic::Status::unimplemented("not these tests'"))
}
async fn send_sign_in_again(&self, _r: tonic::Request<mpb::EstateOrderSignInAgainRequest>) -> Result<tonic::Response<mpb::EstateOrderSignInAgainResponse>, tonic::Status> {
Err(tonic::Status::unimplemented("not these tests'"))
}
/// S8: a lost front made again from its daily backup, on its account's ticket —
/// recorded; someone else's instance (`bolt`) is monetize's one sentence.
async fn restore_front(&self, request: tonic::Request<mpb::EstateOrderRestoreFrontRequest>) -> Result<tonic::Response<mpb::EstateOrderRestoreFrontResponse>, tonic::Status> {
self.bearer_ok(&request)?;
let r = request.into_inner();
self.asked.lock().expect("the ask log").push(Asked {
what: "EstateOrder.RestoreFront".to_owned(),
tenant: String::new(),
renew: false,
ticket_len: r.actor_ticket.len(),
caps_named: false,
caps: Default::default(),
bundle: r.company.clone(),
});
if r.company == "bolt" {
return Err(tonic::Status::unauthenticated("that instance is not your account's"));
}
Ok(tonic::Response::new(mpb::EstateOrderRestoreFrontResponse { words: "acme.gunnar.rs's front is restored from its backup of last night, with its keys".to_owned(), moved: false, public_ipv4: "198.51.100.7".to_owned() }))
}
async fn settle(&self, _r: tonic::Request<mpb::EstateOrderSettleRequest>) -> Result<tonic::Response<mpb::EstateOrderSettleResponse>, tonic::Status> {
Err(tonic::Status::unimplemented("not these tests'"))
}
/// TERMINATE (2026-10-12): recorded (the company in `bundle`, the typed name in
/// `tenant`); ended at once, no backup (A32-27: the wire carries none).
async fn terminate(&self, request: tonic::Request<mpb::EstateOrderTerminateRequest>) -> Result<tonic::Response<mpb::EstateOrderTerminateResponse>, tonic::Status> {
self.bearer_ok(&request)?;
let r = request.into_inner();
self.asked.lock().expect("the ask log").push(Asked {
what: "EstateOrder.Terminate".to_owned(),
tenant: r.confirm.clone(),
renew: false,
ticket_len: r.actor_ticket.len(),
caps_named: false,
caps: Default::default(),
bundle: r.company.clone(),
});
Ok(tonic::Response::new(mpb::EstateOrderTerminateResponse { words: format!("{}.gunnar.rs is terminated: its machines, disks and DNS records are gone.", r.company), done: true }))
}
async fn purchases(&self, request: tonic::Request<mpb::EstateOrderPurchasesRequest>) -> Result<tonic::Response<mpb::EstateOrderPurchasesResponse>, tonic::Status> {
self.bearer_ok(&request)?;
let r = request.into_inner();
self.asked.lock().expect("the ask log").push(Asked {
what: "EstateOrder.Purchases".to_owned(),
tenant: r.account.clone(),
renew: false,
ticket_len: r.actor_ticket.len(),
caps_named: false,
caps: Default::default(),
bundle: String::new(),
});
// Round 5.2: `erik` is an account monetize knows nothing of; `alma` an
// account at this shop that bought nothing yet.
let (bought, account_here) = match r.account.as_str() {
"erik" => (false, false),
"alma" => (false, true),
_ => (true, false),
};
Ok(tonic::Response::new(mpb::EstateOrderPurchasesResponse {
account_here,
estates: if !bought { Vec::new() } else { vec![mpb::EstatePurchase {
company: "acme".into(),
host: "acme.gunnar.rs".into(),
url: "https://acme.gunnar.rs".into(),
state: "ready".into(),
words: "Your Gunnar is ready at https://acme.gunnar.rs; it is paid until 2027-04-07.".into(),
step: "Mini".into(),
resources_words: "7 cores · 16 GB RAM · 40 GB data".into(),
monthly_words: "132.00 EUR a month".into(),
paid_until: "2027-04-07".into(),
repair_words: String::new(),
}] },
rooms: if self.has_room { vec![mpb::RoomPurchase { state: "paid".into(), paid_until: "2026-11-07".into(), cap_bytes: 10_737_418_240 }] } else { Vec::new() },
}))
}
}
#[tonic::async_trait]
impl ProductsService for FakeMonetize {
async fn list(
&self,
request: tonic::Request<mpb::ListProductsRequest>,
) -> Result<tonic::Response<mpb::ListProductsResponse>, tonic::Status> {
self.bearer_ok(&request)?;
Ok(tonic::Response::new(mpb::ListProductsResponse {
products: vec![mpb::ProductInfo {
id: "gunnar".to_owned(),
meters: Vec::new(),
reachable: None,
tenant_renewal: self.tenant_renewal,
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.bearer_ok(&request)?;
if let Some((code, why)) = &self.quote_refuses {
let mut refused = tonic::Status::new(*code, why.clone());
if let Some(words) = &self.quote_refusal_words {
refused
.metadata_mut()
.insert_bin(crate::grpc::CUSTOMER_WORDS_KEY, tonic::metadata::MetadataValue::from_bytes(words.as_bytes()));
}
return Err(refused);
}
let asked = request.into_inner();
self.asked.lock().expect("the ask log").push(Asked {
what: "Purchase.Quote".to_owned(),
tenant: asked.tenant.clone(),
renew: asked.renew,
ticket_len: asked.actor_ticket.len(),
caps_named: !asked.caps.is_empty(),
caps: asked.caps.clone().into_iter().collect(),
bundle: asked.bundle.clone(),
});
Ok(tonic::Response::new(mpb::QuoteResponse {
reference: format!("gunnar/{}/2026-09-17", asked.tenant),
can_serve: true,
period: "year".to_owned(),
// Monetize's rendered amount. Distinct from the raw
// PRICE_MINOR digits so a test can tell "the console carried
// monetize's words" from "the console formatted the number
// itself" — the second is the thing it may not do.
price_words: PRICE_WORDS.to_owned(),
caps: [("pack_bytes".to_owned(), 10_737_418_240u64)].into(),
..Default::default()
}))
}
async fn start(
&self,
request: tonic::Request<mpb::StartPurchaseRequest>,
) -> Result<tonic::Response<mpb::StartPurchaseResponse>, tonic::Status> {
self.bearer_ok(&request)?;
let asked = request.into_inner();
if let Some(users) = asked.users.clone() {
USERS_ASKED.lock().expect("the users log").push((asked.tenant.clone(), users));
}
self.asked.lock().expect("the ask log").push(Asked {
what: "Purchase.Start".to_owned(),
tenant: asked.tenant.clone(),
renew: asked.renew,
ticket_len: asked.actor_ticket.len(),
caps_named: !asked.caps.is_empty(),
caps: asked.caps.clone().into_iter().collect(),
bundle: asked.bundle.clone(),
});
Ok(tonic::Response::new(mpb::StartPurchaseResponse {
reference: format!("gunnar/{}/2026-09-17", asked.tenant),
state: mpb::PurchaseState::Pending as i32,
redirect_url: self.checkout_url.to_owned(),
reason: "waiting for the customer at the vendor".to_owned(),
..Default::default()
}))
}
/// **The shop window.** One offer, so a test can name the whole of it.
async fn bundles(
&self,
request: tonic::Request<mpb::ListBundlesRequest>,
) -> Result<tonic::Response<mpb::ListBundlesResponse>, tonic::Status> {
self.bearer_ok(&request)?;
let asked = request.into_inner();
self.asked.lock().expect("the ask log").push(Asked {
what: "Purchase.Bundles".to_owned(),
tenant: asked.tenant.clone(),
renew: false,
ticket_len: asked.actor_ticket.len(),
caps_named: false,
caps: Default::default(),
bundle: String::new(),
});
if self.reading {
return Ok(tonic::Response::new(mpb::ListBundlesResponse { bundles: Vec::new(), reading: true, ..Default::default() }));
}
if let Some(prolong_ok) = self.plans {
return Ok(tonic::Response::new(mpb::ListBundlesResponse {
reading: false,
bundles: vec![
mpb::Bundle {
id: "prolong-1m".to_owned(),
label: "Prolong 1 month (10 GiB)".to_owned(),
price: "13.60 EUR".to_owned(),
period: "month".to_owned(),
can_serve: prolong_ok,
reason: if prolong_ok { String::new() } else { AHEAD_WORDS.to_owned() },
action: "prolong".to_owned(),
..Default::default()
},
mpb::Bundle {
id: "100gb-12m".to_owned(),
label: "Upgrade to 100 GiB · 12 months".to_owned(),
price: "420.00 EUR".to_owned(),
delta: [("pack_bytes".to_owned(), 100 * BUNDLE_DELTA - HELD_PACK)].into(),
can_serve: true,
action: "upgrade".to_owned(),
target: [("pack_bytes".to_owned(), 100 * BUNDLE_DELTA), ("cache_bytes".to_owned(), HELD_CACHE)].into(),
..Default::default()
},
],
plan: Some(mpb::PlanStatus {
state: "paid".to_owned(),
period: "month".to_owned(),
label: "10 GiB · 1 month".to_owned(),
cap_bytes: HELD_PACK,
days_left: 6,
days_total: 30,
ends_words: "Paid until 2026-11-05 — 6 days left".to_owned(),
price_words: "13.60 EUR per month".to_owned(),
banner: "Your paid time ends on 2026-11-05 — in 6 days.".to_owned(),
banner_action: "prolong-1m".to_owned(),
banner_tone: "warn".to_owned(),
..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, and nothing like a formatting of `PRICE_MINOR`:
// if this console ever renders an amount itself, the two
// differ and the assertion says which one reached the
// browser.
price: BUNDLE_PRICE_WORDS.to_owned(),
period: "month".to_owned(),
delta: [("pack_bytes".to_owned(), BUNDLE_DELTA)].into(),
can_serve: true,
reason: String::new(), ..Default::default()
},
// **The row the owner read for five days**, as monetize sent it
// on 2026-09-20: a real id, a real price, `can_serve: false`,
// and under it the masked sentence. It must not reach the pane.
mpb::Bundle {
id: DEAD_BUNDLE_ID.to_owned(),
label: "100.0 GiB more room".to_owned(),
price: "1000.00 SEK".to_owned(),
period: "month".to_owned(),
delta: [("pack_bytes".to_owned(), 10 * BUNDLE_DELTA)].into(),
can_serve: false,
reason: DEAD_BUNDLE_WORDS.to_owned(), ..Default::default()
}], ..Default::default()
}))
}
async fn status(
&self,
request: tonic::Request<mpb::PurchaseStatusRequest>,
) -> Result<tonic::Response<mpb::PurchaseStatusResponse>, tonic::Status> {
self.bearer_ok(&request)?;
let asked = request.into_inner();
self.asked.lock().expect("the ask log").push(Asked {
what: "Purchase.Status".to_owned(),
tenant: asked.tenant.clone(),
renew: false,
ticket_len: asked.actor_ticket.len(),
caps_named: false,
caps: Default::default(),
bundle: String::new(),
});
Self::owned_by(&asked.reference, &asked.tenant)?;
if self.prolonged {
// `serde_json::to_string(&Step::Reserved { .. })`, as
// monetize-server's `row_to_pb` writes it, cut to the fields
// that matter here.
let reserved = format!(
"{{\"Reserved\":{{\"reference\":\"{}\",\"purchase\":{{\"product\":\"gunnar\",\"caps\":{{\"pack_bytes\":{HELD_PACK}}},\"delta\":{{}},\"price\":{{\"minor\":{PRICE_MINOR}}},\"cost\":{{\"minor\":0}},\"plan\":{{\"verb\":\"prolong\",\"period\":\"Month\",\"size_bytes\":{HELD_PACK}}}}}}}}}",
asked.reference
);
return Ok(tonic::Response::new(mpb::PurchaseStatusResponse {
fact: Some(mpb::EntitlementFact {
caps: [("pack_bytes".to_owned(), HELD_PACK)].into(),
paid_until_unix_ms: Some(PROLONGED_UNTIL),
..Default::default()
}),
reference: asked.reference.clone(),
state: mpb::PurchaseState::Paid as i32,
phase: mpb::PurchasePhase::Done as i32,
rows: vec![mpb::LedgerRow { reference: asked.reference, seq: 1, at_unix_ms: 0, kind: "Reserved".to_owned(), json: reserved }],
// monetize's grow queue line, which it sends for a paid
// prolong too; the page decides not to draw it.
room: Some(mpb::RoomProgress { state: "queued".to_owned(), words: "Paid · queued for today's batch".to_owned() }),
..Default::default()
}));
}
if self.unpaid {
return Ok(tonic::Response::new(mpb::PurchaseStatusResponse {
reference: asked.reference,
state: mpb::PurchaseState::Pending as i32,
phase: mpb::PurchasePhase::WaitingForPayment as i32,
cancellable: true,
reason: "the customer has not paid yet; nothing is provisioned against a \
link nobody has used"
.to_owned(),
rows: vec![mpb::LedgerRow {
reference: "gunnar/alice/2026-09-17".to_owned(),
seq: 1,
at_unix_ms: 0,
kind: "Reserved".to_owned(),
json: "{}".to_owned(),
}],
redirect_url: self.checkout_url.to_owned(),
..Default::default()
}));
}
Ok(tonic::Response::new(mpb::PurchaseStatusResponse {
// The granted fact, as a paid attempt carries it.
fact: Some(mpb::EntitlementFact {
caps: [("pack_bytes".to_owned(), HELD_PACK + BUNDLE_DELTA)].into(),
..Default::default()
}),
reference: asked.reference,
state: mpb::PurchaseState::Paid as i32,
phase: mpb::PurchasePhase::Done as i32,
// **The ledger steps, exactly as monetize sends them**, and a
// `Reserved` step serialises the whole `Purchase` — `cost`
// included. `Purchase.Status` is gated by no ticket at
// monetize, so the promise that a customer never sees the
// provider's margin lives in THIS console. A fake that sent no
// rows would let a handler that forwarded them pass.
rows: vec![mpb::LedgerRow {
reference: "gunnar/alice/2026-09-17".to_owned(),
seq: 1,
at_unix_ms: 0,
kind: "Reserved".to_owned(),
json: format!(
"{{\"purchase\":{{\"price\":{{\"minor\":{PRICE_MINOR}}},\"cost\":{{\"minor\":{COST_MINOR}}}}}}}"
),
}],
..Default::default()
}))
}
async fn abandon(
&self,
request: tonic::Request<mpb::AbandonPurchaseRequest>,
) -> Result<tonic::Response<mpb::AbandonPurchaseResponse>, tonic::Status> {
self.bearer_ok(&request)?;
let asked = request.into_inner();
Self::owned_by(&asked.reference, &asked.tenant)?;
// monetize's `abandon_attempt`: a REFUND of a captured attempt.
// Nothing captured, nothing to refund — refused.
if self.unpaid {
return Err(tonic::Status::failed_precondition(
"nothing was captured on this attempt, so there is nothing to refund",
));
}
self.abandoned
.lock()
.expect("the abandon log")
.push((asked.reference.clone(), asked.reason));
Ok(tonic::Response::new(mpb::AbandonPurchaseResponse {
reference: asked.reference,
released: Vec::new(),
}))
}
async fn cancel(
&self,
request: tonic::Request<mpb::CancelPurchaseRequest>,
) -> Result<tonic::Response<mpb::CancelPurchaseResponse>, tonic::Status> {
self.bearer_ok(&request)?;
let asked = request.into_inner();
self.asked.lock().expect("the ask log").push(Asked {
what: "Purchase.Cancel".to_owned(),
tenant: asked.tenant.clone(),
renew: false,
ticket_len: asked.actor_ticket.len(),
caps_named: false,
caps: Default::default(),
bundle: String::new(),
});
Self::owned_by(&asked.reference, &asked.tenant)?;
// monetize's Cancel: an order with nothing captured is closed; a
// paid one is refused (that is Abandon's, with its refund).
if !self.unpaid {
return Err(tonic::Status::failed_precondition(
"the money for this order was captured; it is not cancelled here",
));
}
self.cancelled
.lock()
.expect("the cancel log")
.push((asked.reference.clone(), asked.reason.clone()));
Ok(tonic::Response::new(mpb::CancelPurchaseResponse {
reference: asked.reference,
already_closed: false,
state: mpb::PurchaseState::Released as i32,
phase: mpb::PurchasePhase::Cancelled as i32,
reason: asked.reason,
released: Vec::new(),
}))
}
async fn history(
&self,
request: tonic::Request<mpb::PurchaseHistoryRequest>,
) -> Result<tonic::Response<mpb::PurchaseHistoryResponse>, tonic::Status> {
self.bearer_ok(&request)?;
let asked = request.into_inner();
self.asked.lock().expect("the ask log").push(Asked {
what: "Purchase.History".to_owned(),
tenant: asked.tenant.clone(),
renew: false,
ticket_len: asked.actor_ticket.len(),
caps_named: false,
caps: Default::default(),
bundle: String::new(),
});
Ok(tonic::Response::new(mpb::PurchaseHistoryResponse {
attempts: vec![mpb::PurchaseAttempt {
reference: format!("gunnar/{}/2026-09-01", asked.tenant),
outcome: "sold".to_owned(),
at_unix_ms: 1_756_000_000_000,
started_unix_ms: 1_755_000_000_000,
caps: [("pack_bytes".to_owned(), 10_737_418_240u64)].into(),
delta: [("pack_bytes".to_owned(), 10_737_418_240u64)].into(),
// **The price monetize is willing to show a customer.** The
// console must still drop it: `Money` is minor units and a
// currency enum, and rendering it is the arithmetic and the
// literal this console may not contain.
price: Some(mpb::Money {
minor: PRICE_MINOR,
currency: mpb::Currency::Eur as i32,
}),
steps: 6,
price_words: PRICE_WORDS.to_owned(),
}],
}))
}
}
struct Rig {
router: axum::Router,
gunnar_calls: Arc<Mutex<Vec<Signed>>>,
monetize_asked: Arc<Mutex<Vec<Asked>>>,
webauthn: Arc<crate::auth::Webauthn>,
/// The console's own admin fingerprint, for the starred test.
console_fingerprint: String,
}
/// Stand a gunnar and a monetize up on loopback and point a console at
/// both, with a real passkey subsystem so sessions are the real thing.
async fn rig(gunnar: FakeGunnar, monetize: FakeMonetize) -> Rig {
let g_listener = tokio::net::TcpListener::bind("127.0.0.1:0")
.await
.expect("a loopback port");
let g_addr = g_listener.local_addr().expect("its address");
let gunnar_calls = gunnar.calls.clone();
tokio::spawn(async move {
tonic::transport::Server::builder()
.add_service(BillingServer::new(gunnar.clone()))
.add_service(EntitlementServer::new(gunnar.clone()))
.add_service(AccessServer::new(gunnar))
.serve_with_incoming(tokio_stream::wrappers::TcpListenerStream::new(g_listener))
.await
});
let m_listener = tokio::net::TcpListener::bind("127.0.0.1:0")
.await
.expect("a loopback port");
let m_addr = m_listener.local_addr().expect("its address");
let monetize_asked = monetize.asked.clone();
tokio::spawn(async move {
tonic::transport::Server::builder()
.add_service(ProductsServer::new(monetize.clone()))
.add_service(EstateOrderServer::new(monetize.clone()))
.add_service(PurchaseServer::new(monetize))
// Signup.Plans: what an account is (the Storage buy line's size, round 5.3.1).
.add_service(crate::mpb::signup_server::SignupServer::new(crate::auth::signup::tests::FakeMonetize::default()))
.serve_with_incoming(tokio_stream::wrappers::TcpListenerStream::new(m_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(&CONSOLE_SEED),
))
.expect("a fixed seed makes a signer"),
);
let console_fingerprint = signer.fingerprint().to_owned();
let endpoint = format!("http://{g_addr}");
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://{m_addr}"), BEARER, "gunnar")
.expect("a lazy channel"),
),
events: crate::sse::EventHub::detached(),
gunnar: crate::grpc::Gunnar::connect_lazy(&endpoint, signer).expect("a lazy handle"),
twin: None,
guest: None,
reviewers: Arc::new(crate::reviewer::ReviewerKeys::new(
endpoint.clone(),
store.clone(),
)),
tenants: Arc::new(crate::tenant::TenantKeys::new(endpoint, 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(),
}));
Rig {
router,
gunnar_calls,
monetize_asked,
webauthn,
console_fingerprint,
}
}
impl Rig {
/// A logged-in tenant: a planted passkey at the VIEWER rung — the rung
/// a paying customer actually lands on — pinned to their own gunnar
/// principal, which on gunnar is their namespace.
fn tenant(&self, user: &str, principal: &str) -> String {
self.webauthn
.plant_credential_for_tests(user, crate::role::Role::Viewer);
self.webauthn
.bind_principal(&format!("planted-{user}-viewer"), principal, 0)
.expect("the planted credential pins");
self.webauthn
.make_session(user, crate::role::Role::Viewer, true)
}
/// A logged-in person with a passkey bound to NO principal.
fn unbound(&self, user: &str) -> String {
self.webauthn
.plant_credential_for_tests(user, crate::role::Role::Viewer);
self.webauthn
.make_session(user, crate::role::Role::Viewer, true)
}
fn minted_for(&self) -> Vec<Signed> {
self.gunnar_calls
.lock()
.expect("the call log")
.iter()
.filter(|c| c.what == "Billing.MintActorTicket")
.cloned()
.collect()
}
fn purchase_calls(&self) -> Vec<Asked> {
self.monetize_asked
.lock()
.expect("the ask log")
.iter()
.filter(|a| a.what.starts_with("Purchase."))
.cloned()
.collect()
}
}
async fn request(
router: axum::Router,
method: http::Method,
uri: &str,
cookie: Option<&str>,
) -> (http::StatusCode, String) {
request_body(router, method, uri, cookie, "{}").await
}
async fn request_body(
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")
.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())
}
// ── the security properties ────────────────────────────────────────────
/// ★ **The ticket is asked for with the TENANT's key, never the console's.**
///
/// This is the whole design in one assertion. This console holds a
/// server-wide admin grant at gunnar; a ticket requested with THAT key
/// would be minted for any namespace the console cared to name, and every
/// other test here would still pass. What makes gunnar the enforcer rather
/// than this crate is that the credential on the `MintActorTicket` call is
/// the one `TenantKeys` enrolled on the tenant's own principal — so gunnar
/// is deciding from the key that signed the call.
///
/// Asserted on the FINGERPRINT in the `gunnar-sshsig-v2` credential, which
/// is the only thing that can tell the two apart from outside this process.
/// An edit that "simplified" `ticket()` to use `state.gunnar` would compile
/// and would fail exactly here.
#[tokio::test]
async fn the_ticket_is_asked_for_over_the_tenants_own_key_and_not_the_consoles() {
let rig = rig(FakeGunnar::default(), FakeMonetize::verifying()).await;
let session = rig.tenant("alice", "alice");
let (status, body) = request(
rig.router.clone(),
http::Method::GET,
"/api/account/subscription",
Some(&session),
)
.await;
assert_eq!(status, http::StatusCode::OK, "{body}");
let minted = rig.minted_for();
assert_eq!(minted.len(), 1, "exactly one ticket per call: {minted:?}");
assert_ne!(
minted[0].fingerprint, rig.console_fingerprint,
"the ticket was asked for with the CONSOLE's server-wide admin key. gunnar would \
then be vouching for whatever namespace this process named, and the appliance \
would have stopped being the thing that decides."
);
assert_eq!(minted[0].namespace, "alice");
assert_eq!(minted[0].purpose, "renew");
}
/// ★ **The namespace is the session's own, and nothing in the request can
/// change it.**
///
/// There is no path segment, query parameter or body field for it by
/// construction — this test spends its effort trying to supply one anyway,
/// because the day somebody adds a convenience parameter this is what
/// catches it.
#[tokio::test]
async fn no_request_field_can_name_another_namespace() {
let rig = rig(FakeGunnar::default(), FakeMonetize::verifying()).await;
let session = rig.tenant("alice", "alice");
let (status, body) = request(
rig.router.clone(),
http::Method::POST,
"/api/account/subscription/renew?namespace=beta&tenant=beta&name=beta",
Some(&session),
)
.await;
assert_eq!(status, http::StatusCode::OK, "{body}");
let minted = rig.minted_for();
assert_eq!(minted[0].namespace, "alice", "a query string chose the namespace");
let bought = rig.purchase_calls();
assert_eq!(bought[0].tenant, "alice", "a query string chose the tenant");
}
/// ★ **An old monetize means the route is not offered — and nothing is
/// bought.**
///
/// proto3 drops unknown fields, so a new console against an old monetize
/// would send an `actor_ticket` that is thrown away and the renewal would
/// go through **unchecked**, looking exactly like success from here. The
/// console must therefore never infer enforcement from a 200.
///
/// The second assertion is the one that matters: no `Purchase` call at all.
/// A route that refused only after the order had been placed would have
/// charged the card it was trying to protect.
#[tokio::test]
async fn an_old_monetize_is_not_offered_the_route_and_sells_nothing() {
let rig = rig(FakeGunnar::default(), FakeMonetize::old()).await;
let session = rig.tenant("alice", "alice");
for (method, uri) in [
(http::Method::GET, "/api/account/subscription"),
(http::Method::POST, "/api/account/subscription/renew"),
(
http::Method::GET,
"/api/account/subscription/status?reference=gunnar/alice/2026-09-17",
),
] {
let (status, body) =
request(rig.router.clone(), method.clone(), uri, Some(&session)).await;
assert_eq!(
status,
http::StatusCode::NOT_IMPLEMENTED,
"{method} {uri} was offered by a monetize that does not verify tickets: {body}"
);
}
assert!(
rig.purchase_calls().is_empty(),
"an unenforced monetize was asked to sell something: {:?}",
rig.purchase_calls()
);
assert!(
rig.minted_for().is_empty(),
"a ticket was minted for a monetize that cannot check one"
);
}
/// ★ **An account door's refusal is the console's `{code, message}`** (A28 H19, P4b): the
/// web reads that shape (`gunnar-ui-web` `api::Fail`: the message shown, the code deciding
/// the summary, a step-up's retry and absence), so it holds wherever the door is answered —
/// here or by the shared router. A sign-in pinned to no principal: `notfound`. A monetize
/// that does not verify tenant tickets: `unimplemented` with its sentence, on every door
/// that would read or spend on a ticket.
#[tokio::test]
async fn an_account_doors_refusal_is_the_consoles_code_and_message() {
let rig = rig(FakeGunnar::default(), FakeMonetize::verifying()).await;
let nobody = rig.unbound("nobody");
let (status, body) = request(rig.router.clone(), http::Method::GET, "/api/account/subscription", Some(&nobody)).await;
let j: serde_json::Value = serde_json::from_str(&body).expect("JSON");
assert_eq!((status, j["code"].as_str()), (http::StatusCode::NOT_FOUND, Some("notfound")), "{body}");
assert!(j["message"].as_str().is_some_and(|m| !m.is_empty()), "{body}");
let rig = rig_old().await;
let session = rig.tenant("alice", "alice");
for (method, uri, sent) in [
(http::Method::GET, "/api/account/subscription", "{}"),
(http::Method::POST, "/api/account/subscription/renew", "{}"),
(http::Method::GET, "/api/account/subscription/status?reference=gunnar/alice/2026-09-17", "{}"),
(http::Method::POST, "/api/account/subscription/cancel", r#"{"reference":"gunnar/alice/2026-09-17"}"#),
(http::Method::GET, "/api/account/subscription/bundles", "{}"),
(http::Method::POST, "/api/account/subscription/buy-bundle", r#"{"bundle":"b1"}"#),
(http::Method::GET, "/api/account/subscription/history", "{}"),
] {
let (status, body) = request_body(rig.router.clone(), method.clone(), uri, Some(&session), sent).await;
let j: serde_json::Value = serde_json::from_str(&body).unwrap_or_default();
assert_eq!((status, j["code"].as_str()), (http::StatusCode::NOT_IMPLEMENTED, Some("unimplemented")), "{method} {uri}: {body}");
assert_eq!(j["message"].as_str(), Some(monetize_site_buy::relay::RENEWAL_NOT_VERIFIED), "{method} {uri}");
}
assert!(rig.purchase_calls().is_empty() && rig.minted_for().is_empty(), "nothing asked of a monetize that cannot check a ticket");
}
/// ★ A28 H19 (P4b): **the account's own doors are the shared buy flow's ROUTER**
/// (`monetize_site_buy::router_of`, its `account` and `orders` groups): the quote, Prolong,
/// the status poll, Cancel order, the window, its grow, the history and Your purchases.
/// What stays here is what only Gunnar answers (the plan, the disk, the typed order, Add
/// users, the queue's lines).
/// ★ g-reds 4b: **the typed order `POST /api/account/subscription/buy` has no door.** The
/// shared crate has no typed order, and nothing calls this one: not gunnar-ui-web (its
/// pages buy by a window bundle, `…/buy-bundle`), not uitest, not a doc, not gunnar's CLI.
/// A tenant's typed size was a second way to spend money that no page offered.
#[tokio::test]
async fn the_typed_buy_has_no_door() {
let here = include_str!("subscription.rs");
let serve = include_str!("../serve.rs");
assert_eq!(here.matches(&["pub async fn ", "buy", "("].concat()).count(), 0, "no typed-buy handler here");
assert_eq!(serve.matches(&["api::subscription::", "buy", ")"].concat()).count(), 0, "no typed-buy route by hand");
let rig = rig(FakeGunnar::default(), FakeMonetize::verifying()).await;
let session = rig.tenant("alice", "alice");
let (status, body) = request_body(rig.router.clone(), http::Method::POST, "/api/account/subscription/buy", Some(&session), r#"{"meter":"pack_bytes","cap":21474836480}"#).await;
assert_eq!(status, http::StatusCode::NOT_FOUND, "{body}");
assert!(rig.purchase_calls().is_empty() && rig.minted_for().is_empty(), "nothing was asked of monetize or gunnar");
}
#[test]
fn the_account_doors_are_the_shared_routers() {
let here = include_str!("subscription.rs");
let serve = include_str!("../serve.rs");
for door in ["offer", "renew", "status", "cancel", "bundles", "buy_bundle", "purchases", "history"] {
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::subscription::", door, ")"].concat()).count(), 0, "{door}: no route by hand");
}
assert!(serve.contains("account: true"), "serve.rs mounts the shared account doors");
}
async fn rig_old() -> Rig {
rig(FakeGunnar::default(), FakeMonetize::old()).await
}
/// ★ **A vendor link the browser may not be sent to never reaches it**
/// (stinky #19): monetize's `redirect_url` becomes the pay button's href
/// and `location.assign`'s argument, so it is checked as the paid signup
/// and the private order check theirs — https, a host, no credentials —
/// and anything else is a 502, never the link.
#[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)", "https://someone@checkout.example/pay"] {
let rig = rig(FakeGunnar::default(), FakeMonetize { checkout_url: bad, unpaid: true, ..FakeMonetize::verifying() }).await;
let session = rig.tenant("alice", "alice");
for (method, uri) in [
(http::Method::POST, "/api/account/subscription/renew"),
(http::Method::GET, "/api/account/subscription/status?reference=gunnar%2Falice%2F2026-09-17"),
] {
let (status, body) = request(rig.router.clone(), method, uri, Some(&session)).await;
assert_eq!(status, http::StatusCode::BAD_GATEWAY, "{bad} at {uri}: {body}");
assert!(!body.contains(bad), "{bad} at {uri}: {body}");
}
}
}
/// A renewal names no caps, says `renew`, and carries the ticket.
#[tokio::test]
async fn a_renewal_names_no_caps_and_carries_the_ticket() {
let rig = rig(FakeGunnar::default(), FakeMonetize::verifying()).await;
let session = rig.tenant("alice", "alice");
let (status, body) = request(
rig.router.clone(),
http::Method::POST,
"/api/account/subscription/renew",
Some(&session),
)
.await;
assert_eq!(status, http::StatusCode::OK, "{body}");
assert!(body.contains(CHECKOUT_URL), "the vendor's own link: {body}");
let bought = rig.purchase_calls();
assert_eq!(bought.len(), 1, "{bought:?}");
assert!(bought[0].renew, "a renewal must say so");
assert!(
!bought[0].caps_named,
"a renewal names no caps — the caps come off the stored fact, and sending them \
would be this console inventing an upgrade"
);
assert!(
bought[0].ticket_len > 0,
"the order carried no ticket, so monetize could not tell this from an operator's"
);
}
/// ★ **A viewer renews their own subscription.**
///
/// The floor here is the opposite of `api::increase`'s, deliberately:
/// there, a viewer and even an operator are refused, because that surface
/// is an operator acting on somebody else's behalf. Here the viewer rung IS
/// the tenant rung — it is what a paid signup lands on — and a customer who
/// could not renew their own plan is the whole defect this lane exists to
/// fix.
#[tokio::test]
async fn a_viewer_renews_their_own_subscription() {
let rig = rig(FakeGunnar::default(), FakeMonetize::verifying()).await;
let session = rig.tenant("vera", "vera");
let (status, body) = request(
rig.router.clone(),
http::Method::POST,
"/api/account/subscription/renew",
Some(&session),
)
.await;
assert_eq!(
status,
http::StatusCode::OK,
"a viewer could not renew their own subscription: {body}"
);
}
/// **The commonest state on the box is not an error.** A tenant who has
/// never bought anything holds no entitlement, so there is no period to
/// extend, and monetize says so as a `TxError::Refused` — which it codes
/// `failed_precondition`.
///
/// MEASURED 2026-09-19: the owner's own account got a red box reading "The
/// request failed." over "nothing to renew for rickard: this server holds
/// no entitlement for them", on the page whose job is to sell them their
/// first subscription. Nothing had failed; they were being alarmed at for
/// not being a subscriber yet. It leaves as an ANSWER now — `can_serve`
/// false and monetize's sentence — which the page draws as a plain line.
#[tokio::test]
async fn a_tenant_with_no_subscription_is_told_so_and_not_shown_a_failure() {
let why = "nothing to renew for vera: this server holds no entitlement for them";
let rig = rig(
FakeGunnar::default(),
FakeMonetize::refusing_the_quote(tonic::Code::FailedPrecondition, why),
)
.await;
let session = rig.tenant("vera", "vera");
let (status, body) = request(
rig.router.clone(),
http::Method::GET,
"/api/account/subscription",
Some(&session),
)
.await;
assert_eq!(status, http::StatusCode::OK, "a refusal was dressed as a failure: {body}");
let seen: serde_json::Value = serde_json::from_str(&body).expect("an answer, not an error page");
assert_eq!(seen["can_serve"], serde_json::json!(false), "{body}");
assert_eq!(seen["reason"], serde_json::json!(why), "monetize's words were reworded: {body}");
// Nothing is invented to fill the gap: no period, no amount, no
// reference. A renewal that cannot happen has none of them.
assert_eq!(seen["period"], serde_json::json!(""), "{body}");
assert_eq!(seen["price_words"], serde_json::json!(""), "{body}");
assert_eq!(seen["reference"], serde_json::json!(""), "{body}");
}
/// **An account that has bought nothing reads "none", not "unknown"**
/// (owner, test2, 2026-10-05: "Plan unknown — Subscription: Renewing is
/// not available right now" on his own fresh account read like a fault).
/// gunnar SAYS there is nothing: `Entitlement.Get` answers no fact. That
/// answer is carried as the word `none`; `unknown` stays for a ladder word
/// this console does not recognise.
#[tokio::test]
async fn a_namespace_gunnar_holds_no_entitlement_for_reads_none_not_unknown() {
let rig = rig(FakeGunnar { no_fact: true, ..Default::default() }, FakeMonetize::verifying()).await;
let session = rig.tenant("vera", "vera");
let (status, body) = request(rig.router.clone(), http::Method::GET, "/api/account/plan", Some(&session)).await;
assert_eq!(status, http::StatusCode::OK, "{body}");
let seen: serde_json::Value = serde_json::from_str(&body).expect("a plan");
assert_eq!(seen["state"], serde_json::json!("none"), "no entitlement is not an unknown one: {body}");
assert_eq!(seen["plan"], serde_json::json!(""), "{body}");
assert_eq!(seen["paid_until_unix_ms"], serde_json::Value::Null, "{body}");
}
/// **Nothing bought, so nothing to renew — said as that.** monetize
/// refuses the renewal (`NothingToRenew`, failed_precondition, with its
/// customer sentence "We cannot renew this account automatically …
/// contact us") and gunnar holds no entitlement: the answer carries
/// `nothing_to_renew`, and the page sends the person to More room
/// instead of telling them to contact anyone.
#[tokio::test]
async fn a_tenant_who_has_bought_nothing_is_told_there_is_nothing_to_renew() {
let rig = rig(
FakeGunnar { no_fact: true, ..Default::default() },
FakeMonetize::refusing_the_quote_with_words(
tonic::Code::FailedPrecondition,
"nothing to renew for vera: this server holds no entitlement for them",
"We cannot renew this account automatically. Nothing has been charged — please contact us and we will set it right.",
),
)
.await;
let session = rig.tenant("vera", "vera");
let (status, body) = request(rig.router.clone(), http::Method::GET, "/api/account/subscription", Some(&session)).await;
assert_eq!(status, http::StatusCode::OK, "{body}");
let seen: serde_json::Value = serde_json::from_str(&body).expect("an answer");
assert_eq!(seen["can_serve"], serde_json::json!(false), "{body}");
assert_eq!(seen["nothing_to_renew"], serde_json::json!(true), "an account with no entitlement was not told it has nothing to renew: {body}");
}
/// **And a real refusal keeps its own words.** The same refusal for a
/// tenant gunnar DOES hold a plan for is not "nothing bought": the answer
/// says `nothing_to_renew: false` and carries monetize's customer
/// sentence as `words`, which the page shows as it arrived — never a
/// sentence composed here, and never the operator's `reason`.
#[tokio::test]
async fn a_refusal_for_a_tenant_who_holds_a_plan_shows_monetizes_own_words() {
let words = "We cannot renew this account automatically. Nothing has been charged — please contact us and we will set it right.";
let why = "vera is past retention (95 days after the paid period ended)";
let rig = rig(FakeGunnar::default(), FakeMonetize::refusing_the_quote_with_words(tonic::Code::FailedPrecondition, why, words)).await;
let session = rig.tenant("vera", "vera");
let (status, body) = request(rig.router.clone(), http::Method::GET, "/api/account/subscription", Some(&session)).await;
assert_eq!(status, http::StatusCode::OK, "{body}");
let seen: serde_json::Value = serde_json::from_str(&body).expect("an answer");
assert_eq!(seen["nothing_to_renew"], serde_json::json!(false), "a held plan was called nothing: {body}");
assert_eq!(seen["words"], serde_json::json!(words), "monetize's customer sentence was not carried: {body}");
assert_eq!(seen["reason"], serde_json::json!(why), "{body}");
}
/// ★ **A paid prolong's status names it a prolong, with its new end**
/// (owner, test2, 2026-10-05: the page back from Stripe after a prolong
/// said "Room added … the disk is enlarged in today's batch"). Both are
/// monetize's facts: the verb off the order's own `Reserved` row, the
/// end off the granted fact. Nothing is computed here.
#[tokio::test]
async fn a_paid_prolongs_status_names_the_prolong_and_the_new_end() {
let rig = rig(FakeGunnar::default(), FakeMonetize { prolonged: true, ..FakeMonetize::verifying() }).await;
let session = rig.tenant("alice", "alice");
let (status, body) = request(
rig.router.clone(),
http::Method::GET,
"/api/account/subscription/status?reference=gunnar%2Falice%2F2026-11-05%2Bpack_bytes%3D10%2Bplan%3Amonth",
Some(&session),
)
.await;
assert_eq!(status, http::StatusCode::OK, "{body}");
let seen: serde_json::Value = serde_json::from_str(&body).expect("an attempt");
assert_eq!(seen["action"], serde_json::json!("prolong"), "{body}");
assert_eq!(seen["paid_until_unix_ms"], serde_json::json!(PROLONGED_UNTIL), "{body}");
// A32-29: and that day in words, the server's (the console draws them as they are).
assert_eq!(seen["paid_until_words"], serde_json::json!(monetize_site_buy::purchase::date_words(PROLONGED_UNTIL)), "{body}");
}
/// And an order for room alone (no plan on its `Reserved` row, a delta)
/// names no action, so its page stays the room page it always was.
#[tokio::test]
async fn a_room_orders_status_names_no_action() {
let rig = rig(FakeGunnar::default(), FakeMonetize::verifying()).await;
let session = rig.tenant("alice", "alice");
let (status, body) = request(rig.router.clone(), http::Method::GET, "/api/account/subscription/status?reference=gunnar%2Falice%2F2026-09-17", Some(&session)).await;
assert_eq!(status, http::StatusCode::OK, "{body}");
let seen: serde_json::Value = serde_json::from_str(&body).expect("an attempt");
assert!(seen.get("action").is_none(), "a room order was given an action: {body}");
}
/// The Account page's button is a renewal by construction (the renew
/// endpoint), and a plan row bought from the window carries the row's
/// own verb — so the panel drawn before the vendor is reached already
/// has the right steps.
#[tokio::test]
async fn an_order_placed_here_says_what_it_does_to_the_plan() {
let plain = rig(FakeGunnar::default(), FakeMonetize::verifying()).await;
let session = plain.tenant("vera", "vera");
let (status, body) = request(plain.router.clone(), http::Method::POST, "/api/account/subscription/renew", Some(&session)).await;
assert_eq!(status, http::StatusCode::OK, "{body}");
let seen: serde_json::Value = serde_json::from_str(&body).expect("an attempt");
assert_eq!(seen["action"], serde_json::json!("renew"), "{body}");
let plans = rig(FakeGunnar::default(), FakeMonetize { plans: Some(true), ..FakeMonetize::verifying() }).await;
let session = plans.tenant("alice", "alice");
for (row, action) in [("prolong-1m", "prolong"), ("100gb-12m", "upgrade")] {
let (status, body) = request_body(
plans.router.clone(),
http::Method::POST,
"/api/account/subscription/buy-bundle",
Some(&session),
&format!(r#"{{"bundle":"{row}"}}"#),
)
.await;
assert_eq!(status, http::StatusCode::OK, "{body}");
let seen: serde_json::Value = serde_json::from_str(&body).expect("an attempt");
assert_eq!(seen["action"], serde_json::json!(action), "{row}: {body}");
}
}
/// **And the other half: a real fault is still a fault.** The rule is about
/// refusals, which are policy, and not about a channel that broke or a
/// credential that was rejected. Dressing one of those as "you have no
/// subscription" would hide an outage behind a sentence about billing.
#[tokio::test]
async fn a_monetize_that_is_actually_broken_still_reads_as_broken() {
for (code, why) in [
(tonic::Code::Unavailable, "the ledger is not open"),
(tonic::Code::Unauthenticated, "this bearer is not ours"),
(tonic::Code::Internal, "a panic while pricing"),
] {
let rig = rig(FakeGunnar::default(), FakeMonetize::refusing_the_quote(code, why)).await;
let session = rig.tenant("vera", "vera");
let (status, body) = request(
rig.router.clone(),
http::Method::GET,
"/api/account/subscription",
Some(&session),
)
.await;
assert_ne!(status, http::StatusCode::OK, "{code} was served as an answer: {body}");
}
}
/// gunnar refusing to vouch is the end of it — nothing is bought.
#[tokio::test]
async fn a_refusal_to_vouch_buys_nothing() {
let rig = rig(
FakeGunnar {
refuse_ticket: true,
..Default::default()
},
FakeMonetize::verifying(),
)
.await;
let session = rig.tenant("alice", "alice");
let (status, body) = request(
rig.router.clone(),
http::Method::POST,
"/api/account/subscription/renew",
Some(&session),
)
.await;
assert_eq!(status, http::StatusCode::FORBIDDEN, "{body}");
assert!(
rig.purchase_calls().is_empty(),
"the appliance would not vouch and an order was placed anyway"
);
}
/// ★ **A reference from another account is refused, and the refusal says
/// nothing about it.**
///
/// A reference is `<product>/<tenant>/<date>` — three guessable parts. A
/// tenant who could learn from the refusal whether one exists, or whose it
/// is, would have an oracle over every attempt on the box. The message must
/// name only the caller's own account.
///
/// Since 2026-10-04 the refusal is monetize's: the console asks with the
/// session's own namespace and the appliance's ticket, and monetize answers
/// only that tenant's attempts.
#[tokio::test]
async fn a_reference_from_another_account_is_refused_and_names_nothing() {
let rig = rig(FakeGunnar::default(), FakeMonetize::verifying()).await;
let session = rig.tenant("alice", "alice");
let (status, body) = request(
rig.router.clone(),
http::Method::GET,
"/api/account/subscription/status?reference=gunnar%2Fbeta%2F2026-09-17",
Some(&session),
)
.await;
assert_eq!(status, http::StatusCode::NOT_FOUND, "{body}");
assert!(
!body.contains("beta"),
"the refusal repeats the namespace that was probed for: {body}"
);
// ★ monetize decides (thin layer, owner 2026-10-04): asked with the
// session's OWN namespace and the appliance's read ticket, and it is
// monetize's uniform NOT_FOUND that comes back.
let asked = rig.purchase_calls();
assert_eq!(asked.len(), 1, "{asked:?}");
assert_eq!(asked[0].what, "Purchase.Status");
assert_eq!(asked[0].tenant, "alice", "the status was not scoped to the session's account");
assert!(asked[0].ticket_len > 0, "the status carried no ticket");
}
/// And the caller's OWN reference is followed, so the check above is a
/// scope and not a route that always refuses. It survives a restart
/// because it remembers nothing: the tenant half of the comparison is
/// re-derived from the session's principal on every call.
#[tokio::test]
async fn the_accounts_own_attempt_is_followed_without_anything_being_remembered() {
let rig = rig(FakeGunnar::default(), FakeMonetize::verifying()).await;
let session = rig.tenant("alice", "alice");
// Never placed through this router — nothing here has a ring, and that
// is the point: `api::increase` would 404 this after a restart and send
// a just-charged customer to a console they cannot reach.
let (status, body) = request(
rig.router.clone(),
http::Method::GET,
"/api/account/subscription/status?reference=gunnar%2Falice%2F2026-09-17",
Some(&session),
)
.await;
assert_eq!(status, http::StatusCode::OK, "{body}");
assert!(body.contains("\"state\":\"paid\""), "{body}");
}
/// A passkey bound to no principal holds no namespace, so there is nothing
/// of its own to renew — and this console must not resolve a name to find
/// one, which is GUI-001.
#[tokio::test]
async fn a_session_bound_to_no_principal_has_nothing_to_renew() {
let rig = rig(FakeGunnar::default(), FakeMonetize::verifying()).await;
let session = rig.unbound("nobody");
let (status, body) = request(
rig.router.clone(),
http::Method::GET,
"/api/account/subscription",
Some(&session),
)
.await;
assert_eq!(status, http::StatusCode::NOT_FOUND, "{body}");
assert!(rig.minted_for().is_empty());
assert!(rig.purchase_calls().is_empty());
}
/// A renewal ticket still names no caps, which is what gunnar refuses an
/// order ticket for having none of — the two paths must not converge.
#[tokio::test]
async fn a_renewal_ticket_still_names_no_caps() {
let rig = rig(FakeGunnar::default(), FakeMonetize::verifying()).await;
let session = rig.tenant("alice", "alice");
let (status, _) = request(
rig.router.clone(),
http::Method::POST,
"/api/account/subscription/renew",
Some(&session),
)
.await;
assert_eq!(status, http::StatusCode::OK);
let minted = rig.minted_for();
assert_eq!(minted[0].purpose, "renew");
assert!(
minted[0].caps.is_empty(),
"a renewal ticket carried caps, which gunnar refuses and monetize refuses again"
);
}
/// ★ **The provider's cost never reaches a customer's browser, even though
/// monetize hands it to this console.**
///
/// `PurchaseStatusResponse.rows` carries each ledger step's raw JSON, and a
/// `Reserved` step serialises the whole `Purchase` — `cost` and all. That
/// is monetize's shape and it is not wrong there: `Purchase.Status` is an
/// operator verb gated by no ticket. What it means is that the promise a
/// customer never sees the provider's MARGIN is kept **here**, by this
/// handler building `Attempt` field by field and never forwarding `rows`.
///
/// Asserted on the bytes rather than the struct, the way
/// `increase::the_offer_carries_no_cost` is: a field added to `Attempt`
/// tomorrow, or a handler that serialised the whole message because it was
/// easier, both fail here. The struct's shape is a claim; the body is the
/// evidence.
#[tokio::test]
async fn the_providers_cost_never_reaches_a_customer() {
let rig = rig(FakeGunnar::default(), FakeMonetize::verifying()).await;
let session = rig.tenant("alice", "alice");
let (status, body) = request(
rig.router.clone(),
http::Method::GET,
"/api/account/subscription/status?reference=gunnar%2Falice%2F2026-09-17",
Some(&session),
)
.await;
assert_eq!(status, http::StatusCode::OK, "{body}");
assert!(
!body.contains(&COST_MINOR.to_string()),
"the provider's cost reached the customer's browser: {body}"
);
assert!(
!body.to_lowercase().contains("cost"),
"the response names a cost: {body}"
);
assert!(
!body.contains("Reserved") && !body.contains("\"rows\""),
"the raw ledger steps were forwarded to a customer; they are the operator's \
vocabulary and they carry the margin: {body}"
);
}
/// ★ **A history is read with a READ ticket, and the amount is monetize's
/// WORDS — never a number this console formatted.**
///
/// Two properties in one test because they fail independently.
///
/// The ticket must be a `read`: a ticket is single-use, so listing a
/// history with a renewal ticket would burn the one the customer came to
/// renew with — and it would make the appliance vouch for a verb nobody was
/// performing.
///
/// The RAW price must not reach the browser, and this one is subtle
/// because monetize is not wrong to send it: `PurchaseAttempt.price` is
/// what the customer was charged, and their own money is theirs to see.
/// The console cannot render `Money { minor, currency }` — that needs the
/// division by 100 and the currency symbol it may not contain — so
/// monetize now also sends `price_words`, and this carries that verbatim
/// while still dropping the machine's number.
///
/// **The fixture makes the two tellable apart on purpose.** `PRICE_WORDS`
/// is `"49.99 EUR"` and `PRICE_MINOR` is `1_000`; a console that formatted
/// the number itself would produce `10.00`, not `49.99`, so "carried
/// monetize's words" and "did the arithmetic anyway" cannot both pass.
///
/// This assertion was `!body.contains("price")` until `price_words`
/// existed, which a legitimate field name now trips. Narrowed to the raw
/// minor units rather than deleted, and strengthened in exchange: the words
/// must be PRESENT and must be monetize's exact string.
#[tokio::test]
async fn a_history_is_read_with_a_read_ticket_and_the_amount_is_monetizes_words() {
let rig = rig(FakeGunnar::default(), FakeMonetize::verifying()).await;
let session = rig.tenant("alice", "alice");
let (status, body) = request(
rig.router.clone(),
http::Method::GET,
"/api/account/subscription/history",
Some(&session),
)
.await;
assert_eq!(status, http::StatusCode::OK, "{body}");
assert!(body.contains("\"steps\":6"), "a fold, not a log: {body}");
assert!(body.contains("sold"), "{body}");
let minted = rig.minted_for();
assert_eq!(minted.len(), 1, "one ticket per call: {minted:?}");
assert_eq!(
minted[0].purpose, "read",
"a history was listed with a {:?} ticket; a ticket is single-use, so that burns \
the one the customer came to renew with",
minted[0].purpose
);
assert!(
minted[0].caps.is_empty(),
"a read buys nothing, so its ticket covers no amount"
);
assert_ne!(
minted[0].fingerprint, rig.console_fingerprint,
"the read ticket was asked for with the CONSOLE's server-wide admin key"
);
assert!(
!body.contains(&PRICE_MINOR.to_string()),
"the machine's raw Money amount reached the browser; this console cannot render \
one without the arithmetic and the currency literal it may not contain: {body}"
);
assert!(
!body.to_lowercase().contains("cost"),
"the provider's margin reached the customer: {body}"
);
assert!(
body.contains(PRICE_WORDS),
"monetize's own rendered amount did not reach the customer, so the pane can tell \
them what they hold but not what it cost: {body}"
);
}
// ── the window and the button ──────────────────────────────────────────
/// **The meters the account already holds and the bundle does not touch
/// stay in the target**, because monetize's own resolution keeps them and
/// the ticket comparison is over the whole map.
#[test]
fn a_target_keeps_the_meters_the_bundle_does_not_raise() {
let held = std::collections::BTreeMap::from([
("pack_bytes".to_owned(), 10u64),
("cache_bytes".to_owned(), 5u64),
]);
let delta = std::collections::BTreeMap::from([("pack_bytes".to_owned(), 3u64)]);
assert_eq!(
target_caps(&held, &delta),
std::collections::BTreeMap::from([
("pack_bytes".to_owned(), 13u64),
("cache_bytes".to_owned(), 5u64),
])
);
}
/// An account nothing was ever sold for holds no caps, and every meter the
/// bundle names starts from nothing rather than from a guess.
#[test]
fn a_meter_the_account_does_not_hold_yet_starts_from_nothing() {
let delta = std::collections::BTreeMap::from([("pack_bytes".to_owned(), 7u64)]);
assert_eq!(
target_caps(&Default::default(), &delta),
std::collections::BTreeMap::from([("pack_bytes".to_owned(), 7u64)])
);
}
/// **Saturating, as monetize's is.** A cap near the ceiling must ask the
/// product an absurd question and be told no, rather than wrap into a small
/// one and be told yes.
#[test]
fn a_target_that_would_overflow_saturates_instead_of_wrapping() {
let held = std::collections::BTreeMap::from([("pack_bytes".to_owned(), u64::MAX - 1)]);
let delta = std::collections::BTreeMap::from([("pack_bytes".to_owned(), 1_000u64)]);
assert_eq!(target_caps(&held, &delta)["pack_bytes"], u64::MAX);
}
/// ★ **The ticket covers the caps monetize will resolve the bundle to, and
/// that is every meter the account holds — not just the one being raised.**
///
/// monetize resolves a bundle into caps BEFORE it checks the ticket, and
/// `monetize_embed::verify_ticket` compares the ticket's caps to the
/// order's with `!=` over the whole map. The account here holds
/// `cache_bytes` that the bundle does not touch; a ticket minted from the
/// delta alone, or from the raised meter alone, is refused by a real
/// monetize with a sentence about meter counts that nobody could act on.
/// [`target_caps`] is the arithmetic and this is the claim it exists for.
#[tokio::test]
async fn a_bundle_ticket_covers_every_cap_the_order_will_resolve_to() {
let rig = rig(FakeGunnar::default(), FakeMonetize::verifying()).await;
let session = rig.tenant("alice", "alice");
let (status, body) = request_body(
rig.router.clone(),
http::Method::POST,
"/api/account/subscription/buy-bundle",
Some(&session),
&format!("{{\"bundle\":\"{BUNDLE_ID}\"}}"),
)
.await;
assert_eq!(status, http::StatusCode::OK, "{body}");
let minted = rig.minted_for();
let order = minted
.iter()
.find(|m| m.purpose == "order")
.unwrap_or_else(|| panic!("no order ticket was minted: {minted:?}"));
assert_eq!(
order.caps,
std::collections::BTreeMap::from([
("pack_bytes".to_owned(), HELD_PACK + BUNDLE_DELTA),
("cache_bytes".to_owned(), HELD_CACHE),
]),
"the appliance vouched for caps monetize will not resolve this bundle to, so the order is refused at the far side"
);
assert_ne!(
order.fingerprint, rig.console_fingerprint,
"the order ticket was asked for with the CONSOLE's server-wide admin key"
);
// The window is read with a READ ticket, never the order's: a ticket is
// single-use, and spending the order's on a listing would leave nothing
// to buy with.
assert!(
minted.iter().any(|m| m.purpose == "read" && m.caps.is_empty()),
"the window was not read with a read ticket naming no caps: {minted:?}"
);
let bought: Vec<Asked> = rig
.purchase_calls()
.into_iter()
.filter(|a| a.what == "Purchase.Start")
.collect();
assert_eq!(bought.len(), 1, "{bought:?}");
assert_eq!(bought[0].bundle, BUNDLE_ID, "the id was not echoed back");
assert!(
!bought[0].caps_named,
"the order named a bundle AND caps; monetize refuses that rather than letting one of them win silently"
);
assert!(!bought[0].renew, "an order is not a renewal");
assert!(bought[0].ticket_len > 0, "the order carried no ticket");
}
/// The window is monetize's words, carried, and read with a ticket that
/// buys nothing.
#[tokio::test]
async fn the_window_is_read_with_a_read_ticket_and_is_monetizes_own_words() {
let rig = rig(FakeGunnar::default(), FakeMonetize::verifying()).await;
let session = rig.tenant("alice", "alice");
let (status, body) = request(
rig.router.clone(),
http::Method::GET,
"/api/account/subscription/bundles",
Some(&session),
)
.await;
assert_eq!(status, http::StatusCode::OK, "{body}");
assert!(body.contains(BUNDLE_ID), "{body}");
assert!(body.contains(BUNDLE_LABEL), "the label was reworded: {body}");
assert!(
body.contains(BUNDLE_PRICE_WORDS),
"monetize's rendered amount did not reach the customer, and this console cannot compose one: {body}"
);
assert!(
!body.contains(&PRICE_MINOR.to_string()),
"a raw Money amount reached the browser: {body}"
);
// ★ The row monetize cannot serve is not in the tenant's window either:
// not its id, not its price, and not the masked sentence. Owner's
// ruling 2026-09-20 — see `api::increase::Window`.
for dead in [DEAD_BUNDLE_ID, "1000.00 SEK", DEAD_BUNDLE_WORDS, "maintenance"] {
assert!(!body.contains(dead), "a dead card reached the tenant ({dead:?}): {body}");
}
assert!(body.contains("\"withheld\":1"), "{body}");
let minted = rig.minted_for();
assert_eq!(minted.len(), 1, "one ticket per call: {minted:?}");
assert_eq!(minted[0].purpose, "read", "a window buys nothing");
assert!(minted[0].caps.is_empty(), "a read covers no amount");
}
/// ★ **The tenant's own window, while monetize is still reading it, says
/// `pending`** with this server's two lines and no rows — the shape the
/// console draws a spinner for and asks again, never the typed order
/// (test2, 2026-10-05). Still read with a read ticket, and nothing else.
#[tokio::test]
async fn the_tenants_window_while_monetize_reads_it_is_pending() {
let rig = rig(FakeGunnar::default(), FakeMonetize { reading: true, ..FakeMonetize::verifying() }).await;
let session = rig.tenant("alice", "alice");
let (status, body) = request(rig.router.clone(), http::Method::GET, "/api/account/subscription/bundles", Some(&session)).await;
assert_eq!(status, http::StatusCode::OK, "{body}");
let w: serde_json::Value = serde_json::from_str(&body).expect("json");
assert_eq!(w["pending"], crate::api::increase::AWAITING_PROVIDER, "{body}");
assert_eq!(w["late"], crate::api::increase::PROVIDER_LATE, "{body}");
assert_eq!(w["offers"], serde_json::json!([]), "{body}");
assert_eq!(w["withheld"], 0, "{body}");
let minted = rig.minted_for();
assert_eq!(minted.len(), 1, "{minted:?}");
assert_eq!(minted[0].purpose, "read");
}
/// ★ **A deployment that sells PLANS: the gauge reaches the page, and a
/// prolong row past its horizon is KEPT with monetize's sentence** — it is
/// the customer's way on ("choose a 12-month plan"), not a size the box
/// cannot hold (2026-10-05).
#[tokio::test]
async fn the_tenants_window_carries_the_plan_and_keeps_a_refused_plan_row() {
let rig = rig(FakeGunnar::default(), FakeMonetize { plans: Some(false), ..FakeMonetize::verifying() }).await;
let session = rig.tenant("alice", "alice");
let (status, body) = request(rig.router.clone(), http::Method::GET, "/api/account/subscription/bundles", Some(&session)).await;
assert_eq!(status, http::StatusCode::OK, "{body}");
let w: serde_json::Value = serde_json::from_str(&body).expect("json");
assert_eq!(w["plan"]["label"], "10 GiB · 1 month", "{body}");
assert_eq!(w["plan"]["days_left"], 6);
assert_eq!(w["plan"]["banner_action"], "prolong-1m");
assert_eq!(w["offers"][0]["id"], "prolong-1m");
assert_eq!(w["offers"][0]["can_serve"], false);
assert_eq!(w["offers"][0]["reason"], AHEAD_WORDS);
assert_eq!(w["offers"][0]["action"], "prolong");
assert_eq!(w["offers"][1]["action"], "upgrade");
assert_eq!(w["withheld"], 0, "a plan row is never withheld: {body}");
}
/// ★ **A prolong row is a RENEWAL: the renew ticket, no caps** — and an
/// upgrade row an ORDER over the caps monetize said it ends at.
#[tokio::test]
async fn a_prolong_row_is_bought_with_a_renew_ticket_and_an_upgrade_with_monetizes_target() {
let rig = rig(FakeGunnar::default(), FakeMonetize { plans: Some(true), ..FakeMonetize::verifying() }).await;
let session = rig.tenant("alice", "alice");
let (status, body) =
request_body(rig.router.clone(), http::Method::POST, "/api/account/subscription/buy-bundle", Some(&session), r#"{"bundle":"prolong-1m"}"#).await;
assert_eq!(status, http::StatusCode::OK, "{body}");
let minted = rig.minted_for();
let last = minted.last().expect("a ticket");
assert_eq!((last.purpose.as_str(), last.caps.len()), ("renew", 0), "{minted:?}");
let (status, body) =
request_body(rig.router.clone(), http::Method::POST, "/api/account/subscription/buy-bundle", Some(&session), r#"{"bundle":"100gb-12m"}"#).await;
assert_eq!(status, http::StatusCode::OK, "{body}");
let minted = rig.minted_for();
let last = minted.last().expect("a ticket");
assert_eq!(last.purpose, "order");
assert_eq!(last.caps.get("pack_bytes"), Some(&(100 * BUNDLE_DELTA)), "the ticket covers monetize's target: {:?}", last.caps);
assert_eq!(last.caps.get("cache_bytes"), Some(&HELD_CACHE));
}
/// ★ **A bundle the window does not offer spends nothing.**
///
/// A stale page is the ordinary way to press a button for an offer that has
/// been withdrawn. The refusal happens before the order ticket is minted,
/// so nothing was vouched for and `Purchase.Start` was never dialled.
#[tokio::test]
async fn a_bundle_the_window_does_not_offer_spends_nothing() {
let rig = rig(FakeGunnar::default(), FakeMonetize::verifying()).await;
let session = rig.tenant("alice", "alice");
let (status, body) = request_body(
rig.router.clone(),
http::Method::POST,
"/api/account/subscription/buy-bundle",
Some(&session),
r#"{"bundle":"one-nobody-declared"}"#,
)
.await;
assert_ne!(status, http::StatusCode::OK, "{body}");
assert!(
!rig.minted_for().iter().any(|m| m.purpose == "order"),
"the appliance vouched for an order that was about to be refused"
);
assert!(
!rig.purchase_calls()
.iter()
.any(|a| a.what == "Purchase.Start"),
"monetize was asked to sell something this console could not name"
);
}
/// A stranger reaches nothing, and no credential of this console's is spent
/// answering them.
#[tokio::test]
async fn an_anonymous_visitor_reaches_no_renewal_route() {
for (method, uri) in [
(http::Method::GET, "/api/account/subscription"),
(http::Method::POST, "/api/account/subscription/renew"),
(http::Method::GET, "/api/account/subscription/bundles"),
(http::Method::POST, "/api/account/subscription/buy-bundle"),
(
http::Method::GET,
"/api/account/subscription/status?reference=gunnar/alice/2026-09-17",
),
] {
let rig = rig(FakeGunnar::default(), FakeMonetize::verifying()).await;
let (status, body) =
request(rig.router.clone(), method.clone(), uri, None).await;
assert_eq!(
status,
http::StatusCode::UNAUTHORIZED,
"{method} {uri} answered a stranger: {body}"
);
assert!(rig.minted_for().is_empty());
assert!(rig.purchase_calls().is_empty());
}
}
/// **"You have 10 GiB → 20 GiB after payment"**, out of the numbers the
/// order was made against: the fact the appliance read and the target the
/// ticket covers — for the raised meter only.
#[tokio::test]
async fn a_bundle_order_carries_before_and_after_for_the_meter_it_raises() {
let rig = rig(FakeGunnar::default(), FakeMonetize::verifying()).await;
let session = rig.tenant("alice", "alice");
let (status, body) = request_body(
rig.router.clone(),
http::Method::POST,
"/api/account/subscription/buy-bundle",
Some(&session),
&format!("{{\"bundle\":\"{BUNDLE_ID}\"}}"),
)
.await;
assert_eq!(status, http::StatusCode::OK, "{body}");
let a: serde_json::Value = serde_json::from_str(&body).expect("json");
assert_eq!(a["before"]["pack_bytes"], HELD_PACK);
assert_eq!(a["after"]["pack_bytes"], HELD_PACK + BUNDLE_DELTA);
assert!(a["before"].get("cache_bytes").is_none(), "a meter the order does not raise is not its news: {body}");
assert_eq!(a["captured"], false);
}
/// A paid attempt, followed after the return from the vendor, carries the
/// caps the account now holds — "Done – you now have 20 GiB".
#[tokio::test]
async fn a_paid_attempt_carries_the_caps_it_granted() {
let rig = rig(FakeGunnar::default(), FakeMonetize::verifying()).await;
let session = rig.tenant("alice", "alice");
let (status, body) = request(
rig.router.clone(),
http::Method::GET,
"/api/account/subscription/status?reference=gunnar%2Falice%2F2026-09-17",
Some(&session),
)
.await;
assert_eq!(status, http::StatusCode::OK, "{body}");
let a: serde_json::Value = serde_json::from_str(&body).expect("json");
assert_eq!(a["state"], "paid");
assert_eq!(a["after"]["pack_bytes"], HELD_PACK + BUNDLE_DELTA);
}
/// **"Cancel order"**: an order nobody has paid is closed on the ledger
/// with the customer's reason; a paid one is not the customer's to cancel
/// here; another account's reference names nothing and closes nothing.
#[tokio::test]
async fn an_unpaid_order_is_cancelled_and_a_paid_one_is_not() {
let unpaid = FakeMonetize { unpaid: true, ..FakeMonetize::verifying() };
// ★ monetize's Cancel closes it (thin layer, owner 2026-10-04). Red
// before: the console sent `Abandon` — a REFUND of a captured
// attempt, which monetize refuses for an unpaid one — so the button
// could never succeed.
let closed = unpaid.cancelled.clone();
let unpaid_rig = rig(FakeGunnar::default(), unpaid).await;
let session = unpaid_rig.tenant("alice", "alice");
let body = r#"{"reference":"gunnar/alice/2026-09-17"}"#;
let (status, answer) = request_body(unpaid_rig.router.clone(), http::Method::POST, "/api/account/subscription/cancel", Some(&session), body).await;
assert_eq!(status, http::StatusCode::OK, "{answer}");
assert!(answer.contains("\"state\":\"released\""), "{answer}");
assert_eq!(
*closed.lock().unwrap(),
vec![("gunnar/alice/2026-09-17".to_owned(), monetize_site_buy::relay::CANCELLED_BY_CUSTOMER.to_owned())]
);
// Somebody else's order: refused naming nothing, nothing closed.
let (status, answer) = request_body(unpaid_rig.router.clone(), http::Method::POST, "/api/account/subscription/cancel", Some(&session), r#"{"reference":"gunnar/beta/2026-09-17"}"#).await;
assert_eq!(status, http::StatusCode::NOT_FOUND, "{answer}");
assert!(!answer.contains("beta"), "{answer}");
assert_eq!(closed.lock().unwrap().len(), 1);
// Paid: not the customer's button.
let paid = FakeMonetize::verifying();
let paid_closed = paid.abandoned.clone();
let paid_cancelled = paid.cancelled.clone();
let paid_rig = rig(FakeGunnar::default(), paid).await;
let session = paid_rig.tenant("alice", "alice");
let (status, answer) = request_body(paid_rig.router.clone(), http::Method::POST, "/api/account/subscription/cancel", Some(&session), body).await;
// The shared door's status for monetize's FAILED_PRECONDITION (`rules::http_status_of`,
// Holger's too): 409, where this console's own door said 400 (A28 H19, P4b). The words
// are monetize's, unchanged, and the web shows them either way.
assert_eq!(status, http::StatusCode::CONFLICT, "{answer}");
assert!(answer.contains("\"code\":\"failedprecondition\"") && answer.contains("captured"), "{answer}");
assert!(paid_closed.lock().unwrap().is_empty(), "a paid order was abandoned by the customer");
assert!(paid_cancelled.lock().unwrap().is_empty(), "monetize closed a paid order");
}
// ── a private Gunnar, ordered by a PROVEN account (one shop, 2026-10-07) ──
/// The bytes the fake gunnar mints for every ticket.
const MINTED: &[u8] = b"{\"v\":1,\"a-real-ticket-is-signed-json\":true}";
fn estate_starts(rig: &Rig) -> Vec<Asked> {
rig.monetize_asked.lock().expect("the ask log").iter().filter(|a| a.what == "EstateOrder.Start").cloned().collect()
}
/// ★ **A private Gunnar is ordered by its PROVEN account** (one shop, owner
/// 2026-10-07): the signed-in person's own account — whatever the form
/// names — and an `estate` ticket gunnar minted for it over the PERSON's
/// own key (never the console's), relayed to monetize's `Start` untouched;
/// where to pay comes back, or monetize's sentence.
#[tokio::test]
async fn a_private_gunnar_is_ordered_with_the_accounts_estate_ticket_over_its_own_key() {
let rig = rig(FakeGunnar::default(), FakeMonetize::verifying()).await;
let session = rig.tenant("alice", "alice");
let (status, body) = request_body(rig.router.clone(), http::Method::POST, "/auth/private/start", Some(&session), r#"{"company":"acme","email":"a@example.com","account":"mallory"}"#).await;
assert_eq!(status, http::StatusCode::OK, "{body}");
let j: serde_json::Value = serde_json::from_str(&body).expect("JSON");
assert!(j["url"].as_str().is_some_and(|u| u.starts_with("https://")), "{j}");
let minted = rig.minted_for();
assert_eq!(minted.len(), 1, "{minted:?}");
assert_eq!((minted[0].namespace.as_str(), minted[0].purpose.as_str()), ("alice", "estate"), "the person's own account, the estate verb");
assert!(minted[0].caps.is_empty(), "an estate ticket names no caps");
assert_ne!(minted[0].fingerprint, rig.console_fingerprint, "over the person's own key, never the console's");
let started = estate_starts(&rig);
assert_eq!(started.len(), 1);
assert_eq!((started[0].tenant.as_str(), started[0].bundle.as_str()), ("alice", "acme"), "the session's account, not the form's");
assert_eq!(started[0].ticket_len, MINTED.len(), "the ticket relayed as minted");
let (status, body) = request_body(rig.router.clone(), http::Method::POST, "/auth/private/start", Some(&session), r#"{"company":"taken","email":"a@example.com"}"#).await;
assert_eq!(status, http::StatusCode::CONFLICT, "{body}");
assert!(body.contains("taken.gunnar.rs is taken"), "monetize's sentence, whole: {body}");
// ★ The e-mail is not changed in the buy (owner 2026-10-22): signed in, the page sends
// none — the order is the account's, on its ticket, and monetize mails the account's own.
let (status, body) = request_body(rig.router.clone(), http::Method::POST, "/auth/private/start", Some(&session), r#"{"company":"bolt"}"#).await;
assert_eq!(status, http::StatusCode::OK, "{body}");
let started = estate_starts(&rig);
assert_eq!((started.len(), started[2].tenant.as_str(), started[2].ticket_len), (3, "alice", MINTED.len()), "the account's own order, on its ticket");
}
/// ★ **An instance is grown by its PROVEN account** (one shop 4, owner
/// 2026-10-07): "Grow" on the sign-up page reads the instance's ladder
/// and asks a step on the signed-in account's `estate` ticket (minted over
/// the person's own key, relayed untouched); where to pay comes back, with
/// monetize's words. Someone else's instance: monetize's sentence, 403.
/// Nobody signed in: sign in first — nothing is dialled.
#[tokio::test]
async fn an_instance_is_grown_on_its_accounts_estate_ticket() {
let rig = rig(FakeGunnar::default(), FakeMonetize::verifying()).await;
let session = rig.tenant("alice", "alice");
let asked = |what: &str| rig.monetize_asked.lock().expect("the ask log").iter().filter(|a| a.what == what).cloned().collect::<Vec<Asked>>();
let (status, body) = request(rig.router.clone(), http::Method::GET, "/auth/private/ladder?company=acme", Some(&session)).await;
assert_eq!(status, http::StatusCode::OK, "{body}");
let j: serde_json::Value = serde_json::from_str(&body).expect("JSON");
assert_eq!((j["laid"].as_bool(), j["current"].as_u64(), j["steps"].as_array().map(Vec::len)), (Some(true), Some(0), Some(2)), "{j}");
// The site's kept new-estate ladder (LADDER-2) reads on its own clock, with no company and
// no ticket; the instance's ladder is the request's.
let ladders: Vec<Asked> = asked("EstateOrder.Ladder").into_iter().filter(|a| !a.bundle.is_empty()).collect();
assert_eq!((ladders[0].bundle.as_str(), ladders[0].ticket_len), ("acme", MINTED.len()), "the instance, on the account's ticket");
let (status, body) = request_body(rig.router.clone(), http::Method::POST, "/auth/private/change", Some(&session), r#"{"company":"acme","main_data_gib":30}"#).await;
assert_eq!(status, http::StatusCode::OK, "{body}");
let j: serde_json::Value = serde_json::from_str(&body).expect("JSON");
assert!(j["url"].as_str().is_some_and(|u| u.starts_with("https://")), "{j}");
assert!(j["words"].as_str().is_some_and(|w| w.contains("main's data disks grow from 10 to 30 GB")), "monetize's words: {j}");
let changes = asked("EstateOrder.ChangePlan");
assert_eq!((changes[0].bundle.as_str(), changes[0].ticket_len, changes[0].caps.get("main_data_gib")), ("acme", MINTED.len(), Some(&30)));
let minted = rig.minted_for();
assert!(minted.iter().all(|m| m.namespace == "alice" && m.purpose == "estate" && m.fingerprint != rig.console_fingerprint), "{minted:?}");
let (status, body) = request_body(rig.router.clone(), http::Method::POST, "/auth/private/change", Some(&session), r#"{"company":"bolt","main_data_gib":30}"#).await;
assert_eq!(status, http::StatusCode::FORBIDDEN, "{body}");
assert!(body.contains("not your account's"), "monetize's sentence: {body}");
let before = asked("EstateOrder.ChangePlan").len();
let (status, body) = request_body(rig.router.clone(), http::Method::POST, "/auth/private/change", None, r#"{"company":"acme","main_data_gib":30}"#).await;
assert_eq!(status, http::StatusCode::UNAUTHORIZED, "{body}");
assert!(body.contains("Sign in"), "{body}");
let (status, _) = request(rig.router.clone(), http::Method::GET, "/auth/private/ladder?company=acme", None).await;
assert_eq!(status, http::StatusCode::UNAUTHORIZED);
assert_eq!(asked("EstateOrder.ChangePlan").len(), before, "nothing dialled for nobody");
}
/// ★ **"Restore front" on an instance's row** (S8, RESUME 5.4 #5): a sold estate whose front
/// was lost is made again from the front's daily backup with its keys — monetize's
/// `EstateOrder.RestoreFront`, on the signed-in account's own `estate` ticket (minted over
/// the person's key), relayed untouched; monetize's words come back. Someone else's
/// instance: monetize's sentence, 403. Nobody signed in: nothing is dialled.
#[tokio::test]
async fn a_lost_front_is_restored_on_its_accounts_estate_ticket() {
let rig = rig(FakeGunnar::default(), FakeMonetize::verifying()).await;
let session = rig.tenant("alice", "alice");
let asked = |what: &str| rig.monetize_asked.lock().expect("the ask log").iter().filter(|a| a.what == what).cloned().collect::<Vec<Asked>>();
let (status, body) = request_body(rig.router.clone(), http::Method::POST, "/auth/private/restore-front", Some(&session), r#"{"company":"acme"}"#).await;
assert_eq!(status, http::StatusCode::OK, "{body}");
let j: serde_json::Value = serde_json::from_str(&body).expect("JSON");
assert!(j["words"].as_str().is_some_and(|w| w.contains("restored from its backup")), "monetize's words: {j}");
assert_eq!((j["moved"].as_bool(), j["public_ipv4"].as_str()), (Some(false), Some("198.51.100.7")), "{j}");
let asks = asked("EstateOrder.RestoreFront");
assert_eq!((asks.len(), asks[0].bundle.as_str(), asks[0].ticket_len), (1, "acme", MINTED.len()), "the instance, on the account's ticket");
let minted = rig.minted_for();
assert!(minted.iter().all(|m| m.namespace == "alice" && m.purpose == "estate" && m.fingerprint != rig.console_fingerprint), "{minted:?}");
let (status, body) = request_body(rig.router.clone(), http::Method::POST, "/auth/private/restore-front", Some(&session), r#"{"company":"bolt"}"#).await;
assert_eq!(status, http::StatusCode::FORBIDDEN, "{body}");
assert!(body.contains("not your account's"), "{body}");
let (status, _) = request_body(rig.router.clone(), http::Method::POST, "/auth/private/restore-front", Some(&session), r#"{"company":"../x"}"#).await;
assert_eq!(status, http::StatusCode::BAD_REQUEST);
let before = asked("EstateOrder.RestoreFront").len();
let (status, body) = request_body(rig.router.clone(), http::Method::POST, "/auth/private/restore-front", None, r#"{"company":"acme"}"#).await;
assert_eq!(status, http::StatusCode::UNAUTHORIZED, "{body}");
assert_eq!(asked("EstateOrder.RestoreFront").len(), before, "nothing dialled for nobody");
}
/// ★ **Your purchases lead with the instance** (one shop, owner
/// 2026-10-07): the account's own — gunnar mints its `estate` ticket over
/// the PERSON's key, monetize answers on it — its private Gunnar first
/// (name, address, step and what is laid, the month, the paid period,
/// Grow, Open). An account WITHOUT room here gets the owner's two lines for
/// its Storage page — "Your Gunnar is acme.gunnar.rs", linked, and "Want
/// space here on <this host> too? Buy 10GB/month" — and no room line.
#[tokio::test]
async fn your_purchases_lead_with_the_instance_on_the_accounts_own_ticket() {
let rig = rig(FakeGunnar::default(), FakeMonetize::verifying()).await;
let session = rig.tenant("alice", "alice");
let (status, body) = request(rig.router.clone(), http::Method::GET, "/api/account/purchases", Some(&session)).await;
assert_eq!(status, http::StatusCode::OK, "{body}");
let j: serde_json::Value = serde_json::from_str(&body).expect("JSON");
let acme = &j["estates"][0];
assert_eq!((acme["company"].as_str(), acme["url"].as_str(), acme["step"].as_str(), acme["paid_until"].as_str()), (Some("acme"), Some("https://acme.gunnar.rs"), Some("Mini"), Some("2027-04-07")), "{j}");
assert_eq!(acme["grow_url"], "/signup?plan=grow&instance=acme", "{j}");
assert_eq!(acme["renew_url"], "/signup?plan=private&renew=acme", "renew or stop renewing (A52/A51): {j}");
assert_eq!(j["rooms"].as_array().map(Vec::len), Some(0), "{j}");
assert_eq!(j["storage_lines"][0]["words"], "Your Gunnar is acme.gunnar.rs", "{j}");
assert_eq!(j["storage_lines"][0]["url"], "https://acme.gunnar.rs", "{j}");
assert_eq!(j["buy"]["words"], "Want space here on console.example too? Buy 10GB/month", "{j}");
assert_eq!(j["buy"]["url"], "/signup?plan=10gb", "{j}");
// A ZERO, never a size that ends in one ("40 GB data" is a size).
let zero = |needle: &str| body.match_indices(needle).any(|(i, _)| !body[..i].ends_with(|c: char| c.is_ascii_digit()));
for never in ["0 B", "0 GB"] {
assert!(!zero(never), "{never:?} in {body}");
}
assert!(!body.contains("free"), "\"free\" in {body}");
let minted = rig.minted_for();
assert_eq!((minted[0].namespace.as_str(), minted[0].purpose.as_str()), ("alice", "estate"));
assert_ne!(minted[0].fingerprint, rig.console_fingerprint, "over the person's own key");
let asked: Vec<Asked> = rig.monetize_asked.lock().unwrap().iter().filter(|a| a.what == "EstateOrder.Purchases").cloned().collect();
assert_eq!((asked[0].tenant.as_str(), asked[0].ticket_len), ("alice", MINTED.len()));
// An account that bought room here too: its room is a line, and no
// Storage lines take its room's place.
let rig = rig_with_room().await;
let session = rig.tenant("carol", "carol");
let (status, body) = request(rig.router.clone(), http::Method::GET, "/api/account/purchases", Some(&session)).await;
assert_eq!(status, http::StatusCode::OK, "{body}");
let j: serde_json::Value = serde_json::from_str(&body).expect("JSON");
assert_eq!(j["rooms"][0]["cap_bytes"], 10_737_418_240u64, "{j}");
assert_eq!(j["storage_lines"].as_array().map(Vec::len), Some(0), "{j}");
assert!(j["buy"].is_null(), "{j}");
// Nobody signed in has no purchases.
let (status, _) = request(rig.router.clone(), http::Method::GET, "/api/account/purchases", None).await;
assert_eq!(status, http::StatusCode::UNAUTHORIZED);
}
/// ★ **Only an account monetize KNOWS to hold no room here is shown what it holds
/// elsewhere** (round 5.2, the UI matrix): an account at this shop that bought nothing
/// yet gets the way to room here; an account monetize knows nothing of — its room is
/// Gunnar's own, as on a box where the operator gave it — gets no Storage lines and no
/// offer, so its Storage page keeps its room.
#[tokio::test]
async fn only_an_account_monetize_knows_without_room_is_shown_what_it_holds_elsewhere() {
let rig = rig(FakeGunnar::default(), FakeMonetize::verifying()).await;
let alma = rig.tenant("alma", "alma");
let (status, body) = request(rig.router.clone(), http::Method::GET, "/api/account/purchases", Some(&alma)).await;
assert_eq!(status, http::StatusCode::OK, "{body}");
let j: serde_json::Value = serde_json::from_str(&body).expect("JSON");
assert_eq!(j["estates"].as_array().map(Vec::len), Some(0), "{j}");
assert_eq!(j["buy"]["words"], "Want space here on console.example too? Buy 10GB/month", "an account at this shop: {j}");
let erik = rig.tenant("erik", "erik");
let (status, body) = request(rig.router.clone(), http::Method::GET, "/api/account/purchases", Some(&erik)).await;
assert_eq!(status, http::StatusCode::OK, "{body}");
let j: serde_json::Value = serde_json::from_str(&body).expect("JSON");
assert_eq!(j["storage_lines"].as_array().map(Vec::len), Some(0), "{j}");
assert!(j["buy"].is_null(), "nothing known of erik: his Storage keeps his room: {j}");
}
async fn rig_with_room() -> Rig {
rig(FakeGunnar::default(), FakeMonetize { has_room: true, ..FakeMonetize::verifying() }).await
}
/// ★ **gunnar refusing to vouch orders nothing** (a member of an account,
/// a name the box holds no namespace for): ONE plain sentence — gunnar's
/// own names principals and grants and stays in the log (wiring trace B,
/// private.rs raw refusal) — and monetize is never asked to hold anything.
#[tokio::test]
async fn a_refusal_to_vouch_orders_no_private_gunnar() {
let rig = rig(FakeGunnar { refuse_ticket: true, ..FakeGunnar::default() }, FakeMonetize::verifying()).await;
let session = rig.tenant("alice", "alice");
let (status, body) = request_body(rig.router.clone(), http::Method::POST, "/auth/private/start", Some(&session), r#"{"company":"acme","email":"a@example.com"}"#).await;
assert_eq!(status, http::StatusCode::FORBIDDEN, "{body}");
assert!(body.contains(crate::auth::private::ONLY_THE_ADMIN_BUYS), "the plain sentence: {body}");
assert!(!body.contains("grant") && !body.contains("namespace") && !body.contains("principal"), "never gunnar's own words: {body}");
assert!(estate_starts(&rig).is_empty(), "nothing was ordered");
}
/// ★ **i54a run32 A3, at the routes**: a private Gunnar is bought by an ACCOUNT — a
/// sign-in with a namespace of its own. The kit's (or the operator's) machine sign-in
/// holds none: gunnar answers the `estate` ticket `NOT_FOUND`, and the page says there is
/// no account to buy for (404) — never the admin sentence (403) the mock met — and
/// monetize is never asked. The same console, signed in as the account, orders.
#[tokio::test]
async fn a_private_gunnar_is_bought_by_an_account_with_a_namespace_never_a_bare_sign_in() {
let held = FakeGunnar { namespaces: Some(vec!["a3buyer".to_owned()]), ..FakeGunnar::default() };
let rig = rig(held, FakeMonetize::verifying()).await;
let kit = rig.tenant("kit-claude", "kit-claude");
let (status, body) = request_body(rig.router.clone(), http::Method::POST, "/auth/private/start", Some(&kit), r#"{"company":"acme","email":"a@example.com"}"#).await;
assert_eq!(status, http::StatusCode::NOT_FOUND, "{body}");
assert!(body.contains(crate::auth::private::NO_ACCOUNT_TO_BUY_FOR), "no account to buy for: {body}");
assert!(!body.contains("admin") && !body.contains("namespace"), "{body}");
assert!(estate_starts(&rig).is_empty(), "nothing was ordered for a sign-in with no account");
let buyer = rig.tenant("a3buyer", "a3buyer");
let (status, body) = request_body(rig.router.clone(), http::Method::POST, "/auth/private/start", Some(&buyer), r#"{"company":"acme","email":"a@example.com"}"#).await;
assert_eq!(status, http::StatusCode::OK, "the account orders: {body}");
let started = estate_starts(&rig);
assert_eq!((started.len(), started[0].tenant.as_str()), (1, "a3buyer"), "{started:?}");
}
// ── TERMINATE a private Gunnar (owner 2026-10-12): admin only, a fresh login, the name typed back ──
fn terminates(rig: &Rig) -> Vec<Asked> {
rig.monetize_asked.lock().expect("the ask log").iter().filter(|a| a.what == "EstateOrder.Terminate").cloned().collect()
}
/// A tenant whose login is NOT user-verified recently: not stepped up.
fn stale_tenant(rig: &Rig, user: &str) -> String {
rig.webauthn.plant_credential_for_tests(user, crate::role::Role::Viewer);
rig.webauthn.bind_principal(&format!("planted-{user}-viewer"), user, 0).expect("the planted credential pins");
rig.webauthn.make_session(user, crate::role::Role::Viewer, false)
}
/// ★ **The account's admin, freshly logged in, types the name back**: the instance is ended
/// with no backup (owner 2026-10-22; A32-27: no backup word on the wire), an older page's
/// `backup` word changing nothing — each call on the account's own `estate` ticket.
#[tokio::test]
async fn the_admin_terminates_a_private_gunnar_with_no_backup() {
let rig = rig(FakeGunnar::default(), FakeMonetize::verifying()).await;
let session = rig.tenant("alice", "alice");
let (status, body) = request_body(rig.router.clone(), http::Method::POST, "/auth/private/terminate", Some(&session), r#"{"company":"acme","confirm":"acme"}"#).await;
assert_eq!(status, http::StatusCode::OK, "{body}");
let j: serde_json::Value = serde_json::from_str(&body).expect("JSON");
assert_eq!(j["done"].as_bool(), Some(true), "{j}");
assert!(j.get("backup_offered").is_none() && j.get("backups").is_none(), "{j}");
let (status, body) = request_body(rig.router.clone(), http::Method::POST, "/auth/private/terminate", Some(&session), r#"{"company":"acme","confirm":"acme","backup":"skip"}"#).await;
assert_eq!(status, http::StatusCode::OK, "{body}");
let j: serde_json::Value = serde_json::from_str(&body).expect("JSON");
assert_eq!(j["done"].as_bool(), Some(true), "{j}");
let asked = terminates(&rig);
assert_eq!(asked.len(), 2, "{asked:?}");
assert!(asked.iter().all(|a| a.bundle == "acme" && a.tenant == "acme" && a.ticket_len > 0), "the company, the typed name and the account's ticket: {asked:?}");
let minted = rig.minted_for();
assert!(minted.iter().all(|m| m.namespace == "alice" && m.purpose == "estate"), "{minted:?}");
}
/// ★ **The wrong name typed back ends nothing** — refused here, monetize never asked.
#[tokio::test]
async fn a_terminate_with_the_wrong_name_typed_back_is_refused() {
let rig = rig(FakeGunnar::default(), FakeMonetize::verifying()).await;
let session = rig.tenant("alice", "alice");
for typed in ["acne", "", "bolt"] {
let (status, body) = request_body(rig.router.clone(), http::Method::POST, "/auth/private/terminate", Some(&session), &format!(r#"{{"company":"acme","confirm":"{typed}","backup":"skip"}}"#)).await;
assert_eq!(status, http::StatusCode::BAD_REQUEST, "{typed:?}: {body}");
assert!(body.contains("acme") && body.contains("Nothing was ended"), "{body}");
}
assert!(terminates(&rig).is_empty(), "monetize was never asked");
}
/// ★ **Admin only**: a sign-in gunnar will not vouch for as the account's admin (a member),
/// and nobody signed in, terminate nothing.
#[tokio::test]
async fn a_non_admin_terminates_nothing() {
let rig = rig(FakeGunnar { refuse_ticket: true, ..FakeGunnar::default() }, FakeMonetize::verifying()).await;
let session = rig.tenant("alice", "alice");
let (status, body) = request_body(rig.router.clone(), http::Method::POST, "/auth/private/terminate", Some(&session), r#"{"company":"acme","confirm":"acme","backup":"skip"}"#).await;
assert_eq!(status, http::StatusCode::FORBIDDEN, "{body}");
assert!(body.contains(crate::auth::private::ONLY_THE_ADMIN_BUYS), "{body}");
let (status, body) = request_body(rig.router.clone(), http::Method::POST, "/auth/private/terminate", None, r#"{"company":"acme","confirm":"acme","backup":"skip"}"#).await;
assert_eq!(status, http::StatusCode::UNAUTHORIZED, "{body}");
assert!(terminates(&rig).is_empty(), "monetize was never asked");
}
/// ★ **A recent fresh login**: a session whose user-verified login is not recent is asked to
/// log in again, before anything is minted or asked.
#[tokio::test]
async fn a_terminate_needs_a_fresh_login() {
let rig = rig(FakeGunnar::default(), FakeMonetize::verifying()).await;
let session = stale_tenant(&rig, "alice");
let (status, body) = request_body(rig.router.clone(), http::Method::POST, "/auth/private/terminate", Some(&session), r#"{"company":"acme","confirm":"acme","backup":"skip"}"#).await;
assert_eq!(status, http::StatusCode::FORBIDDEN, "{body}");
assert!(body.contains("fresh touch of your key"), "{body}");
assert!(body.contains(&format!("\"code\":\"{}\"", crate::auth::STEP_UP_CODE)), "machine-readable: {body}");
assert!(terminates(&rig).is_empty() && rig.minted_for().is_empty(), "nothing minted or asked");
}
/// ★ ACCT-3 (owner 2026-10-29): **the site's admin is never told "A Free account cannot
/// push"** — gunnar.rs's own admin read it on his Account page. A site admin on a Free
/// account pushes (gunnar-core `EntitlementState::Free.allows_write()`, and his
/// server-wide grant); a person without that grant who buys for a Free account is still
/// told, and a member never is (his account's admin buys).
#[test]
fn a_site_admin_on_free_is_never_gated_and_a_free_buyer_still_is() {
use crate::auth::Standing;
use crate::role::Role;
let individual = Standing { account: "alma".into(), role: "individual".into(), admin: String::new() };
let account_admin = Standing { account: "acme".into(), role: "admin".into(), admin: "alma".into() };
let member = Standing { account: "acme".into(), role: "employee".into(), admin: "alma".into() };
assert!(!free_gates_push(Role::Admin, &individual, true), "the site's admin on Free pushes");
assert!(!free_gates_push(Role::Admin, &account_admin, true), "the site's admin who also runs an account pushes");
for site_role in [Role::Viewer, Role::Operator] {
assert!(free_gates_push(site_role, &individual, true), "{site_role}: a Free individual is still told");
assert!(free_gates_push(site_role, &account_admin, true), "{site_role}: a Free account's admin is still told");
assert!(!free_gates_push(site_role, &individual, false), "{site_role}: a plan pushes");
assert!(!free_gates_push(site_role, &member, true), "{site_role}: a member is never told; his admin buys");
}
}
}