crates/monetize-ui-server/tests/api.rs
//! The REST surface against an in-process mock of the monetize services the
//! console reads, written on the generated server traits. Every endpoint
//! refuses without a session; the operator `Set` and an order need Admin; an
//! order capping an undeclared meter is refused here, by name, before monetize
//! ever hears of it.
//!
//! The mock also serves `Events.Watch`, because the console's feed is no longer
//! built by polling: it is monetize's own stream, fanned out over SSE. Two tests
//! at the bottom hold that to account on the WIRE — an event published by the
//! mock must come out of `/api/events` with its fields intact, and a monetize
//! that cannot stream must make the console SAY it dropped to polling rather
//! than going quietly stale.
use std::sync::{Arc, Mutex};
use axum::body::Body;
use axum::Router;
use http::{Method, Request, StatusCode};
use http_body_util::BodyExt;
use monetize_proto as pb;
use monetize_proto::entitlement_server::{Entitlement, EntitlementServer};
use monetize_proto::estate_admin_server::{EstateAdmin, EstateAdminServer};
use monetize_proto::events_server::{Events, EventsServer};
use monetize_proto::guardian_server::{Guardian, GuardianServer};
use monetize_proto::ledger_server::{Ledger, LedgerServer};
use monetize_proto::products_server::{Products, ProductsServer};
use monetize_proto::purchase_server::{Purchase, PurchaseServer};
use monetize_proto::query_ledger_request::By;
use monetize_proto::usage_server::{Usage, UsageServer};
use monetize_ui_server::app::App;
use monetize_ui_server::auth::{RelyingParty, StoredCred, Webauthn};
use monetize_ui_server::grpc::Monetize;
use monetize_ui_server::role::Role;
use monetize_ui_server::sse::EventHub;
use monetize_ui_server::state::StateStore;
use monetize_ui_server::serve;
use serde_json::{json, Value};
use tonic::{Request as TReq, Response, Status};
use tower::ServiceExt;
const BEARER: &str = "the-suite-bearer";
const ADMIN_TOKEN: &str = "break-glass";
/// **The mock monetize signs for real.** A "signed" badge that the console
/// computes itself is only testable against genuine Ed25519: a fixture that
/// hands out `vec![7; 64]` cannot tell a working verification from a missing
/// one, which is exactly the hole this replaces.
fn signing_key() -> &'static ed25519_dalek::SigningKey {
static KEY: std::sync::OnceLock<ed25519_dalek::SigningKey> = std::sync::OnceLock::new();
KEY.get_or_init(|| ed25519_dalek::SigningKey::from_bytes(&[3u8; 32]))
}
/// Fill in a fact's signature the way monetize would, over the ONE canonical
/// form (`monetize_embed::signing::fact_message`) — the same bytes the console
/// verifies, and the same bytes a product verifies.
fn sign(mut f: pb::EntitlementFact) -> pb::EntitlementFact {
use ed25519_dalek::Signer as _;
let rust = pb_to_embed(&f);
f.signature = signing_key().sign(&monetize_embed::signing::fact_message(&rust)).to_bytes().to_vec();
f
}
fn pb_to_embed(f: &pb::EntitlementFact) -> monetize_embed::EntitlementFact {
monetize_embed::EntitlementFact {
tenant: monetize_embed::TenantId(f.tenant.clone()),
plan: f.plan.clone(),
state: match f.state() {
pb::State::Paid => monetize_embed::State::Paid,
pb::State::Grace => monetize_embed::State::Grace,
pb::State::Suspended => monetize_embed::State::Suspended,
pb::State::Retention => monetize_embed::State::Retention,
pb::State::Free | pb::State::Unspecified => monetize_embed::State::Free,
},
paid_until_unix_ms: f.paid_until_unix_ms,
issued_unix_ms: f.issued_unix_ms,
issued_signature: f.issued_signature.clone(),
caps: f.caps.clone(),
source: f.source.clone(),
signature: f.signature.clone(),
}
}
struct Mock {
facts: Mutex<Vec<pb::EntitlementFact>>,
sets: Mutex<Vec<pb::SetEntitlementRequest>>,
exports: Mutex<Vec<()>>,
starts: Mutex<Vec<pb::StartPurchaseRequest>>,
quotes: Mutex<Vec<pb::QuoteRequest>>,
recent_limits: Mutex<Vec<u32>>,
/// Every `Usage.Trend` the BFF made, so a test can prove WHICH meter and
/// WHICH tenant were asked for rather than only that something came back.
trends_asked: Mutex<Vec<pb::TrendRequest>>,
fleets_asked: Mutex<Vec<String>>,
sales_asked: Mutex<Vec<pb::SalesRequest>>,
/// What `Products.List` says about reachability. `None` is a monetize that
/// does not report it — which must reach the console as `unknown`, never as
/// a green dot the console invented.
reachable: Mutex<Option<pb::Reachability>>,
sales: Mutex<Vec<pb::Sale>>,
/// What `Usage.Fleet` answers. `None` = the unmeasured arm, which is what a
/// gunnar predating `Admin.Capacity` gives.
fleet: Mutex<Option<pb::FleetResponse>>,
/// What `Usage.Trend` answers. `None` = the "not enough history" arm.
trend: Mutex<Option<pb::TrendResponse>>,
/// What `Events.Watch` hands out. The test pushes into it; the BFF's stream
/// driver is on the other end.
events: tokio::sync::broadcast::Sender<pb::Event>,
/// How many `Events.Watch` streams are open. A test must not publish before
/// the BFF has subscribed, or it would be asserting on a race.
watchers: std::sync::atomic::AtomicUsize,
/// **A monetize that cannot stream**, the way an older build or a broken
/// deployment is: `Watch` is refused outright.
refuse_watch: bool,
/// Every `EstateAdmin.Draw` the BFF made — the picture is per PRODUCT, so a
/// test has to be able to say which estate was asked for.
estates_asked: Mutex<Vec<String>>,
/// What `Draw` answers, keyed by product. A product absent from here is
/// NOT_FOUND, exactly as monetize refuses one it does not serve.
estates: Mutex<std::collections::BTreeMap<String, String>>,
}
impl Default for Mock {
fn default() -> Mock {
Mock {
facts: Mutex::new(Vec::new()),
sets: Mutex::new(Vec::new()),
exports: Mutex::new(Vec::new()),
starts: Mutex::new(Vec::new()),
quotes: Mutex::new(Vec::new()),
recent_limits: Mutex::new(Vec::new()),
trends_asked: Mutex::new(Vec::new()),
fleets_asked: Mutex::new(Vec::new()),
sales_asked: Mutex::new(Vec::new()),
reachable: Mutex::new(None),
sales: Mutex::new(Vec::new()),
fleet: Mutex::new(None),
trend: Mutex::new(None),
events: tokio::sync::broadcast::channel(64).0,
watchers: std::sync::atomic::AtomicUsize::new(0),
refuse_watch: false,
estates_asked: Mutex::new(Vec::new()),
estates: Mutex::new(
[
// gunnar's board, with a chip on it.
("gunnar".to_string(), "<svg xmlns=\"http://www.w3.org/2000/svg\"><text>gunnar estate</text><rect/></svg>\n".to_string()),
// holger's: a deployment that declares no machines. A
// BOARD, titled and empty — the case the live box is in.
("holger".to_string(), "<svg xmlns=\"http://www.w3.org/2000/svg\"><text>holger estate</text></svg>\n".to_string()),
]
.into(),
),
}
}
}
fn fact(tenant: &str, state: pb::State, source: &str) -> pb::EntitlementFact {
sign(pb::EntitlementFact { tenant: tenant.into(), plan: "gunnar/team/paid/2026-09-01".into(), state: state as i32, paid_until_unix_ms: Some(1_800_000_000_000), caps: [("pack_bytes".to_string(), 10u64 << 30)].into(), source: source.into(), signature: vec![], issued_unix_ms: None, issued_signature: vec![] })
}
/// A fact whose signature is 64 bytes of the wrong thing — the shape a restore
/// under another key, a hand-edited store or a future write-path bug leaves
/// behind. `!signature.is_empty()` calls this signed; verifying it does not.
fn corrupted_fact(tenant: &str, state: pb::State, source: &str) -> pb::EntitlementFact {
let mut f = fact(tenant, state, source);
f.signature = vec![7; 64];
f
}
fn row(reference: &str, seq: u32, kind: &str) -> pb::LedgerRow {
pb::LedgerRow { reference: reference.into(), seq, at_unix_ms: 1_000 + u64::from(seq), kind: kind.into(), json: "{}".into() }
}
impl Mock {
fn seeded() -> Arc<Mock> {
let m = Mock::default();
*m.facts.lock().unwrap() = vec![
fact("team/paid", pb::State::Paid, "payment:invoice:gunnar/team/paid/2026-09-01"),
fact("team/grace", pb::State::Grace, "operator"),
// Signed with sixty-four bytes of nothing. The old badge called this
// signed; the console now checks and says otherwise.
corrupted_fact("team/forged", pb::State::Paid, "operator"),
];
Arc::new(m)
}
/// The same seeded mock, but one whose `Events.Watch` is refused — an older
/// monetize, or a broken one.
fn seeded_refusing_events() -> Arc<Mock> {
let m = Mock { refuse_watch: true, ..Mock::default() };
let seeded = Mock::seeded();
*m.facts.lock().unwrap() = seeded.facts.lock().unwrap().clone();
Arc::new(m)
}
fn usages(&self) -> Vec<pb::UsageReport> {
vec![
pb::UsageReport { tenant: "team/paid".into(), meters: vec![pb::Meter { name: "pack_bytes".into(), value: 3 << 30 }], measured_at_unix_ms: 5 },
pb::UsageReport { tenant: "team/free".into(), meters: vec![pb::Meter { name: "pack_bytes".into(), value: 1 << 20 }], measured_at_unix_ms: 6 },
]
}
/// The ledger the mock holds. `team/declined` is the point: NO fact names it
/// (the vendor said no, so no entitlement was ever written), and it is the
/// NEWEST row — so a "recent" built from the entitlement table cannot show
/// it, and one built from the ledger's time index must show it first.
fn rows(&self) -> Vec<pb::LedgerRow> {
let mut v = vec![
row("gunnar/team/paid/2026-09-01", 0, "Reserved"),
row("gunnar/team/paid/2026-09-01", 1, "Captured"),
row("gunnar/team/open/2026-09-04", 0, "Reserved"),
];
v.push(pb::LedgerRow { reference: "gunnar/team/declined/2026-09-02".into(), seq: 0, at_unix_ms: 9_000, kind: "Declined".into(), json: "{}".into() });
v
}
}
struct Svc(Arc<Mock>);
#[tonic::async_trait]
impl Products for Svc {
async fn list(&self, _: TReq<pb::ListProductsRequest>) -> Result<Response<pb::ListProductsResponse>, Status> {
// One wall and one line item, so a test can tell the two apart.
let meters = vec![
pb::MeterInfo { name: "pack_bytes".into(), meaning: "packed object bytes".into(), enforcement: pb::Enforcement::Enforced as i32, scope: pb::Scope::Tenant as i32, unit: "disk_gib_month".into() },
pb::MeterInfo { name: "lfs_bytes".into(), meaning: "LFS bytes".into(), enforcement: pb::Enforcement::Measured as i32, scope: pb::Scope::Tenant as i32, unit: String::new() },
// One PER-PRODUCT meter, so a test can tell the axis apart: nobody
// measures a tenant's cores, and a console that listed this beside
// the per-tenant bars would be inviting a sum of nothing.
pb::MeterInfo { name: "cpu_millicores".into(), meaning: "CPU bought for the box".into(), enforcement: pb::Enforcement::Provisioned as i32, scope: pb::Scope::Product as i32, unit: "cpu_core_month".into() },
];
let reachable = self.0.reachable.lock().unwrap().clone();
Ok(Response::new(pb::ListProductsResponse {
products: vec![pb::ProductInfo {
id: "gunnar".into(),
meters,
reachable,
tenant_renewal: true,
// A metered appliance: the door was proven this tick. The
// community case is the interesting one and it is tested where
// the probe lives (`monetize_server::poller`).
metered_door: Some(pb::MeteredDoor { state: "enforcing".into(), checked_at_unix_ms: 1, detail: "Entitlement.List answered".into() }),
}],
vendors: vec!["invoice".into()],
clouds: vec!["sandbox".into(), "upcloud".into()],
// The wire on 2026-09-05: one sandbox disk against a 10 EUR lifetime
// ceiling. `ceiling_monthly_minor` is None on purpose — a limit not
// set must reach the console as null, never as 0. And, second in the
// server's (alphabetical) order, the cloud a sale really spends at
// since 2026-09-14 — the one the BFF used to drop.
spend: vec![
pb::CloudSpend { provider: "sandbox".into(), currency: "EUR".into(), spent_minor: 226, spent_this_month_minor: Some(226), ceiling_total_minor: Some(1000), ceiling_monthly_minor: None },
pb::CloudSpend { provider: "upcloud".into(), currency: "EUR".into(), spent_minor: 0, spent_this_month_minor: Some(0), ceiling_total_minor: Some(2500), ceiling_monthly_minor: Some(2500) },
],
// The terms: 22.00 SEK per GiB-month of disk in SE, nothing for cores.
prices: vec![pb::PriceList { product: "gunnar".into(), market: pb::Market::Se as i32, currency: pb::Currency::Sek as i32, units: [("disk_gib_month".to_string(), 2200u64), ("cpu_core_month".to_string(), 0)].into() }],
policy: Some(pb::Policy { period: "month".into(), grace_days: 14, retention_days: 90 }),
hosts: [("gunnar".to_string(), "gunnar-se-sto1".to_string())].into(),
default_cloud: "upcloud".into(),
vendor_modes: vec![],
estate_vendors: Default::default(),
}))
}
}
/// **The fleet, as the ONE monetize has heard it.** The console names no estate
/// of its own: `/api/estates/{estate}/map` resolves the estate to its product
/// here, then asks the ONE to draw that product. `idun.rs` is declared but its
/// product is not drawn here — the 404 is monetize's own.
#[tonic::async_trait]
impl Guardian for Svc {
async fn cases(&self, _: TReq<pb::GuardianCasesRequest>) -> Result<Response<pb::GuardianCasesResponse>, Status> {
let estate = |instance: &str, product: &str| pb::GuardianEstate {
instance: instance.into(),
product: product.into(),
boxes: vec![pb::GuardianBox { role: "main".into(), heard_unix_ms: 1, silent: false, ..Default::default() }],
..Default::default()
};
Ok(Response::new(pb::GuardianCasesResponse {
estates: vec![estate("gunnar.rs", "gunnar"), estate("holger.rs", "holger"), estate("idun.rs", "idun")],
}))
}
async fn press(&self, _: TReq<pb::GuardianPressRequest>) -> Result<Response<pb::GuardianCase>, Status> {
Err(Status::unimplemented("not in this mock"))
}
async fn set_auto_fix(&self, _: TReq<pb::GuardianAutoFixRequest>) -> Result<Response<pb::GuardianAutoFixResponse>, Status> {
Err(Status::unimplemented("not in this mock"))
}
async fn set_hold(&self, _: TReq<pb::GuardianHoldRequest>) -> Result<Response<pb::GuardianHoldResponse>, Status> {
Err(Status::unimplemented("not in this mock"))
}
}
/// **The estate picture, as monetize serves it.** The mock renders nothing: the
/// drawing is `monetize_server::estate_svg`'s and is held to account in that
/// crate. What is under test HERE is the BFF's half — the content type, the
/// validator, and the session in front of both.
#[tonic::async_trait]
impl EstateAdmin for Svc {
async fn draw(&self, r: TReq<pb::DrawEstateRequest>) -> Result<Response<pb::DrawEstateResponse>, Status> {
let product = r.into_inner().product;
self.0.estates_asked.lock().unwrap().push(product.clone());
match self.0.estates.lock().unwrap().get(&product) {
Some(svg) => Ok(Response::new(pb::DrawEstateResponse { svg: svg.clone(), machines: Some(u32::from(svg.contains("<rect"))) })),
None => Err(Status::not_found(format!("product {product:?} is not served here; linked: [\"gunnar\", \"holger\"]"))),
}
}
}
#[tonic::async_trait]
impl Usage for Svc {
async fn read(&self, r: TReq<pb::ReadUsageRequest>) -> Result<Response<pb::ReadUsageResponse>, Status> {
let t = r.into_inner().tenant;
Ok(Response::new(pb::ReadUsageResponse { usage: self.0.usages().into_iter().find(|u| u.tenant == t) }))
}
async fn read_all(&self, _: TReq<pb::ReadAllUsageRequest>) -> Result<Response<pb::ReadAllUsageResponse>, Status> {
Ok(Response::new(pb::ReadAllUsageResponse { usages: self.0.usages() }))
}
/// **Three readings of one day, so the FRICTION fold has something to get
/// wrong.**
///
/// The BFF's fleet page folds `refused_today` out of this
/// (`monetize_product::flow::window_total`), and the failure that fold
/// exists to prevent is arithmetic: `refused_today` is a DAILY counter that
/// repeats in every reading of its day, so summing the readings reports a
/// number proportional to the poll rate. The three readings below carry
/// `2, 2, 2` for one tenant on one day — a naive sum says 6, the correct
/// fold says 2 — which is why the mock hands back three and not one.
///
/// The other tenant carries no `refused_today` at all: a product that never
/// reported the meter must not read as a tenant with no refusals.
async fn history(&self, r: TReq<pb::UsageHistoryRequest>) -> Result<Response<pb::UsageHistoryResponse>, Status> {
let tenant = r.into_inner().tenant;
const DAY_MS: u64 = 86_400_000;
let readings = if tenant == "team/paid" {
let day = 20_000 * DAY_MS;
(0..3)
.map(|tick| pb::UsageReport {
tenant: tenant.clone(),
meters: vec![pb::Meter { name: "refused_today".into(), value: 2 }],
measured_at_unix_ms: day + tick * 900_000,
})
.collect()
} else {
vec![pb::UsageReport {
tenant: tenant.clone(),
meters: vec![pb::Meter { name: "pack_bytes".into(), value: 1 << 20 }],
measured_at_unix_ms: 20_000 * DAY_MS,
}]
};
Ok(Response::new(pb::UsageHistoryResponse { readings, kept: 3 }))
}
/// **A refusal, not an empty series.**
///
/// The console draws an empty `days` as *this customer has gone quiet*, so
/// a mock that answered "no days" would make the Activity route look green
/// while saying something false about every tenant in the fixture. A test
/// that wants a real series stands up its own answer.
async fn activity(&self, _: TReq<pb::ActivityRequest>) -> Result<Response<pb::ActivityResponse>, Status> {
Err(Status::unimplemented("not in the mock"))
}
/// The rate, as monetize would fold it. The mock does not do the arithmetic
/// — `monetize::trend` is tested where it lives, and monetize-server is
/// tested where the fold reaches the wire. What is under test HERE is the
/// BFF's mapping, so the mock hands back a canned answer and RECORDS what it
/// was asked, which is the only way to prove the BFF asked for the right
/// meter and the right tenant.
/// The fleet total, canned. Same reason as `trend`: `monetize::fleet` is
/// tested where it lives and where it reaches the wire; what is under test
/// here is the BFF's mapping and the fact that it asked for the right
/// product.
async fn fleet(&self, r: TReq<pb::FleetRequest>) -> Result<Response<pb::FleetResponse>, Status> {
self.0.fleets_asked.lock().unwrap().push(r.into_inner().product);
let canned = self.0.fleet.lock().unwrap().clone();
Ok(Response::new(canned.unwrap_or(pb::FleetResponse {
meters: vec![pb::FleetMeter {
meter: "pack_bytes".into(),
tenants: 3,
used: 4 << 30,
tenants_without_reading: 1,
allowance: 20 << 30,
tenants_without_cap: 1,
oversell: "unmeasured".into(),
oversell_bytes: 0,
oversell_reason: "this gunnar has no Admin.Capacity RPC".into(),
capacity: None,
is_a_floor: true,
}],
servable_measured: false,
servable_bytes: 0,
total_bytes: 0,
servable_reason: "this gunnar has no Admin.Capacity RPC".into(),
servable_at_unix_ms: 1_700_000_000_000,
growth: String::new(),
growth_detail: String::new(),
cloud: None,
})))
}
async fn trend(&self, r: TReq<pb::TrendRequest>) -> Result<Response<pb::TrendResponse>, Status> {
let asked = r.into_inner();
self.0.trends_asked.lock().unwrap().push(asked.clone());
let canned = self.0.trend.lock().unwrap().clone();
Ok(Response::new(canned.unwrap_or(pb::TrendResponse {
enough_history: false,
needed_span_ms: 302_400_000,
window_ms: 2_592_000_000,
unmeasured_tenants: 1,
..Default::default()
})))
}
}
#[tonic::async_trait]
impl Entitlement for Svc {
async fn get(&self, r: TReq<pb::GetEntitlementRequest>) -> Result<Response<pb::GetEntitlementResponse>, Status> {
let t = r.into_inner().tenant;
// One tenant whose fact the product REFUSED, so the console's
// unacknowledged-push line has something to draw.
let pending_push = (t == "team/refused").then(|| pb::PendingPush {
since_unix_ms: 1_788_400_000_000,
attempts: 4,
last_error: "gunnar InvalidArgument: cap `pushes` is not a meter this appliance honours".into(),
refused: true,
});
Ok(Response::new(pb::GetEntitlementResponse { fact: self.0.facts.lock().unwrap().iter().find(|f| f.tenant == t).cloned(), pending_push }))
}
async fn list(&self, _: TReq<pb::ListEntitlementsRequest>) -> Result<Response<pb::ListEntitlementsResponse>, Status> {
Ok(Response::new(pb::ListEntitlementsResponse { facts: self.0.facts.lock().unwrap().clone() }))
}
async fn set(&self, r: TReq<pb::SetEntitlementRequest>) -> Result<Response<pb::SetEntitlementResponse>, Status> {
let r = r.into_inner();
// "Sign it for me": monetize signs the stored fact, so the console's
// badge on the reply is checking a real signature.
let f = sign(r.fact.clone().unwrap());
self.0.sets.lock().unwrap().push(r);
Ok(Response::new(pb::SetEntitlementResponse { fact: Some(f), pushed: true, push_error: String::new() }))
}
async fn public_key(&self, _: TReq<pb::PublicKeyRequest>) -> Result<Response<pb::PublicKeyResponse>, Status> {
let key = signing_key().verifying_key().to_bytes();
Ok(Response::new(pb::PublicKeyResponse {
public_key: key.to_vec(),
hex: key.iter().map(|b| format!("{b:02x}")).collect(),
algorithm: "ed25519".into(),
}))
}
/// One bought disk and one served from the pool, so the console is tested
/// telling them apart — a fixture with only bought resources could not.
async fn held(&self, r: TReq<pb::HeldRequest>) -> Result<Response<pb::HeldResponse>, Status> {
let r = r.into_inner();
let disk = |gib| Some(pb::Resource { kind: Some(pb::resource::Kind::Disk(pb::resource::Disk { server: "gunnar-se-sto1".into(), gib })) });
Ok(Response::new(pb::HeldResponse {
holdings: vec![
pb::Holding { reference: format!("gunnar/{}/2026-09-03", r.tenant), resource: disk(10), provider: "mockcloud".into(), id: "res-0".into() },
pb::Holding { reference: format!("gunnar/{}/2026-09-04", r.tenant), resource: disk(4), provider: String::new(), id: String::new() },
],
}))
}
}
/// The refusals a real monetize makes before any money moves, in ITS words —
/// so the BFF is tested passing them through rather than inventing a second
/// opinion. A tenant on 10 GiB ordering 1 GiB is a downgrade; `team/appliance`
/// is a gunnar predating `Admin.Capacity`; `team/sealed` is the sealed set.
fn refusal_for(r_tenant: &str, caps: &std::collections::BTreeMap<String, u64>) -> Option<Status> {
let pack = caps.get("pack_bytes").copied().unwrap_or(0);
if pack > 0 && pack < 10u64 << 30 {
return Some(Status::failed_precondition(format!("the order would SHRINK pack_bytes for {r_tenant} from 10737418240 to {pack}. Shrinking is a different verb")));
}
if r_tenant == "team/appliance" && pack == 100 << 30 {
return Some(Status::failed_precondition(
"the product cannot deliver this to team/appliance: gunnar did not answer whether it can serve \
100.0 GiB of pack_bytes: this gunnar has no Admin.Capacity RPC — it predates it, so nothing on its \
control plane can say how large its data set is. THAT IS A MEASUREMENT THAT DID NOT HAPPEN, not a \
full disk",
));
}
if r_tenant == "team/sealed" && pack == 100 << 30 {
return Some(Status::failed_precondition(
"the product cannot deliver this to team/sealed: gunnar can serve the pack_bytes cap of 100.0 \
GiB, but the order also budgets 20.0 GiB of cache_bytes on the SAME filesystem and only 10.0 GiB is \
left once the raise is served — short 10.0 GiB. AND THIS SET CANNOT GROW WHILE GUNNAR RUNS: gunnar \
has no runtime data-set growth in any build and is PID 1 with no shell, so a disk bought and \
attached to this box would be BILLED AND INVISIBLE. Serving more needs a fresh install onto a \
larger set, not another disk",
));
}
None
}
#[tonic::async_trait]
impl Purchase for Svc {
/// **THE SHELF THE OWNER HAD ON SCREEN**, 2026-09-20, answered the way
/// monetize answered it: three declared bundles, and the box can serve one.
///
/// The refusal sentence is the CUSTOMER's, which is what this rpc carries;
/// the operator's reason (the one that names `/data`, the free bytes and the
/// disk floor) never leaves monetize's own log.
async fn bundles(&self, _r: TReq<pb::ListBundlesRequest>) -> Result<Response<pb::ListBundlesResponse>, Status> {
let gib = |n: u64| std::collections::BTreeMap::from([("pack_bytes".to_owned(), n << 30)]);
let wall = "We cannot add more space to your account right now while we carry out maintenance. \
Nothing has been charged — please try again later.";
Ok(Response::new(pb::ListBundlesResponse {
bundles: vec![
pb::Bundle {
id: "100gb".into(),
label: "100.0 GiB more room".into(),
price: "1000.00 SEK".into(),
period: "month".into(),
delta: gib(100),
can_serve: false,
reason: wall.into(), ..Default::default()
},
pb::Bundle {
id: "10gb".into(),
label: "10.0 GiB more room".into(),
price: "100.00 SEK".into(),
period: "month".into(),
delta: gib(10),
can_serve: true,
reason: String::new(), ..Default::default()
},
pb::Bundle {
id: "50gb".into(),
label: "50.0 GiB more room".into(),
price: "500.00 SEK".into(),
period: "month".into(),
delta: gib(50),
can_serve: false,
reason: wall.into(), ..Default::default()
},
],
reading: false, ..Default::default()
}))
}
async fn quote(&self, r: TReq<pb::QuoteRequest>) -> Result<Response<pb::QuoteResponse>, Status> {
let r = r.into_inner();
if let Some(refused) = refusal_for(&r.tenant, &r.caps) {
// A downgrade is not a quote at all; a product's refusal IS one,
// with `can_serve = false` and the sentence.
if refused.message().contains("SHRINK") {
return Err(refused);
}
let msg = refused.message().to_owned();
self.0.quotes.lock().unwrap().push(r);
return Ok(Response::new(pb::QuoteResponse { can_serve: false, reason: msg, price: Some(pb::Money { minor: 198_000, currency: pb::Currency::Sek as i32 }), ..Default::default() }));
}
let pack = r.caps.get("pack_bytes").copied().unwrap_or(0);
let delta = pack.saturating_sub(10 << 30);
let gib = delta.div_ceil(1 << 30);
let q = pb::QuoteResponse {
reference: format!("gunnar/{}/2026-09-04+pack_bytes={pack}", r.tenant),
price: Some(pb::Money { minor: gib * 2200, currency: pb::Currency::Sek as i32 }),
cost: Some(pb::Money { minor: gib * 23, currency: pb::Currency::Eur as i32 }),
current: [("pack_bytes".to_string(), 10u64 << 30)].into(),
caps: r.caps.clone(),
delta: [("pack_bytes".to_string(), delta)].into(),
resources: vec![pb::Resource { kind: Some(pb::resource::Kind::Disk(pb::resource::Disk { server: "gunnar-se-sto1".into(), gib })) }],
can_serve: true,
reason: String::new(),
period: "month".into(),
words: String::new(),
// The operator console formats its own amount from `price`; this
// is the string a CUSTOMER-facing console renders verbatim.
price_words: format!("{}.{:02} SEK", (gib * 2200) / 100, (gib * 2200) % 100),
};
self.0.quotes.lock().unwrap().push(r);
Ok(Response::new(q))
}
async fn start(&self, r: TReq<pb::StartPurchaseRequest>) -> Result<Response<pb::StartPurchaseResponse>, Status> {
let r = r.into_inner();
if let Some(refused) = refusal_for(&r.tenant, &r.caps) {
return Err(refused);
}
if !r.vendor.is_empty() && r.vendor != "mockpay" {
return Err(Status::not_found(format!("vendor {} is not linked into this server", r.vendor)));
}
let pack = r.caps.get("pack_bytes").copied().unwrap_or(0);
let reference = format!("gunnar/{}/2026-09-04+pack_bytes={pack}", r.tenant);
// A first order (10 GiB from nothing) is PENDING at this vendor; an
// order for MORE on a tenant already paid is answered PAID with the
// signed fact, so both endings are reachable from the console.
let more = pack > 10 << 30;
let fact = more.then(|| {
let mut f = fact(&r.tenant, pb::State::Paid, &format!("payment:mockpay:{reference}"));
f.plan = reference.clone();
f.caps = r.caps.clone();
sign(f)
});
self.0.starts.lock().unwrap().push(r);
Ok(Response::new(pb::StartPurchaseResponse {
reference,
state: if more { pb::PurchaseState::Paid as i32 } else { pb::PurchaseState::Pending as i32 },
handle_vendor: "mockpay".into(),
handle_id: if more { "h-2".into() } else { "h-1".into() },
redirect_url: if more { String::new() } else { "https://pay.example/h-1".into() },
reason: if more { String::new() } else { "the customer has not authorized".into() },
// PENDING carries no fact: nothing is signed until the money is drawn.
fact,
// …and no granted sentence either, for the same reason: nothing has
// been given yet.
words: if more { "the room is yours now".into() } else { String::new() },
users: None,
}))
}
async fn abandon(&self, r: TReq<pb::AbandonPurchaseRequest>) -> Result<Response<pb::AbandonPurchaseResponse>, Status> {
let r = r.into_inner();
if r.reason.trim().is_empty() {
return Err(Status::invalid_argument("abandon needs a reason"));
}
Ok(Response::new(pb::AbandonPurchaseResponse { reference: r.reference, released: vec!["mockcloud:res-0".into()] }))
}
/// **The OPERATOR console does not read a tenant's own history**, so this
/// double answers the way a server would to a route nobody here calls.
/// `Purchase.History` is the customer pane's call and is proven over the
/// real service in `monetize-server/tests/tenant_history.rs`; a fixture here
/// would be a second, drifting copy of a fold this console never renders.
async fn history(&self, _r: TReq<pb::PurchaseHistoryRequest>) -> Result<Response<pb::PurchaseHistoryResponse>, Status> {
Err(Status::unimplemented("this double serves the operator console, which has no tenant history pane"))
}
async fn status(&self, r: TReq<pb::PurchaseStatusRequest>) -> Result<Response<pb::PurchaseStatusResponse>, Status> {
let reference = r.into_inner().reference;
Ok(Response::new(pb::PurchaseStatusResponse {
reference,
state: pb::PurchaseState::Paid as i32,
fact: self.0.facts.lock().unwrap().first().cloned(),
reason: String::new(),
rows: self.0.rows(),
// The vendor's own per-order checkout URL, as monetize now answers
// it on a STATUS read and not only on `Start`.
redirect_url: "https://mockpay.example/checkout/h-1".into(),
..Default::default()
}))
}
/// The operator console has no cancel button (it abandons); a customer's
/// cancel is proven over the real service in
/// `monetize-server/tests/order_scoping.rs`.
async fn cancel(&self, _r: TReq<pb::CancelPurchaseRequest>) -> Result<Response<pb::CancelPurchaseResponse>, Status> {
Err(Status::unimplemented("this double serves the operator console, which cancels nothing"))
}
}
#[tonic::async_trait]
impl Events for Svc {
type WatchStream = std::pin::Pin<Box<dyn tokio_stream::Stream<Item = Result<pb::Event, Status>> + Send + 'static>>;
async fn watch(&self, _: TReq<pb::WatchRequest>) -> Result<Response<Self::WatchStream>, Status> {
if self.0.refuse_watch {
return Err(Status::unimplemented("this monetize has no Events service"));
}
let rx = self.0.events.subscribe();
self.0.watchers.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
// The prologue first, exactly as the real bus does: the epoch leads
// every stream, and the BFF's driver acts on it.
let opened = tokio_stream::once(Ok(pb::Event::new(
0,
1,
pb::event::Payload::StreamOpened(pb::StreamOpened { epoch: "mock-epoch".into(), ..Default::default() }),
)));
let live = tokio_stream::StreamExt::filter_map(tokio_stream::wrappers::BroadcastStream::new(rx), |r| r.ok().map(Ok));
Ok(Response::new(Box::pin(tokio_stream::StreamExt::chain(opened, live))))
}
}
#[tonic::async_trait]
impl Ledger for Svc {
async fn query(&self, r: TReq<pb::QueryLedgerRequest>) -> Result<Response<pb::QueryLedgerResponse>, Status> {
let all = self.0.rows();
let (rows, references) = match r.into_inner().by.unwrap() {
By::Reference(x) => (all.into_iter().filter(|r| r.reference == x).collect(), vec![]),
By::Tenant(t) => {
let prefix = format!("{}/{}/", t.product, t.tenant);
let rows: Vec<_> = all.into_iter().filter(|r| r.reference.starts_with(&prefix)).collect();
let mut refs: Vec<String> = rows.iter().map(|r| r.reference.clone()).collect();
refs.dedup();
(rows, refs)
}
By::Unfinished(_) => (all.into_iter().filter(|r| r.reference.contains("/open/")).collect(), vec!["gunnar/team/open/2026-09-04".into()]),
By::Recent(limit) => {
self.0.recent_limits.lock().unwrap().push(limit);
let mut rows = all;
// Newest first, ties broken by reference then seq — what the real
// ledger's time index gives.
rows.sort_by(|a, b| b.at_unix_ms.cmp(&a.at_unix_ms).then(b.reference.cmp(&a.reference)).then(b.seq.cmp(&a.seq)));
rows.truncate(if limit == 0 { rows.len() } else { limit as usize });
let mut refs: Vec<String> = Vec::new();
for r in &rows {
if !refs.contains(&r.reference) {
refs.push(r.reference.clone());
}
}
(rows, refs)
}
};
Ok(Response::new(pb::QueryLedgerResponse { rows, references }))
}
/// Recent attempts, folded. Canned: `monetize::ledger::Sale` is tested where
/// it lives, and the fold reaching the wire is tested in monetize-server.
/// What is under test here is the BFF's mapping and the product filter it
/// passes on.
async fn sales(&self, r: TReq<pb::SalesRequest>) -> Result<Response<pb::SalesResponse>, Status> {
self.0.sales_asked.lock().unwrap().push(r.into_inner());
Ok(Response::new(pb::SalesResponse { sales: self.0.sales.lock().unwrap().clone() }))
}
async fn export(&self, _: TReq<pb::ExportBackupRequest>) -> Result<Response<pb::ExportBackupResponse>, Status> {
self.0.exports.lock().unwrap().push(());
Ok(Response::new(pb::ExportBackupResponse {
dir: "/var/lib/monetize/backup".into(),
ledger_rows: 7,
usage_readings: 3,
signing_public_key: "aa".repeat(32),
}))
}
}
/// The bearer gate the real server has: proves the BFF presents the token.
fn bearer_gate(req: TReq<()>) -> Result<TReq<()>, Status> {
match req.metadata().get("authorization").and_then(|v| v.to_str().ok()) {
Some(v) if v == format!("Bearer {BEARER}") => Ok(req),
_ => Err(Status::unauthenticated("bearer missing or wrong")),
}
}
async fn spawn_mock(mock: Arc<Mock>) -> String {
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
let s = || Svc(mock.clone());
tokio::spawn(
tonic::transport::Server::builder()
.add_service(ProductsServer::with_interceptor(s(), bearer_gate))
.add_service(UsageServer::with_interceptor(s(), bearer_gate))
.add_service(EntitlementServer::with_interceptor(s(), bearer_gate))
.add_service(LedgerServer::with_interceptor(s(), bearer_gate))
.add_service(PurchaseServer::with_interceptor(s(), bearer_gate))
.add_service(EventsServer::with_interceptor(s(), bearer_gate))
.add_service(EstateAdminServer::with_interceptor(s(), bearer_gate))
.add_service(GuardianServer::with_interceptor(s(), bearer_gate))
.serve_with_incoming(tonic::transport::server::TcpIncoming::from(listener)),
);
addr.to_string()
}
struct Rig { router: Router, mock: Arc<Mock>, webauthn: Arc<Webauthn>, _dir: tempfile::TempDir }
enum Auth<'a> { None, Token, Cookie(&'a str) }
impl Rig {
async fn up() -> Rig {
Rig::up_with(Mock::seeded()).await
}
async fn up_with(mock: Arc<Mock>) -> Rig {
let control = spawn_mock(mock.clone()).await;
let dir = tempfile::tempdir().unwrap();
let store = StateStore::open(dir.path().join("state.redb")).unwrap();
let rp = RelyingParty { rp_id: "console.example".into(), origin: "https://console.example".into(), rp_name: "monetize".into() };
let webauthn = Arc::new(Webauthn::new(rp, store).unwrap());
let monetize = Monetize::connect_lazy(&control, BEARER).unwrap();
let app = Arc::new(App { webauthn: Some(webauthn.clone()), admin_token: Some(ADMIN_TOKEN.into()), events: EventHub::spawn(monetize.clone(), std::time::Duration::from_secs(3600)), monetize, operator: None, ui_dir: None });
Rig { router: serve::router(app), mock, webauthn, _dir: dir }
}
fn cookie(&self, role: Role) -> String {
self.webauthn.mint_session("bob", role, true)
}
async fn call(&self, method: Method, path: &str, auth: Auth<'_>, body: Option<Value>) -> (StatusCode, Value, http::HeaderMap) {
let mut req = Request::builder().method(method).uri(path).header("origin", "https://console.example");
match auth {
Auth::None => {}
Auth::Token => req = req.header("authorization", format!("Bearer {ADMIN_TOKEN}")),
Auth::Cookie(c) => req = req.header("cookie", format!("monetize_ui_session={c}")),
}
let req = match body {
Some(b) => req.header("content-type", "application/json").body(Body::from(b.to_string())).unwrap(),
None => req.body(Body::empty()).unwrap(),
};
let res = self.router.clone().oneshot(req).await.unwrap();
let status = res.status();
let headers = res.headers().clone();
if headers.get("content-type").is_some_and(|v| v.to_str().unwrap_or("").starts_with("text/event-stream")) {
return (status, Value::Null, headers);
}
let bytes = res.into_body().collect().await.unwrap().to_bytes();
let value = serde_json::from_slice(&bytes).unwrap_or_else(|_| Value::String(String::from_utf8_lossy(&bytes).into_owned()));
(status, value, headers)
}
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn an_order_can_be_placed_from_the_console_and_only_by_an_admin() {
let rig = Rig::up().await;
let body = json!({ "product": "gunnar", "tenant": "team/paid", "caps": { "pack_bytes": 10u64 << 30 }, "vendor": "mockpay" });
// Anonymous, then a VIEWER: this route spends money, so both are refused and
// neither reaches monetize.
let (status, _, _) = rig.call(Method::POST, "/api/purchase", Auth::None, Some(body.clone())).await;
assert!(status == StatusCode::UNAUTHORIZED || status == StatusCode::FORBIDDEN, "got {status}");
let viewer = rig.cookie(Role::Viewer);
let (status, _, _) = rig.call(Method::POST, "/api/purchase", Auth::Cookie(&viewer), Some(body.clone())).await;
assert_eq!(status, StatusCode::FORBIDDEN, "a viewer must not spend money");
assert_eq!(rig.mock.starts.lock().unwrap().len(), 0, "a refusal must not reach monetize");
// Admin: it starts, and the console gets what it needs to send the customer on.
let (status, out, _) = rig.call(Method::POST, "/api/purchase", Auth::Token, Some(body)).await;
assert_eq!(status, StatusCode::OK, "{out}");
assert_eq!(out["reference"], format!("gunnar/team/paid/2026-09-04+pack_bytes={}", 10u64 << 30));
assert_eq!(out["state"], "PENDING");
assert_eq!(out["redirect_url"], "https://pay.example/h-1");
assert_eq!(out["handle_id"], "h-1");
assert!(out["fact"].is_null(), "PENDING signs nothing: {out}");
// What reached monetize is what was asked — the ORDER, as caps — and
// `starts_unix_ms` was omitted so the SERVER owns the clock (0 = now).
let sent = rig.mock.starts.lock().unwrap().clone();
assert_eq!(sent.len(), 1);
assert_eq!((sent[0].product.as_str(), sent[0].tenant.as_str(), sent[0].caps["pack_bytes"]), ("gunnar", "team/paid", 10u64 << 30));
assert_eq!(sent[0].starts_unix_ms, 0, "an omitted start must mean 'now', decided by monetize");
assert_eq!(sent[0].payer, "", "an omitted payer is empty, not invented");
assert_eq!(sent[0].market, 0, "an omitted market is the deployment's default, decided by monetize");
// MONETIZE'S REFUSALS PASS THROUGH IN ITS OWN WORDS — the BFF must not grow a
// second opinion about vendors. What it DOES refuse itself, by name, is a
// cap on a meter the product does not declare.
let bad_meter = json!({ "product": "gunnar", "tenant": "t", "caps": { "moon_phase": 1 }, "vendor": "mockpay" });
let (status, out, _) = rig.call(Method::POST, "/api/purchase", Auth::Token, Some(bad_meter)).await;
assert_eq!(status, StatusCode::BAD_REQUEST, "{out}");
assert!(out["message"].as_str().unwrap().contains("\"moon_phase\""), "names the meter: {out}");
assert_eq!(rig.mock.starts.lock().unwrap().len(), 1, "the refusal is the BFF's; monetize never saw it");
let bad_vendor = json!({ "product": "gunnar", "tenant": "t", "caps": { "pack_bytes": 10u64 << 30 }, "vendor": "swish" });
let (status, out, _) = rig.call(Method::POST, "/api/purchase", Auth::Token, Some(bad_vendor)).await;
assert_eq!(status, StatusCode::NOT_FOUND, "{out}");
assert!(out.to_string().contains("not linked into this server"), "{out}");
// STATUS: a reference holds slashes, so it must survive as the rest of the path.
let (status, out, _) = rig.call(Method::GET, "/api/purchase/gunnar/team%2Fpaid/2026-09-04", Auth::Cookie(&viewer), None).await;
assert_eq!(status, StatusCode::OK, "a read is a viewer's: {out}");
assert_eq!(out["state"], "PAID");
assert!(out["rows"].as_array().is_some_and(|r| !r.is_empty()), "the ledger timeline comes with it: {out}");
// …and so does the vendor's checkout link, verbatim. It is the reason a
// status read carries one at all: an operator reopening an attempt because
// the customer never got the link cannot re-run `Start` to get it back.
assert_eq!(out["redirect_url"], "https://mockpay.example/checkout/h-1", "{out}");
}
/// **An order for MORE is the same door**, and the refusals monetize makes
/// before any money moves must arrive with monetize's own sentence in them,
/// because that sentence is what the operator reads out to the customer — and
/// the QUOTE before the order says the same thing without moving anything.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn an_order_for_more_is_quoted_first_and_carries_monetizes_refusal_verbatim() {
let rig = Rig::up().await;
let viewer = rig.cookie(Role::Viewer);
// THE QUOTE: a read, so a viewer's. The delta, the price from the list,
// the cost from the provider, the iron in words — and nothing spent.
let (status, q, _) = rig.call(Method::GET, &format!("/api/quote?product=gunnar&tenant=team%2Fpaid&caps=pack_bytes%3D{}&market=SE", 100u64 << 30), Auth::Cookie(&viewer), None).await;
assert_eq!(status, StatusCode::OK, "{q}");
assert_eq!(q["delta"]["pack_bytes"].as_u64(), Some(90u64 << 30), "the 90 they lack: {q}");
assert_eq!((q["price_minor"].as_u64(), q["currency"].as_str()), (Some(90 * 2200), Some("SEK")), "1 980.00 SEK at 22.00 per GiB-month");
assert_eq!((q["cost_minor"].as_u64(), q["cost_currency"].as_str()), (Some(90 * 23), Some("EUR")), "what the provider charges us, beside it");
assert_eq!(q["resources"][0]["detail"], "90 GiB on gunnar-se-sto1");
assert_eq!((q["can_serve"].as_bool(), q["period"].as_str()), (Some(true), Some("month")));
let asked = rig.mock.quotes.lock().unwrap().clone();
assert_eq!((asked[0].tenant.as_str(), asked[0].caps["pack_bytes"], asked[0].market), ("team/paid", 100u64 << 30, pb::Market::Se as i32), "the market travels");
assert_eq!(rig.mock.starts.lock().unwrap().len(), 0, "a quote is not an order");
// A malformed caps query, and a bad market, are the BFF's own refusals, by name.
let (status, out, _) = rig.call(Method::GET, "/api/quote?product=gunnar&tenant=t&caps=pack_bytes", Auth::Cookie(&viewer), None).await;
assert_eq!(status, StatusCode::BAD_REQUEST, "{out}");
assert!(out.to_string().contains("meter=value"), "{out}");
let (status, out, _) = rig.call(Method::GET, "/api/quote?product=gunnar&tenant=t&caps=pack_bytes%3D1&market=NO", Auth::Cookie(&viewer), None).await;
assert_eq!(status, StatusCode::BAD_REQUEST, "{out}");
assert!(out["message"].as_str().unwrap().contains("\"NO\""), "{out}");
// THE ORDER. A viewer may not; an admin may, and gets the signed fact back.
let body = json!({ "product": "gunnar", "tenant": "team/paid", "caps": { "pack_bytes": 100u64 << 30 }, "vendor": "mockpay" });
let (status, out, _) = rig.call(Method::POST, "/api/purchase", Auth::Cookie(&viewer), Some(body.clone())).await;
assert_eq!(status, StatusCode::FORBIDDEN, "a viewer must not spend money: {out}");
let (status, out, _) = rig.call(Method::POST, "/api/purchase", Auth::Token, Some(body)).await;
assert_eq!(status, StatusCode::OK, "{out}");
assert_eq!(out["state"], "PAID");
assert_eq!(out["reference"], q["reference"], "the order runs under the reference the quote named");
assert_eq!(out["fact"]["signed"], true, "the fact must verify against monetize's key: {out}");
assert_eq!(out["fact"]["order"], q["reference"], "and names the order it came from");
assert!(out["fact"]["plan"].is_null(), "the console never sees the word");
// A DOWNGRADE, refused by name, in monetize's words.
let down = json!({ "product": "gunnar", "tenant": "team/paid", "caps": { "pack_bytes": 1u64 << 30 }, "vendor": "mockpay" });
let (status, out, _) = rig.call(Method::POST, "/api/purchase", Auth::Token, Some(down)).await;
// 400: `failed_precondition` is a BAD_REQUEST in this BFF's mapping (grpc::Fail).
assert_eq!(status, StatusCode::BAD_REQUEST, "{out}");
assert!(out.to_string().contains("SHRINK"), "the operator must be told it is a downgrade: {out}");
// A PRODUCT THAT CANNOT ABSORB IT. The reason is the product's own, passed
// through — a generic "precondition failed" would leave the operator with
// nothing to say to the customer. The QUOTE carries the same sentence as a
// quote, and the price of what was refused.
let (status, q, _) = rig.call(Method::GET, &format!("/api/quote?product=gunnar&tenant=team%2Fappliance&caps=pack_bytes%3D{}", 100u64 << 30), Auth::Cookie(&viewer), None).await;
assert_eq!(status, StatusCode::OK, "{q}");
assert_eq!(q["can_serve"], false);
assert!(q["reason"].as_str().unwrap().contains("MEASUREMENT THAT DID NOT HAPPEN"), "{q}");
assert_eq!(q["price_minor"].as_u64(), Some(198_000), "the price is still there beside the refusal");
let cannot = json!({ "product": "gunnar", "tenant": "team/appliance", "caps": { "pack_bytes": 100u64 << 30 }, "vendor": "mockpay" });
let (status, out, _) = rig.call(Method::POST, "/api/purchase", Auth::Token, Some(cannot)).await;
assert_eq!(status, StatusCode::BAD_REQUEST, "{out}");
assert!(out.to_string().contains("how large its data set is"), "the product's own words: {out}");
assert_eq!(out["code"], "failedprecondition", "the console reads the code to head the box correctly: {out}");
assert!(out["message"].as_str().unwrap().contains("MEASUREMENT THAT DID NOT HAPPEN"), "{out}");
// **A SEALED SET.** The whole sentence has to survive to the browser, all
// the way to its last clause: an operator who reads only the shortfall buys
// a disk, and on that box a disk is billed and invisible. The BFF must not
// shorten, summarise or re-word it — this asserts the clause that is
// furthest from the start of the string, which is the one a truncation
// would take first.
let sealed = json!({ "product": "gunnar", "tenant": "team/sealed", "caps": { "pack_bytes": 100u64 << 30 }, "vendor": "mockpay" });
let (status, out, _) = rig.call(Method::POST, "/api/purchase", Auth::Token, Some(sealed)).await;
assert_eq!(status, StatusCode::BAD_REQUEST, "{out}");
let msg = out["message"].as_str().expect("a refusal carries a message");
assert!(msg.contains("short 10.0 GiB"), "{msg}");
assert!(msg.contains("BILLED AND INVISIBLE"), "the remediation must reach the operator: {msg}");
assert!(msg.ends_with("not another disk"), "the LAST clause is the one that stops the wrong purchase: {msg}");
assert_eq!(rig.mock.starts.lock().unwrap().len(), 1, "a refused order must not be recorded as one");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn abandoning_a_purchase_needs_admin_and_a_reason() {
let rig = Rig::up().await;
let body = json!({ "reference": "gunnar/team/paid/2026-09-04", "reason": "gunnar cannot accept the fact" });
// It moves money BACK, so a viewer must not reach it.
let viewer = rig.cookie(Role::Viewer);
let (status, _, _) = rig.call(Method::POST, "/api/purchase/abandon", Auth::Cookie(&viewer), Some(body.clone())).await;
assert_eq!(status, StatusCode::FORBIDDEN, "a viewer must not move money");
// A blank reason is refused BY MONETIZE, and the words come through.
let no_reason = json!({ "reference": "gunnar/team/paid/2026-09-04", "reason": " " });
let (status, out, _) = rig.call(Method::POST, "/api/purchase/abandon", Auth::Token, Some(no_reason)).await;
assert_eq!(status, StatusCode::BAD_REQUEST, "{out}");
assert!(out.to_string().contains("reason"), "{out}");
let (status, out, _) = rig.call(Method::POST, "/api/purchase/abandon", Auth::Token, Some(body)).await;
assert_eq!(status, StatusCode::OK, "{out}");
assert_eq!(out["reference"], "gunnar/team/paid/2026-09-04");
assert_eq!(out["released"][0], "mockcloud:res-0", "what went back is reported: {out}");
}
const ENDPOINTS: &[(&str, &str)] = &[
("GET", "/api/products"), ("GET", "/api/products/gunnar/tenants"), ("GET", "/api/tenants/gunnar/team%2Fpaid"),
("GET", "/api/quote?product=gunnar&tenant=t&caps=pack_bytes%3D1"), ("GET", "/api/ledger?unfinished=1"), ("POST", "/api/entitlement/gunnar/team%2Fpaid"),
("GET", "/api/events"), ("GET", "/api/health"), ("GET", "/auth/passkey/credentials"),
("POST", "/api/backup"), ("POST", "/api/purchase"), ("GET", "/api/purchase/gunnar/t/2026-09-04"),
("POST", "/api/purchase/abandon"),
// The estate picture. On the roster because the bytes name provider ids,
// plan names and per-machine disk — an operator's business and nobody
// else's — and because a route that serves an image is the easy one to
// forget when this list is read as "the JSON API".
("GET", "/api/estates/gunnar.rs/map"),
("GET", "/api/estates/gunnar.rs/summary"),
("GET", "/api/overview"),
// The New-estate slot (OVW-CLEAN): a real purchase.
("POST", "/api/estates/new"),
];
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn backup_is_admin_only_and_takes_no_caller_chosen_path() {
let rig = Rig::up().await;
// Anonymous: refused, like every mutation.
let (status, _, _) = rig.call(Method::POST, "/api/backup", Auth::None, None).await;
assert!(status == StatusCode::UNAUTHORIZED || status == StatusCode::FORBIDDEN, "anonymous backup must be refused, got {status}");
assert_eq!(rig.mock.exports.lock().unwrap().len(), 0, "and must not reach monetize");
// A VIEWER session: this route writes to the filesystem, so a read-only
// session must not drive it.
let viewer = rig.cookie(Role::Viewer);
let (status, _, _) = rig.call(Method::POST, "/api/backup", Auth::Cookie(&viewer), None).await;
assert_eq!(status, StatusCode::FORBIDDEN, "a viewer must not take a backup");
assert_eq!(rig.mock.exports.lock().unwrap().len(), 0);
// Admin: it runs, and the DESTINATION comes from monetize, never the caller.
let (status, body, _) = rig.call(Method::POST, "/api/backup", Auth::Token, None).await;
assert_eq!(status, StatusCode::OK, "{body}");
assert_eq!(body["ledger_rows"], 7);
assert_eq!(body["usage_readings"], 3);
assert_eq!(body["dir"], "/var/lib/monetize/backup", "the path is monetize's, not a parameter");
assert_eq!(body["contains_no_secrets"], true);
assert_eq!(rig.mock.exports.lock().unwrap().len(), 1, "it really reached monetize");
}
#[tokio::test]
async fn every_endpoint_refuses_without_a_session_and_a_forged_cookie_is_no_session() {
let rig = Rig::up().await;
for (m, p) in ENDPOINTS {
let method: Method = m.parse().unwrap();
let (status, ..) = rig.call(method.clone(), p, Auth::None, Some(json!({}))).await;
assert_eq!(status, StatusCode::UNAUTHORIZED, "{m} {p} answered without a session");
let (status, ..) = rig.call(method, p, Auth::Cookie("bm90LWEtc2Vzc2lvbg.bm90LWEtbWFj"), Some(json!({}))).await;
assert_eq!(status, StatusCode::UNAUTHORIZED, "{m} {p} accepted a forged cookie");
}
let (status, body, _) = rig.call(Method::GET, "/auth/status", Auth::None, None).await;
assert_eq!(status, StatusCode::OK);
assert_eq!((body["authenticated"].as_bool(), body["passkey_enabled"].as_bool(), body["bootstrap"].as_bool()), (Some(false), Some(true), Some(true)));
}
#[tokio::test]
async fn products_are_cards_with_tenant_counts_by_state() {
let rig = Rig::up().await;
let viewer = rig.cookie(Role::Viewer);
let (status, body, _) = rig.call(Method::GET, "/api/products", Auth::Cookie(&viewer), None).await;
assert_eq!(status, StatusCode::OK, "{body}");
let card = &body["products"][0];
assert_eq!(card["id"], "gunnar");
assert_eq!(card["meters"][0]["name"], "pack_bytes");
// paid + forged (facts), grace (fact only), free (usage only) = four tenants.
assert_eq!(card["tenants"], 4);
assert_eq!((card["by_state"]["paid"].as_u64(), card["by_state"]["grace"].as_u64(), card["by_state"]["free"].as_u64()), (Some(2), Some(1), Some(1)));
assert_eq!(body["vendors"][0], "invoice");
}
#[tokio::test]
async fn a_tenant_whose_fact_the_product_refused_carries_the_pending_push() {
let rig = Rig::up().await;
let viewer = rig.cookie(Role::Viewer);
let (status, body, _) = rig.call(Method::GET, "/api/tenants/gunnar/team%2Frefused", Auth::Cookie(&viewer), None).await;
assert_eq!(status, StatusCode::OK, "{body}");
let p = &body["pending_push"];
assert_eq!(p["refused"], true, "{body}");
assert_eq!(p["attempts"], 4);
assert!(p["last_error"].as_str().unwrap().contains("not a meter this appliance honours"), "{p}");
// A tenant the product acknowledged carries nothing, so the line is not
// drawn for everybody.
let (status, ok, _) = rig.call(Method::GET, "/api/tenants/gunnar/team%2Fpaid", Auth::Cookie(&viewer), None).await;
assert_eq!(status, StatusCode::OK, "{ok}");
assert!(ok["pending_push"].is_null(), "{}", ok["pending_push"]);
}
#[tokio::test]
async fn tenants_are_joined_from_usage_and_entitlement() {
let rig = Rig::up().await;
let viewer = rig.cookie(Role::Viewer);
let (status, body, _) = rig.call(Method::GET, "/api/products/gunnar/tenants", Auth::Cookie(&viewer), None).await;
assert_eq!(status, StatusCode::OK, "{body}");
let t = body["tenants"].as_array().unwrap();
assert_eq!(t.len(), 4);
let paid = t.iter().find(|r| r["tenant"] == "team/paid").unwrap();
assert_eq!((paid["state"].as_str(), paid["order"].as_str(), paid["signed"].as_bool()), (Some("paid"), Some("gunnar/team/paid/2026-09-01"), Some(true)));
assert_eq!(paid["meters"]["pack_bytes"], 3u64 << 30);
// What they are ALLOWED rides on the row too, so `UI.md` screen 2 can sort
// tenants by who is closest to their cap without a round trip per row.
assert_eq!(paid["caps"]["pack_bytes"], 10u64 << 30, "the fact's caps, on the row: {paid}");
let free = t.iter().find(|r| r["tenant"] == "team/free").unwrap();
assert_eq!((free["state"].as_str(), free["order"].as_str()), (Some("free"), Some("")));
assert!(free["caps"].as_object().is_some_and(|c| c.is_empty()), "no fact, no caps — the product's default, which is not zero: {free}");
let grace = t.iter().find(|r| r["tenant"] == "team/grace").unwrap();
assert!(grace["measured_at_unix_ms"].is_null(), "no usage yet");
// The whole point of the badge: this fact has 64 bytes in its signature
// field and they are not monetize's. It sits in the same list, same state,
// same order — only the signature differs, and only verification can see it.
let forged = t.iter().find(|r| r["tenant"] == "team/forged").unwrap();
assert_eq!((forged["state"].as_str(), forged["signed"].as_bool()), (Some("paid"), Some(false)), "a fact signed with the wrong bytes is not signed");
let (status, body, _) = rig.call(Method::GET, "/api/products/holger/tenants", Auth::Cookie(&viewer), None).await;
assert_eq!(status, StatusCode::NOT_FOUND, "{body}");
}
#[tokio::test]
async fn a_tenant_detail_carries_the_fact_its_ledger_rows_and_the_terms() {
let rig = Rig::up().await;
let viewer = rig.cookie(Role::Viewer);
let (status, body, _) = rig.call(Method::GET, "/api/tenants/gunnar/team%2Fpaid", Auth::Cookie(&viewer), None).await;
assert_eq!(status, StatusCode::OK, "{body}");
assert_eq!(body["fact"]["source"], "payment:invoice:gunnar/team/paid/2026-09-01");
assert_eq!(body["fact"]["caps"]["pack_bytes"], 10u64 << 30);
assert_eq!(body["row"]["meters"]["pack_bytes"], 3u64 << 30);
assert_eq!(body["rows"].as_array().unwrap().len(), 2);
assert_eq!(body["rows"][1]["kind"], "Captured");
assert_eq!(body["references"][0], "gunnar/team/paid/2026-09-01");
// THE TERMS the order form prices by: this product's list and the policy.
assert_eq!(body["prices"][0]["units"]["disk_gib_month"], 2200);
assert_eq!((body["prices"][0]["market"].as_str(), body["prices"][0]["currency"].as_str()), (Some("SE"), Some("SEK")));
assert_eq!((body["policy"]["period"].as_str(), body["policy"]["grace_days"].as_u64(), body["policy"]["retention_days"].as_u64()), (Some("month"), Some(14), Some(90)));
assert!(body["plans"].is_null(), "no catalogue on the wire");
// Every meter says what unit prices it, or that none does.
let unit_of = |name: &str| body["meters"].as_array().unwrap().iter().find(|m| m["name"] == name).unwrap()["unit"].as_str().unwrap().to_owned();
assert_eq!((unit_of("pack_bytes").as_str(), unit_of("lfs_bytes").as_str()), ("disk_gib_month", ""));
// **WHAT THE TENANT IS STANDING ON.** The bars say how much of a quota is
// used; without this the page could not say what iron was under it, which is
// the question an operator has to answer before selling more.
let holdings = body["holdings"].as_array().expect("the pane needs a list").clone();
assert_eq!(holdings.len(), 2, "{body}");
// Bought: the id a reclaim would give back, and the words for the disk.
assert_eq!(holdings[0]["provisioned"], true);
assert_eq!(holdings[0]["kind"], "disk");
assert_eq!(holdings[0]["detail"], "10 GiB on gunnar-se-sto1");
assert_eq!(holdings[0]["id"], "mockcloud:res-0");
assert!(holdings[0]["reference"].as_str().is_some_and(|r| r.starts_with("gunnar/")), "{body}");
// ON HAND: real capacity the tenant is standing on that monetize does NOT
// own. It must be listed and it must be marked, or a pool-served tenant
// reads as having nothing provisioned — the opposite of the truth.
assert_eq!(holdings[1]["provisioned"], false, "a pool grant must not claim to be bought: {body}");
assert_eq!(holdings[1]["detail"], "4 GiB on gunnar-se-sto1");
assert_eq!(holdings[1]["id"], "", "nothing to give back, so no id");
}
/// **The products screen carries the terms**: every price list, the one
/// policy, and the hosts — so the console prices an order before it is placed
/// and says `free` where a product has no list.
#[tokio::test]
async fn the_products_response_carries_the_price_lists_and_the_policy() {
let rig = Rig::up().await;
let (status, body, _) = rig.call(Method::GET, "/api/products", Auth::Cookie(&rig.cookie(Role::Viewer)), None).await;
assert_eq!(status, StatusCode::OK, "{body}");
let prices = body["prices"].as_array().expect("the price lists ride on the products response");
assert_eq!(prices.len(), 1);
assert_eq!((prices[0]["product"].as_str(), prices[0]["market"].as_str(), prices[0]["currency"].as_str()), (Some("gunnar"), Some("SE"), Some("SEK")));
assert_eq!(prices[0]["units"]["disk_gib_month"], 2200);
assert_eq!(prices[0]["units"]["cpu_core_month"], 0, "a zero unit is free, and is on the wire as zero");
assert!(prices[0]["units"]["ram_gib_month"].is_null(), "an absent unit is absent — also free");
assert_eq!(body["policy"]["period"], "month");
assert_eq!(body["hosts"]["gunnar"], "gunnar-se-sto1");
}
/// (r) **The Ledger page's "recent" is the LEDGER's answer, not a reconstruction
/// from the entitlement table.**
///
/// It used to be built here: every fact whose `source` reads
/// `payment:<vendor>:<reference>`, then that reference's rows. A fact is not a
/// row, so the page could not show a DECLINED attempt (no fact was ever written
/// for it) — which is exactly the attempt an operator goes looking for. The mock
/// holds one, and it is the newest row in the ledger, so this asserts the ORDER
/// as well: the row the old path could not see must come FIRST.
#[tokio::test]
async fn the_ledger_lists_unfinished_attempts_first_then_the_newest_rows_of_the_whole_ledger() {
let rig = Rig::up().await;
let (status, body, _) = rig.call(Method::GET, "/api/ledger?unfinished=1", Auth::Cookie(&rig.cookie(Role::Viewer)), None).await;
assert_eq!(status, StatusCode::OK, "{body}");
assert_eq!(body["unfinished"][0]["reference"], "gunnar/team/open/2026-09-04");
assert_eq!(body["unfinished"][0]["rows"][0]["kind"], "Reserved");
let recent = body["recent"].as_array().unwrap();
let seen: Vec<(&str, &str)> = recent.iter().map(|r| (r["reference"].as_str().unwrap(), r["kind"].as_str().unwrap())).collect();
assert_eq!(
seen,
vec![
("gunnar/team/declined/2026-09-02", "Declined"),
("gunnar/team/paid/2026-09-01", "Captured"),
// Same millisecond as the open attempt's Reserved: the tiebreak is
// reference then seq, both descending, so the order is stated rather
// than whatever the store happened to do.
("gunnar/team/paid/2026-09-01", "Reserved"),
("gunnar/team/open/2026-09-04", "Reserved"),
],
"every row of the ledger, newest first — including the reference no fact names"
);
// And it asked monetize for a bounded page, once, rather than walking facts.
assert_eq!(*rig.mock.recent_limits.lock().unwrap(), vec![monetize_ui_server::api::RECENT_ROWS]);
}
#[tokio::test]
async fn the_operator_set_needs_admin_and_is_sent_as_source_operator_unsigned() {
let rig = Rig::up().await;
let body = json!({ "label": "trial", "state": "paid", "paid_until_unix_ms": 1_900_000_000_000u64 });
for role in [Role::Viewer, Role::Operator] {
let (status, reply, _) = rig.call(Method::POST, "/api/entitlement/gunnar/team%2Ffree", Auth::Cookie(&rig.cookie(role)), Some(body.clone())).await;
assert_eq!(status, StatusCode::FORBIDDEN, "{role}: {reply}");
assert!(reply.as_str().unwrap().contains("admin"), "the refusal names the role needed: {reply}");
}
assert!(rig.mock.sets.lock().unwrap().is_empty());
let (status, reply, _) = rig.call(Method::POST, "/api/entitlement/gunnar/team%2Ffree", Auth::Cookie(&rig.cookie(Role::Admin)), Some(body.clone())).await;
assert_eq!(status, StatusCode::OK, "{reply}");
assert_eq!((reply["pushed"].as_bool(), reply["fact"]["state"].as_str(), reply["fact"]["signed"].as_bool()), (Some(true), Some("paid"), Some(true)));
let sent = rig.mock.sets.lock().unwrap()[0].clone();
let fact = sent.fact.as_ref().unwrap();
assert_eq!((sent.product.as_str(), fact.tenant.as_str(), fact.source.as_str(), fact.plan.as_str()), ("gunnar", "team/free", "operator", "trial"), "the operator's label rides in the fact's `plan` field");
assert!(fact.signature.is_empty(), "empty signature = \"sign it for me\"");
let (status, reply, _) = rig.call(Method::POST, "/api/entitlement/gunnar/x", Auth::Token, Some(json!({ "state": "banana" }))).await;
assert_eq!(status, StatusCode::BAD_REQUEST, "{reply}");
}
/// **A state change must not silently uncap a paying tenant.**
///
/// The console's "Set entitlement…" form posts `{label, state,
/// paid_until_unix_ms}` and NO `caps` (`pages/tenant.rs`), because caps are
/// bought and not typed. `SetBody.caps` was a plain `BTreeMap` with
/// `#[serde(default)]`, so an absent field arrived as an EMPTY map, and
/// `Entitlement.Set` replaces the whole fact rather than merging into it. An
/// operator moving a paying tenant to `grace` for a week therefore deleted
/// their `pack_bytes` and `cache_bytes` caps — and on gunnar an absent cap means
/// UNLIMITED, so the punishment for not paying was an uncapped plan.
///
/// Red first — MEASURED 2026-09-07:
///
/// ```text
/// assertion `left == right` failed: a state change that names no caps must
/// carry the tenant's caps forward; wiping them means an absent cap, and gunnar
/// reads an absent cap as UNLIMITED
/// left: {}
/// right: {"cache_bytes": 21474836480, "pack_bytes": 10737418240}
/// ```
///
/// `{}` — the whole plan, gone, on a request that only meant to change a word.
/// The tenant is `capped/tenant` and not one of the rig's own: the mock's `get`
/// answers with the FIRST fact whose tenant matches, so pushing a second fact
/// for a name the fixture already uses would have tested the fixture's caps
/// instead of these.
///
/// The fix is on the BFF and not in monetize: the caps field is now an
/// `Option`, so the three cases stay apart — absent means KEEP what the tenant
/// has, `{}` means CLEAR, and a map means SET. The proto's `caps` is a plain map
/// and cannot express the difference, which is exactly why the distinction has
/// to be made where the JSON still can.
#[tokio::test]
async fn an_operator_state_change_carries_the_tenants_caps_forward() {
let rig = Rig::up().await;
let caps: std::collections::BTreeMap<String, u64> =
[("pack_bytes".to_string(), 10u64 << 30), ("cache_bytes".to_string(), 20u64 << 30)].into();
rig.mock.facts.lock().unwrap().push(pb::EntitlementFact {
tenant: "capped/tenant".into(),
plan: "gunnar/capped/tenant/2026-09-01".into(),
state: pb::State::Paid as i32,
paid_until_unix_ms: Some(1_900_000_000_000),
caps: caps.clone(),
source: "payment:invoice:gunnar/capped/tenant/2026-09-01".into(),
signature: vec![1],
issued_unix_ms: None,
issued_signature: vec![],
});
// ── absent caps: KEEP ─────────────────────────────────────────────────
let (status, reply, _) = rig
.call(
Method::POST,
"/api/entitlement/gunnar/capped%2Ftenant",
Auth::Cookie(&rig.cookie(Role::Admin)),
Some(json!({ "label": "grace week", "state": "grace" })),
)
.await;
assert_eq!(status, StatusCode::OK, "{reply}");
let sent = rig.mock.sets.lock().unwrap().last().unwrap().clone();
assert_eq!(
sent.fact.as_ref().unwrap().caps,
caps,
"a state change that names no caps must carry the tenant's caps forward; wiping them \
means an absent cap, and gunnar reads an absent cap as UNLIMITED"
);
assert_eq!(sent.fact.as_ref().unwrap().state, pb::State::Grace as i32, "the state still moved");
// ── an explicit map: SET ──────────────────────────────────────────────
//
// Without this the test would pass against a BFF that ignored `caps`
// altogether and always echoed the stored fact.
let bigger: std::collections::BTreeMap<String, u64> = [("pack_bytes".to_string(), 100u64 << 30)].into();
let (status, reply, _) = rig
.call(
Method::POST,
"/api/entitlement/gunnar/capped%2Ftenant",
Auth::Cookie(&rig.cookie(Role::Admin)),
Some(json!({ "label": "bigger", "state": "paid", "caps": { "pack_bytes": 100u64 << 30 } })),
)
.await;
assert_eq!(status, StatusCode::OK, "{reply}");
assert_eq!(
rig.mock.sets.lock().unwrap().last().unwrap().fact.as_ref().unwrap().caps,
bigger,
"an explicit caps map replaces, and does not merge into, what is stored"
);
// ── an explicit EMPTY map: CLEAR, deliberately ────────────────────────
//
// The escape hatch. It must stay reachable, or the only way to uncap a
// tenant would be to edit the ledger by hand — but it must be something the
// caller TYPED, never the shape of a form that did not mention caps at all.
let (status, reply, _) = rig
.call(
Method::POST,
"/api/entitlement/gunnar/capped%2Ftenant",
Auth::Cookie(&rig.cookie(Role::Admin)),
Some(json!({ "label": "uncapped", "state": "paid", "caps": {} })),
)
.await;
assert_eq!(status, StatusCode::OK, "{reply}");
assert!(
rig.mock.sets.lock().unwrap().last().unwrap().fact.as_ref().unwrap().caps.is_empty(),
"an explicitly empty caps map must still clear them"
);
// ── a tenant with no fact at all: nothing to carry, and no failure ────
let (status, reply, _) = rig
.call(
Method::POST,
"/api/entitlement/gunnar/nobody",
Auth::Cookie(&rig.cookie(Role::Admin)),
Some(json!({ "label": "trial", "state": "free" })),
)
.await;
assert_eq!(status, StatusCode::OK, "a first fact must not need a previous one: {reply}");
assert!(rig.mock.sets.lock().unwrap().last().unwrap().fact.as_ref().unwrap().caps.is_empty());
}
#[tokio::test]
async fn the_break_glass_token_works_until_an_admin_passkey_exists() {
let rig = Rig::up().await;
let (status, ..) = rig.call(Method::GET, "/api/products", Auth::Token, None).await;
assert_eq!(status, StatusCode::OK);
rig.webauthn.adopt(StoredCred { id: "k1".into(), x: vec![0; 32], y: vec![0; 32], sign_count: 0, label: "yubikey".into(), created: 0, user: "bob".into(), role: Role::Admin, last_used: None, disabled: false }).unwrap();
let (status, ..) = rig.call(Method::GET, "/api/products", Auth::Token, None).await;
assert_eq!(status, StatusCode::UNAUTHORIZED, "the token is spent history once an admin passkey exists");
let (_, body, _) = rig.call(Method::GET, "/auth/status", Auth::None, None).await;
assert_eq!(body["bootstrap"], false);
let (status, ..) = rig.call(Method::GET, "/api/products", Auth::Cookie(&rig.cookie(Role::Admin)), None).await;
assert_eq!(status, StatusCode::OK, "the passkey session is the credential now");
}
#[tokio::test]
async fn a_cookie_authenticated_mutation_from_elsewhere_is_blocked() {
let rig = Rig::up().await;
let cookie = rig.cookie(Role::Admin);
let req = Request::builder()
.method(Method::POST)
.uri("/api/entitlement/gunnar/t")
.header("cookie", format!("monetize_ui_session={cookie}"))
.header("origin", "https://evil.example")
.header("content-type", "application/json")
.body(Body::from(json!({ "state": "free" }).to_string()))
.unwrap();
let res = rig.router.clone().oneshot(req).await.unwrap();
assert_eq!(res.status(), StatusCode::FORBIDDEN);
assert!(rig.mock.sets.lock().unwrap().is_empty());
}
#[tokio::test]
async fn the_event_stream_opens_for_a_session_and_health_names_the_products() {
let rig = Rig::up().await;
let viewer = rig.cookie(Role::Viewer);
let (status, _, headers) = rig.call(Method::GET, "/api/events", Auth::Cookie(&viewer), None).await;
assert_eq!(status, StatusCode::OK);
assert!(headers.get("content-type").unwrap().to_str().unwrap().starts_with("text/event-stream"));
assert_eq!(headers.get("content-security-policy").unwrap().to_str().unwrap().split(';').next(), Some("default-src 'none'"));
let (status, body, _) = rig.call(Method::GET, "/api/health", Auth::Cookie(&viewer), None).await;
assert_eq!(status, StatusCode::OK);
assert_eq!((body["serving"].as_bool(), body["products"][0].as_str()), (Some(true), Some("gunnar")));
}
/// (r) **The "signed" badge VERIFIES; it does not trust a store invariant.**
///
/// The console computed `signed` as `!signature.is_empty()`, defended by a true
/// invariant — monetize-server refuses to store a fact whose non-empty signature
/// does not verify. The invariant holds and the badge was still worthless: it
/// reported that the FIELD IS POPULATED. Sixty-four bytes of anything turned it
/// green, and every path that could put wrong bytes there — a restore taken under
/// a different key, a hand-edited store, a bug in a future write path — is
/// exactly the one the badge exists to catch.
///
/// So this asserts the pair that only real verification can tell apart: two facts
/// in the same list, same state, same order, same 64-byte signature LENGTH, one
/// signed by monetize's key and one not. Under the old code both were `true`.
///
/// It also asserts the key really travels: the console must have fetched
/// `Entitlement.PublicKey`, because with no key nothing can verify and the
/// correctly-signed fact would be `false` too.
#[tokio::test]
async fn the_signed_badge_checks_the_signature_against_monetizes_fetched_key() {
let rig = Rig::up().await;
let viewer = rig.cookie(Role::Viewer);
let signed_of = |body: &Value| body["fact"]["signed"].as_bool();
let (status, good, _) = rig.call(Method::GET, "/api/tenants/gunnar/team%2Fpaid", Auth::Cookie(&viewer), None).await;
assert_eq!(status, StatusCode::OK, "{good}");
assert_eq!(signed_of(&good), Some(true), "a fact monetize really signed verifies: {good}");
assert_eq!(good["fact"]["signature"], Value::Null, "the raw signature is not shipped to the browser; the verdict is");
let (status, forged, _) = rig.call(Method::GET, "/api/tenants/gunnar/team%2Fforged", Auth::Cookie(&viewer), None).await;
assert_eq!(status, StatusCode::OK, "{forged}");
assert_eq!(signed_of(&forged), Some(false), "64 bytes of the wrong thing is NOT signed: {forged}");
// Same everything else — so the badge cannot be reading any other field.
assert_eq!(forged["fact"]["state"], good["fact"]["state"]);
assert_eq!(forged["fact"]["order"], good["fact"]["order"]);
assert_eq!(forged["row"]["signed"], Value::Bool(false), "the tenant row agrees with the fact panel");
// The same verdict on every surface that shows one.
let (_, list, _) = rig.call(Method::GET, "/api/products/gunnar/tenants", Auth::Cookie(&viewer), None).await;
let row = |t: &str| list["tenants"].as_array().unwrap().iter().find(|r| r["tenant"] == t).unwrap()["signed"].as_bool();
assert_eq!((row("team/paid"), row("team/forged")), (Some(true), Some(false)));
// An operator Set comes back verified, because the mock monetize signs it
// for real — the reply's badge is a check, not an echo.
let body = json!({ "state": "paid", "paid_until_unix_ms": 1_900_000_000_000u64 });
let (status, reply, _) = rig.call(Method::POST, "/api/entitlement/gunnar/team%2Ffree", Auth::Token, Some(body)).await;
assert_eq!(status, StatusCode::OK, "{reply}");
assert_eq!(reply["fact"]["signed"].as_bool(), Some(true), "{reply}");
}
/// (r) **A `Verifier` with no key verifies nothing.** The failure direction that
/// matters: if monetize will not name its key, the honest answer is "not
/// verified", never "signed". Asserted directly on the unit so a route that ever
/// degrades to a keyless verifier cannot quietly paint the console green.
#[test]
fn a_verifier_without_a_key_refuses_a_genuinely_signed_fact() {
use monetize_ui_server::api::Verifier;
let good = fact("team/paid", pb::State::Paid, "operator");
let key = signing_key().verifying_key().to_bytes();
assert!(Verifier::from_bytes(&key).verifies(&good), "the real key verifies the real signature");
assert!(!Verifier::none().verifies(&good), "no key, no verdict — and the verdict is not 'signed'");
// A key that is not 32 bytes, and 32 bytes that are not a point, both mean
// "no key" rather than "assume it is fine".
assert!(!Verifier::from_bytes(&key[..31]).verifies(&good));
assert!(!Verifier::from_bytes(&[0xffu8; 32]).verifies(&good));
// And the right key still refuses the wrong signature.
assert!(!Verifier::from_bytes(&key).verifies(&corrupted_fact("team/forged", pb::State::Paid, "operator")));
}
// ---------------------------------------------------------------------------
// The live feed, on the wire. `/api/events` is an SSE stream, so these read the
// actual `data:` frames off the response body rather than a JSON reply.
// ---------------------------------------------------------------------------
/// Open `/api/events` and return the `data:` payloads until `want` matches one,
/// or panic with everything seen. Reads REAL SSE bytes — a helper that asked the
/// hub for its buffer would prove nothing about what a browser receives.
async fn sse_until(router: &Router, cookie: &str, deadline: tokio::time::Instant, want: impl Fn(&Value) -> bool) -> (Value, Vec<Value>) {
let req = Request::builder()
.method(Method::GET)
.uri("/api/events")
.header("origin", "https://console.example")
.header("cookie", format!("monetize_ui_session={cookie}"))
.body(Body::empty())
.unwrap();
let res = router.clone().oneshot(req).await.unwrap();
assert_eq!(res.status(), StatusCode::OK, "the feed must open for a session");
assert!(res.headers().get("content-type").is_some_and(|v| v.to_str().unwrap_or("").starts_with("text/event-stream")));
let mut body = res.into_body();
let mut seen = Vec::new();
let mut buf = String::new();
loop {
let frame = tokio::time::timeout_at(deadline, body.frame())
.await
.unwrap_or_else(|_| panic!("the feed went quiet; frames so far: {seen:?}"));
let Some(Ok(frame)) = frame else { panic!("the feed ended; frames so far: {seen:?}") };
let Ok(data) = frame.into_data() else { continue };
buf.push_str(&String::from_utf8_lossy(&data));
// SSE frames are `event: feed\ndata: {…}\n\n`; only complete records
// are parsed, so a split chunk is not mistaken for a bad line.
while let Some(end) = buf.find("\n\n") {
let record: String = buf.drain(..end + 2).collect();
for line in record.lines() {
let Some(rest) = line.strip_prefix("data: ") else { continue };
let Ok(v) = serde_json::from_str::<Value>(rest) else { continue };
if want(&v) {
return (v, seen);
}
seen.push(v);
}
}
}
}
/// (r) **An event monetize publishes reaches the browser, with its fields.**
///
/// The mock pushes one real `LedgerAppended` into its `Events.Watch` stream; the
/// BFF's driver must be holding that stream, translate the event, and put it on
/// every open SSE connection. Asserted on the reference, the step, the tenant
/// and the open flag — not on "a frame arrived": the keep-alive comment and the
/// opening mode line are both frames, and neither is the event.
///
/// Nothing in this test is reachable by polling. The mock's ledger and
/// entitlement tables never change, so the fallback poller's diff is empty
/// forever; the only way this line exists is the stream.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn an_event_monetize_publishes_arrives_on_the_console_feed() {
let rig = Rig::up().await;
let viewer = rig.cookie(Role::Viewer);
let deadline = tokio::time::Instant::now() + std::time::Duration::from_secs(20);
// Wait for the BFF to actually be subscribed: publishing into a broadcast
// nobody holds is publishing into nothing, and the test would then be
// asserting on a race rather than on the feed.
while rig.mock.watchers.load(std::sync::atomic::Ordering::SeqCst) == 0 {
assert!(tokio::time::Instant::now() < deadline, "the BFF never opened Events.Watch");
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
}
// The publisher runs alongside the reader and repeats, because the SSE
// connection is opened inside `sse_until`: one shot could land before the
// browser subscribed. Repetition changes nothing about what is asserted —
// the FIRST matching line is checked, field by field.
let mock = rig.mock.clone();
let pump = tokio::spawn(async move {
for _ in 0..200 {
let _ = mock.events.send(pb::Event::new(
7,
4_242,
pb::event::Payload::LedgerAppended(pb::LedgerAppended {
reference: "gunnar/team/paid/2026-09-04".into(),
product: "gunnar".into(),
tenant: "team/paid".into(),
seq: 3,
at_unix_ms: 4_200,
step: "Captured".into(),
open: true,
}),
));
tokio::time::sleep(std::time::Duration::from_millis(25)).await;
}
});
let (row, seen) = sse_until(&rig.router, &viewer, deadline, |v| v["kind"] == "ledger").await;
assert_eq!(
(row["reference"].as_str(), row["step"].as_str(), row["tenant"].as_str(), row["seq"].as_u64(), row["open"].as_bool(), row["at_unix_ms"].as_u64()),
(Some("gunnar/team/paid/2026-09-04"), Some("Captured"), Some("team/paid"), Some(3), Some(true), Some(4_200)),
"the row's own fields, off monetize's stream; frames seen before it: {seen:?}"
);
// And the console was told it is LIVE before that delta reached it. The
// very first frame may still be the opening `polling` line — the SSE
// connection can be made in the instant between the driver subscribing and
// its first frame landing — so the claim is about order, not about index 0.
assert!(
seen.iter().any(|v| v["kind"] == "feed" && v["mode"] == "stream"),
"the console must be told the feed is live before it is handed streamed events: {seen:?}"
);
pump.abort();
}
/// (r) **A monetize that cannot stream makes the console SAY so.**
///
/// The whole reason the poller is still in the tree. This mock refuses
/// `Events.Watch`; the BFF must fall back, and the first frame a browser gets
/// must be a `feed` line reading `polling` WITH monetize's own refusal in it.
///
/// The message is the load-bearing half of the assertion. A hub that had never
/// tried also reports `polling`, so a test that only checked the mode would pass
/// against a BFF that never opened a stream at all — and that is precisely the
/// bug this is here to catch.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn a_monetize_without_events_makes_the_console_announce_that_it_is_polling() {
let rig = Rig::up_with(Mock::seeded_refusing_events()).await;
let viewer = rig.cookie(Role::Viewer);
let deadline = tokio::time::Instant::now() + std::time::Duration::from_secs(20);
// Re-open the feed until the refusal has been recorded: the first attempt
// may reach the hub before its driver has heard from monetize at all.
loop {
assert!(tokio::time::Instant::now() < deadline, "the console never announced the fallback");
let (line, _) = sse_until(&rig.router, &viewer, deadline, |v| v["kind"] == "feed").await;
if line["message"].as_str().unwrap_or("").contains("Events.Watch refused") {
assert_eq!(line["mode"], "polling", "{line}");
assert!(line["message"].as_str().unwrap().contains("no Events service"), "monetize's own words: {line}");
break;
}
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
}
// And it is still a working console: the polled surface answers.
let (status, out, _) = rig.call(Method::GET, "/api/products/gunnar/tenants", Auth::Cookie(&viewer), None).await;
assert_eq!(status, StatusCode::OK, "the polled surface still answers: {out}");
assert!(out["tenants"].as_array().is_some_and(|t| !t.is_empty()), "and it has rows: {out}");
}
/// (r) **The console gets "not enough history" as its own answer, never a zero.**
///
/// The whole risk of this route is a JSON body in which an unmeasurable tenant
/// and a tenant that is not growing look identical. `per_week` is ABSENT in the
/// unknown arm and `0.0` in the flat arm, and a template that forgot to check
/// `known` renders nothing rather than a flat line.
///
/// It also asserts WHICH meter the BFF asked for, on the recorded request: a
/// rate for the wrong meter is a wrong number that looks right.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn a_rate_reaches_the_console_with_its_uncertainty_and_its_meter() {
let rig = Rig::up().await;
let cookie = rig.cookie(Role::Viewer);
// The default mock answer is the NOT-ENOUGH-HISTORY arm.
let (status, out, _) = rig.call(Method::GET, "/api/tenants/gunnar/team%2Fpaid", Auth::Cookie(&cookie), None).await;
assert_eq!(status, StatusCode::OK, "{out}");
assert_eq!(out["trend"]["known"], false);
assert!(out["trend"]["per_week"].is_null(), "unknown carries no number at all: {}", out["trend"]);
assert_eq!(out["trend"]["meter"], "pack_bytes", "the wall is the meter a screen leads with");
assert_eq!(out["trend"]["needed_span_ms"], 302_400_000u64, "how long it still has to wait");
// The BFF asked monetize for THIS tenant and THIS meter, not for the fleet.
let asked = rig.mock.trends_asked.lock().unwrap().clone();
assert_eq!(asked.len(), 1);
assert_eq!((asked[0].product.as_str(), asked[0].tenant.as_str(), asked[0].meter.as_str()), ("gunnar", "team/paid", "pack_bytes"));
// A FLAT tenant: known, and zero. The arm the unknown one must not look like.
*rig.mock.trend.lock().unwrap() = Some(pb::TrendResponse {
enough_history: true,
per_week: 0.0,
confidence: "firm".into(),
readings: 2880,
span_ms: 2_592_000_000,
needed_span_ms: 302_400_000,
window_ms: 2_592_000_000,
..Default::default()
});
let (_, out, _) = rig.call(Method::GET, "/api/tenants/gunnar/team%2Fpaid", Auth::Cookie(&cookie), None).await;
assert_eq!(out["trend"]["known"], true);
assert_eq!(out["trend"]["per_week"], 0.0, "measured and flat is a NUMBER");
assert_eq!(out["trend"]["marker"], "", "a firm figure carries no tilde");
assert_eq!(out["trend"]["may_project"], true);
// A BURSTY one: the figure stands, the projection does not.
*rig.mock.trend.lock().unwrap() = Some(pb::TrendResponse {
enough_history: true,
per_week: 12.0 * (1u64 << 30) as f64,
confidence: "bursty".into(),
largest_step_share: 1.0,
readings: 30,
span_ms: 2_592_000_000,
needed_span_ms: 302_400_000,
window_ms: 2_592_000_000,
..Default::default()
});
let (_, out, _) = rig.call(Method::GET, "/api/tenants/gunnar/team%2Fpaid", Auth::Cookie(&cookie), None).await;
assert_eq!(out["trend"]["known"], true);
assert_eq!(out["trend"]["per_week"], 12.0 * (1u64 << 30) as f64, "the rate is true even when it may not be projected");
assert_eq!(out["trend"]["marker"], "~", "UI.md rule 4: a projection carries its uncertainty");
assert_eq!(out["trend"]["may_project"], false, "one push must not become a date");
// THE FLEET, on the two product-shaped routes: an empty tenant, same meter.
rig.mock.trends_asked.lock().unwrap().clear();
let (_, out, _) = rig.call(Method::GET, "/api/products/gunnar/tenants", Auth::Cookie(&cookie), None).await;
assert_eq!(out["trend"]["meter"], "pack_bytes");
let (_, cards, _) = rig.call(Method::GET, "/api/products", Auth::Cookie(&cookie), None).await;
assert_eq!(cards["products"][0]["trend"]["meter"], "pack_bytes", "the Products page's per-row rate: {cards}");
let asked = rig.mock.trends_asked.lock().unwrap().clone();
assert_eq!(asked.len(), 2);
assert!(asked.iter().all(|a| a.tenant.is_empty() && a.meter == "pack_bytes"), "a fleet rate names no tenant: {asked:?}");
}
/// (r) **The oversell number reaches the console, and an unmeasured box does not
/// arrive as a zero.**
///
/// Also the scope axis: the console is told which meters are per tenant and
/// which belong to the product, so it cannot draw a per-tenant bar for cores
/// nobody measured.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn the_fleet_total_and_its_oversell_reach_the_console_with_the_scope_axis() {
let rig = Rig::up().await;
let cookie = rig.cookie(Role::Viewer);
// Default: a gunnar that predates `Admin.Capacity` — the live appliance.
let (status, out, _) = rig.call(Method::GET, "/api/products/gunnar/tenants", Auth::Cookie(&cookie), None).await;
assert_eq!(status, StatusCode::OK, "{out}");
assert_eq!(out["fleet"]["servable_measured"], false);
assert!(out["fleet"]["servable_reason"].as_str().unwrap().contains("Admin.Capacity"), "{}", out["fleet"]);
let pack = &out["fleet"]["meters"][0];
assert_eq!(pack["meter"], "pack_bytes");
assert_eq!(pack["oversell"], "unmeasured", "silence must not arrive as a verdict");
assert!(pack["capacity"].is_null(), "an unmeasured box has no denominator: {pack}");
assert_eq!(pack["is_a_floor"], true);
// The halves monetize CAN fold are still there.
assert_eq!(pack["used"], 4u64 << 30);
assert_eq!(pack["allowance"], 20u64 << 30);
// THE SCOPE AXIS, by name.
let scopes: Vec<(String, String)> = out["meters"]
.as_array()
.unwrap()
.iter()
.map(|m| (m["name"].as_str().unwrap().to_string(), m["scope"].as_str().unwrap().to_string()))
.collect();
assert_eq!(
scopes,
vec![
("pack_bytes".to_string(), "tenant".to_string()),
("lfs_bytes".to_string(), "tenant".to_string()),
("cpu_millicores".to_string(), "product".to_string()),
],
"the console must be told which meters may be summed across tenants"
);
// An OVERSOLD fleet: the number is the point.
*rig.mock.fleet.lock().unwrap() = Some(pb::FleetResponse {
meters: vec![pb::FleetMeter {
meter: "pack_bytes".into(),
tenants: 10,
used: 10 << 30,
tenants_without_reading: 0,
allowance: 100 << 30,
tenants_without_cap: 0,
oversell: "oversold".into(),
oversell_bytes: 35 << 30,
oversell_reason: String::new(),
capacity: Some(65 << 30),
is_a_floor: false,
}],
servable_measured: true,
servable_bytes: 55 << 30,
total_bytes: 75 << 30,
servable_reason: String::new(),
servable_at_unix_ms: 1_700_000_000_000,
growth: "sealed".into(),
growth_detail: "a disk bought and attached to this box would be BILLED AND INVISIBLE. Serving more needs a fresh install onto a larger set, not another disk".into(),
cloud: None,
});
let (_, cards, _) = rig.call(Method::GET, "/api/products", Auth::Cookie(&cookie), None).await;
let pack = &cards["products"][0]["fleet"]["meters"][0];
assert_eq!(pack["oversell"], "oversold");
assert_eq!(pack["oversell_bytes"], 35u64 << 30, "ten tenants at 10 GiB on a 65 GiB fleet: {pack}");
assert_eq!(pack["capacity"], 65u64 << 30);
assert_eq!(pack["is_a_floor"], false);
assert_eq!(cards["products"][0]["fleet"]["servable_at_unix_ms"], 1_700_000_000_000u64, "how old the denominator is");
// THE GROWTH FACT, as one field the badge keys on and holds. RED before
// `FleetJson.growth` existed: `⚠ cannot grow` had nothing to read.
let growth = &cards["products"][0]["fleet"]["growth"];
assert_eq!(growth["state"], "sealed");
assert!(growth["detail"].as_str().unwrap().contains("fresh install"), "the product's remedy, verbatim: {growth}");
// And the unmeasured default above carried NO growth claim.
assert_eq!(out["fleet"]["growth"]["state"], "", "a box nobody could measure has not said whether it can grow");
// Both product-shaped routes asked monetize for THIS product.
let asked = rig.mock.fleets_asked.lock().unwrap().clone();
assert_eq!(asked, vec!["gunnar", "gunnar"]);
}
fn a_sale(tenant: &str, outcome: &str, minor: u64, drawn: bool, needs_you: bool) -> pb::Sale {
pb::Sale {
reference: format!("gunnar/{tenant}/2026-09-04"),
product: "gunnar".into(),
tenant: tenant.into(),
price: Some(pb::Money { minor, currency: pb::Currency::Sek as i32 }),
// The provider's cost beside the price, in ITS currency: 2.30 EUR for
// the 10 GiB. Different numbers and a different unit, so a row that
// confused the two is visibly wrong.
cost: Some(pb::Money { minor: 230, currency: pb::Currency::Eur as i32 }),
// What Stripe kept, in the SAME currency the customer paid — beside a
// cost in another one, so a row that confused the three is visible.
fee: Some(pb::Money { minor: 574, currency: pb::Currency::Sek as i32 }),
// The VAT on the net price at the recorded 25 %, as the fold says it.
tax: Some(pb::Money { minor: minor / 4, currency: pb::Currency::Sek as i32 }),
tax_from_vendor: false,
vat_basis_points: 2500,
caps: [("pack_bytes".to_string(), 10u64 << 30)].into(),
delta: [("pack_bytes".to_string(), 10u64 << 30)].into(),
started_unix_ms: 1_757_000_000_000,
at_unix_ms: 1_757_000_060_000,
steps: 6,
drawn,
outcome: outcome.into(),
reason: if outcome == "granted" || outcome == "open" { String::new() } else { "the cloud refused".into() },
needs_you,
livemode: None,
customer_country: String::new(),
}
}
/// (r) **"Recently bought" reaches the console as OUTCOMES, one line per
/// attempt** — `UI.md` rule 6 — with the amount, and with the three endings kept
/// apart.
///
/// The step-by-step rows are still there for the disclosure; what changed is
/// what the page leads with.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn recently_bought_arrives_as_one_line_per_attempt_with_its_outcome() {
let rig = Rig::up().await;
let cookie = rig.cookie(Role::Viewer);
*rig.mock.sales.lock().unwrap() = vec![
a_sale("bob", "granted", 22000, true, false),
a_sale("zz-visibility", "refunded", 100, false, false),
a_sale("stuck", "open", 22000, true, true),
];
let (status, out, _) = rig.call(Method::GET, "/api/ledger", Auth::Cookie(&cookie), None).await;
assert_eq!(status, StatusCode::OK, "{out}");
let sales = out["sales"].as_array().expect("the ledger page leads with outcomes");
assert_eq!(sales.len(), 3, "one line per attempt, not one per step");
// `Sold bob 10 GiB · 220.00 SEK ✓`
assert_eq!(sales[0]["tenant"], "bob");
assert_eq!(sales[0]["outcome"], "granted");
assert_eq!(sales[0]["price_minor"], 22000, "the amount is on the row, in minor units");
assert_eq!(sales[0]["currency"], "SEK");
assert_eq!(sales[0]["caps"]["pack_bytes"], 10u64 << 30, "so the row can say '10 GiB for bob'");
assert_eq!(sales[0]["delta"]["pack_bytes"], 10u64 << 30, "and what of it was SOLD");
assert_eq!((sales[0]["cost_minor"].as_u64(), sales[0]["cost_currency"].as_str()), (Some(230), Some("EUR")), "the cost beside the price, so a margin is a fold");
assert!(sales[0]["plan"].is_null(), "no catalogue vocabulary on the row");
assert_eq!(sales[0]["steps"], 6, "and the disclosure knows how many steps are behind it");
assert_eq!(sales[0]["needs_you"], false);
// The VAT per order (owner 2026-09-28), with the rate it was charged at.
assert_eq!((sales[0]["tax_minor"].as_u64(), sales[0]["tax_currency"].as_str(), sales[0]["vat_basis_points"].as_u64(), sales[0]["tax_from_vendor"].as_bool()), (Some(5500), Some("SEK"), Some(2500), Some(false)), "{}", sales[0]);
// A refund is NOT a sale, and says so.
assert_eq!(sales[1]["outcome"], "refunded");
assert_eq!(sales[1]["drawn"], false, "a released reservation never drew a krona");
assert_eq!(sales[1]["reason"], "the cloud refused");
assert_ne!(sales[1]["outcome"], sales[0]["outcome"]);
// An open attempt is a TASK and is distinguishable from both.
assert_eq!(sales[2]["outcome"], "open");
assert_eq!(sales[2]["needs_you"], true, "paid, never delivered is the only row that is a task");
assert_eq!(sales[2]["drawn"], true, "and the money DID move");
// The steps are still reachable — the outcomes replace the DEFAULT
// rendering, not the data.
assert!(out["recent"].is_array(), "the disclosure still has its rows: {out}");
assert!(out["unfinished"].is_array());
// `RECENTLY BOUGHT` on the product screen asks for that product only.
let (_, product, _) = rig.call(Method::GET, "/api/products/gunnar/tenants", Auth::Cookie(&cookie), None).await;
assert_eq!(product["sales"].as_array().unwrap().len(), 3);
let asked = rig.mock.sales_asked.lock().unwrap().clone();
assert_eq!(asked.iter().map(|a| a.product.as_str()).collect::<Vec<_>>(), ["", "gunnar"]);
assert!(asked.iter().all(|a| a.limit > 0), "a page asks for a page, not the whole ledger: {asked:?}");
}
/// (r) **The reachability dot has three states and the page never waits for it.**
///
/// `unknown` is the one that matters: a monetize that has not finished a poll
/// tick — or one too old to report at all — must not arrive as a green dot the
/// console invented. And `down` carries the product's own words, because
/// `UI.md`'s `○ holger — not connected` is useless without them.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn the_reachability_dot_is_unknown_until_something_has_actually_asked() {
let rig = Rig::up().await;
let cookie = rig.cookie(Role::Viewer);
// A monetize that reports nothing: UNKNOWN, and it says why.
let (status, out, _) = rig.call(Method::GET, "/api/products", Auth::Cookie(&cookie), None).await;
assert_eq!(status, StatusCode::OK, "{out}");
let dot = &out["products"][0]["reachable"];
assert_eq!(dot["state"], "unknown", "an unreported product must not be painted green: {dot}");
assert_eq!(dot["checked_at_unix_ms"], 0, "nothing has asked");
// DOWN, with the plugin's own reason.
*rig.mock.reachable.lock().unwrap() = Some(pb::Reachability {
state: "down".into(),
checked_at_unix_ms: 1_757_000_000_000,
last_up_unix_ms: 0,
detail: "product unavailable: holger at 127.0.0.1:50051: tcp connect error".into(),
});
let (_, out, _) = rig.call(Method::GET, "/api/products", Auth::Cookie(&cookie), None).await;
let dot = &out["products"][0]["reachable"];
assert_eq!(dot["state"], "down");
assert_eq!(dot["detail"], "product unavailable: holger at 127.0.0.1:50051: tcp connect error", "`not connected` needs its reason");
assert_eq!(dot["last_up_unix_ms"], 0, "it has never answered");
assert_eq!(dot["checked_at_unix_ms"], 1_757_000_000_000u64, "a cached answer is only honest if its age is on the row");
// UP — a third, distinguishable state.
*rig.mock.reachable.lock().unwrap() = Some(pb::Reachability {
state: "up".into(),
checked_at_unix_ms: 1_757_000_060_000,
last_up_unix_ms: 1_757_000_060_000,
detail: String::new(),
});
let (_, out, _) = rig.call(Method::GET, "/api/products", Auth::Cookie(&cookie), None).await;
let up = &out["products"][0]["reachable"];
assert_eq!(up["state"], "up");
assert_eq!(up["detail"], "");
assert_ne!(up["state"], dot["state"]);
}
/// **`UI.md` screen 1's last lines.** The spend against each cloud's ceiling
/// rides at the TOP of `/api/products` — the ceiling is per cloud, not per
/// product — with the shape the console's `spend_line` reads.
///
/// RED before (2026-09-14): `spend` was one object, the server's FIRST line,
/// so with two clouds linked the sandbox's ceiling stood in for the cloud that
/// spends real money, and `default_cloud` was not on the wire.
#[tokio::test]
async fn the_spend_against_the_ceiling_rides_on_the_products_response() {
let rig = Rig::up().await;
let viewer = rig.cookie(Role::Viewer);
let (status, body, _) = rig.call(Method::GET, "/api/products", Auth::Cookie(&viewer), None).await;
assert_eq!(status, StatusCode::OK, "{body}");
let lines = body["spend"].as_array().unwrap_or_else(|| panic!("`spend` is one entry per cloud, a list: {body}"));
assert_eq!(lines.len(), 2, "every cloud's line, not the first: {body}");
assert_eq!(body["default_cloud"], "upcloud", "the cloud a sale provisions on rides beside the lines");
let upcloud = &lines[1];
assert_eq!((upcloud["provider"].as_str(), upcloud["ceiling_total_minor"].as_u64(), upcloud["ceiling_monthly_minor"].as_u64()), (Some("upcloud"), Some(2500), Some(2500)), "{upcloud}");
let spend = &lines[0];
assert_eq!(spend["provider"], "sandbox");
assert_eq!(spend["currency"], "EUR");
assert_eq!(spend["spent_minor"], 226);
assert_eq!(spend["spent_this_month_minor"], 226);
assert_eq!(spend["ceiling_total_minor"], 1000);
assert!(spend["ceiling_monthly_minor"].is_null(), "a limit that is not set is null, never zero (zero means refuse all): {spend}");
assert_eq!(body["clouds"][0], "sandbox");
}
// ── The estate, drawn ─────────────────────────────────────────────────
//
// `GET /api/estates/{estate}/map` (was `/api/estate/{product}` until
// 2026-10-02) is the door onto `monetize_server::estate_svg`. The estate is
// one the ONE monetize's fleet has heard of; its product is drawn by the ONE
// — the console never dials the estate itself. The door
// was finished, tested and unreachable. The drawing is held to account in
// monetize-server; what these tests own is the half that lives here: the bytes
// arrive as an image, the human gate is in front of them, and the caching
// decision is the one that cannot show a stale estate.
/// The raw answer, headers and body bytes, with arbitrary request headers —
/// `Rig::call` parses JSON and takes no extras, and a picture is neither.
async fn raw(rig: &Rig, path: &str, auth: Auth<'_>, extra: &[(&str, &str)]) -> (StatusCode, http::HeaderMap, Vec<u8>) {
let mut req = Request::builder().method(Method::GET).uri(path).header("origin", "https://console.example");
match auth {
Auth::None => {}
Auth::Token => req = req.header("authorization", format!("Bearer {ADMIN_TOKEN}")),
Auth::Cookie(c) => req = req.header("cookie", format!("monetize_ui_session={c}")),
}
for (k, v) in extra {
req = req.header(*k, *v);
}
let res = rig.router.clone().oneshot(req.body(Body::empty()).unwrap()).await.unwrap();
let status = res.status();
let headers = res.headers().clone();
(status, headers, res.into_body().collect().await.unwrap().to_bytes().to_vec())
}
fn header(h: &http::HeaderMap, name: &str) -> String {
h.get(name).and_then(|v| v.to_str().ok()).unwrap_or_default().to_owned()
}
/// **The estate is served to an operator as a picture**, not as JSON about a
/// picture: one `image/svg+xml` document a browser renders and an `<img>` can
/// point at.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn the_estate_is_served_to_an_operator_as_an_svg_document() {
let rig = Rig::up().await;
let viewer = rig.cookie(Role::Viewer);
let (status, headers, body) = raw(&rig, "/api/estates/gunnar.rs/map", Auth::Cookie(&viewer), &[]).await;
assert_eq!(status, StatusCode::OK);
assert_eq!(header(&headers, "content-type"), "image/svg+xml", "a browser must render it, not download it");
let body = String::from_utf8(body).expect("an SVG document is text");
assert!(body.starts_with("<svg"), "the document itself, verbatim: {}", &body[..body.len().min(40)]);
assert!(body.contains("gunnar estate"));
// The picture is monetize's, re-published: the console does not draw.
assert_eq!(*rig.mock.estates_asked.lock().unwrap(), vec!["gunnar".to_string()], "one Draw, for the product in the path");
}
/// **A caller with no session gets no estate, and monetize is never asked.**
///
/// The bytes name provider ids, plan names and per-machine disk. The gate is
/// `require_auth` over everything under `/api/` — the same one every other
/// operator read on this console sits behind — and the refusal happens here,
/// before the bearer this process holds is ever spent.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn the_estate_is_refused_to_a_caller_with_no_session_before_monetize_is_asked() {
let rig = Rig::up().await;
let (status, _, _) = raw(&rig, "/api/estates/gunnar.rs/map", Auth::None, &[]).await;
assert_eq!(status, StatusCode::UNAUTHORIZED, "an operator's picture needs an operator");
assert!(rig.mock.estates_asked.lock().unwrap().is_empty(), "a refusal must not reach monetize");
// And it is not the shape that is handed out without a cookie: `is_public_asset`
// serves a single-segment `*.svg` (trunk fetches the console's own bundle
// that way), which is exactly the trap this path's three segments avoid.
let (status, _, _) = raw(&rig, "/estate.svg", Auth::None, &[]).await;
assert_eq!(status, StatusCode::NOT_FOUND, "no such route — and if there were, it would be public");
}
/// **Looking is a viewer's right; the picture changes nothing.** The same floor
/// every other read on this console has (`role_floor`: GET ⇒ viewer).
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn a_viewer_may_look_at_the_estate_because_looking_spends_nothing() {
let rig = Rig::up().await;
for role in [Role::Viewer, Role::Operator, Role::Admin] {
let cookie = rig.cookie(role);
let (status, _, _) = raw(&rig, "/api/estates/gunnar.rs/map", Auth::Cookie(&cookie), &[]).await;
assert_eq!(status, StatusCode::OK, "{role} may look");
}
}
/// **A deployment that declares no machines is answered with a BOARD.**
///
/// This is the case the live box is in today — `[machines]` is emitted by a
/// tool (`cargo xtask estate emit`) and nothing has run it — so an empty board
/// is the first thing this route will actually serve. An empty estate is not an
/// error: nobody promised machines, and a 500 here would teach an operator that
/// the picture is broken when it is merely honest.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn a_product_that_declares_no_machines_is_answered_with_a_board_and_not_an_error() {
let rig = Rig::up().await;
let viewer = rig.cookie(Role::Viewer);
let (status, headers, body) = raw(&rig, "/api/estates/holger.rs/map", Auth::Cookie(&viewer), &[]).await;
assert_eq!(status, StatusCode::OK, "an undeclared estate still draws");
assert_eq!(header(&headers, "content-type"), "image/svg+xml");
let body = String::from_utf8(body).unwrap();
assert!(body.starts_with("<svg"), "a board");
assert!(body.contains("holger estate"), "titled, so the reader knows what is empty");
}
/// **A product monetize does not serve is a 404 here too**, in monetize's own
/// words — never a blank board, which is what an undeclared estate looks like.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn a_product_this_monetize_does_not_serve_is_a_404_with_monetizes_own_words() {
let rig = Rig::up().await;
let viewer = rig.cookie(Role::Viewer);
let (status, _, body) = raw(&rig, "/api/estates/idun.rs/map", Auth::Cookie(&viewer), &[]).await;
assert_eq!(status, StatusCode::NOT_FOUND);
let body: Value = serde_json::from_slice(&body).expect("a refusal is JSON, like every other refusal here");
assert_eq!(body["code"], "notfound");
assert!(body["message"].as_str().unwrap().contains("idun"), "{body}");
}
/// **Always revalidate, rarely transfer.**
///
/// The renderer is deterministic, so a hash of the bytes is an honest `ETag`.
/// The freshness lifetime is NOT honest at any value above zero: half the
/// picture is live (a box's disk, whether the twin answered), and this picture
/// is opened precisely when something has moved. So `no-cache, private` — store
/// it, ask every time — and the `ETag` pays on the transfer.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn an_unchanged_estate_revalidates_to_304_and_is_never_served_from_a_cache_unasked() {
let rig = Rig::up().await;
let viewer = rig.cookie(Role::Viewer);
let (status, headers, body) = raw(&rig, "/api/estates/gunnar.rs/map", Auth::Cookie(&viewer), &[]).await;
assert_eq!(status, StatusCode::OK);
let etag = header(&headers, "etag");
assert!(etag.starts_with('"') && etag.ends_with('"'), "a strong validator: {etag:?}");
let cache = header(&headers, "cache-control");
assert!(cache.contains("no-cache"), "a live half must never be shown stale: {cache:?}");
assert!(cache.contains("private"), "an operator's picture is nobody else's to keep: {cache:?}");
let (status, headers, empty) = raw(&rig, "/api/estates/gunnar.rs/map", Auth::Cookie(&viewer), &[("if-none-match", &etag)]).await;
assert_eq!(status, StatusCode::NOT_MODIFIED, "the same estate draws the same bytes");
assert_eq!(header(&headers, "etag"), etag, "and says so with the same validator");
assert!(empty.is_empty(), "a 304 carries no body");
// The saving is the transfer, not the work: the board has to be drawn
// before it can be hashed, and this test states that plainly rather than
// letting anyone believe the call was skipped.
assert_eq!(rig.mock.estates_asked.lock().unwrap().len(), 2, "a 304 still asked monetize; what it saved is the kilobytes");
assert!(!body.is_empty());
}
/// **A moved box is a new validator and the new picture.** The red twin of the
/// 304 above: the estate changes, the caller's old tag no longer matches, and
/// it gets the drawing that is true now.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn an_estate_that_moved_answers_the_new_picture_rather_than_the_held_one() {
let rig = Rig::up().await;
let viewer = rig.cookie(Role::Viewer);
let (_, headers, _) = raw(&rig, "/api/estates/gunnar.rs/map", Auth::Cookie(&viewer), &[]).await;
let stale = header(&headers, "etag");
// The twin went dark since the first look.
rig.mock
.estates
.lock()
.unwrap()
.insert("gunnar".into(), "<svg xmlns=\"http://www.w3.org/2000/svg\"><text>gunnar estate</text><text>silent</text></svg>\n".into());
let (status, headers, body) = raw(&rig, "/api/estates/gunnar.rs/map", Auth::Cookie(&viewer), &[("if-none-match", &stale)]).await;
assert_eq!(status, StatusCode::OK, "a held picture that is no longer true must not be revalidated into place");
assert_ne!(header(&headers, "etag"), stale, "a changed estate is a changed validator");
assert!(String::from_utf8(body).unwrap().contains("silent"), "and the new drawing comes with it");
}
/// ★ **THE SHOP WINDOW CAN BE READ WITHOUT BUYING ANYTHING** — the door that
/// did not exist on 2026-09-20, and whose absence is why a live appliance
/// advertised 100 GiB and 50 GiB at full price on a box that could serve 37.8.
///
/// It is a GET on the viewer's floor, it reserves nothing, and every field is
/// monetize's own: the verdict is not recomputed here and the sentence is not
/// summarised. The two counts say the one thing that matters about a shop —
/// how much of it is real.
#[tokio::test]
async fn the_shop_window_is_readable_and_carries_monetizes_verdict_per_row() {
let rig = Rig::up().await;
let viewer = rig.cookie(Role::Viewer);
let (status, body, _) = rig.call(Method::GET, "/api/bundles?product=gunnar&tenant=team%2Fpaid", Auth::Cookie(&viewer), None).await;
assert_eq!(status, StatusCode::OK, "{body}");
// The whole declared shop, in the operator's order, untouched.
let rows = body["bundles"].as_array().expect("a list");
let ids: Vec<&str> = rows.iter().map(|b| b["id"].as_str().unwrap()).collect();
assert_eq!(ids, ["100gb", "10gb", "50gb"], "an OPERATOR sees the whole shop, refused rows included: {body}");
// And which of it can be delivered — monetize's answer, per row.
assert_eq!((body["declared"].as_u64(), body["deliverable"].as_u64()), (Some(3), Some(1)), "{body}");
let by = |id: &str| rows.iter().find(|b| b["id"] == id).unwrap().clone();
assert_eq!(by("10gb")["can_serve"], true);
assert_eq!(by("10gb")["reason"], "", "a bundle that can be served carries no refusal");
assert_eq!(by("100gb")["can_serve"], false);
assert_eq!(by("50gb")["can_serve"], false);
assert!(by("100gb")["reason"].as_str().unwrap().contains("maintenance"), "the refusal is carried VERBATIM, not summarised: {}", by("100gb"));
// Words, never minor units — the same string the customer's button carries.
assert_eq!(by("100gb")["price_words"], "1000.00 SEK");
assert_eq!(by("10gb")["price_words"], "100.00 SEK");
assert_eq!(by("10gb")["delta"]["pack_bytes"], 10u64 << 30);
// **And it spent nothing.** No order was placed and no quote reserved: the
// mock records every `Start` it is asked for, and it was asked for none.
assert_eq!(rig.mock.starts.lock().unwrap().len(), 0, "reading the shop window placed an order");
assert_eq!(rig.mock.quotes.lock().unwrap().len(), 0, "reading the shop window went through the ORDER path");
// No session, no window: it reads a tenant's standing caps back out of a
// price list, so it is behind the same door as everything else.
let (status, _, _) = rig.call(Method::GET, "/api/bundles?product=gunnar&tenant=team%2Fpaid", Auth::None, None).await;
assert_eq!(status, StatusCode::UNAUTHORIZED);
}
/// ★ **A row of the shop window can be quoted BY ID**, through the purchase
/// path the Buy button takes.
///
/// The window advertises; the quote is what the order will actually run. Asking
/// both about the same bundle is how a shop is caught offering something it
/// would refuse — and until 2026-09-20 the operator console could only ask
/// about caps an operator typed, so nothing could ask the two questions and
/// compare them.
///
/// The id is carried UNPARSED and `caps` goes empty beside it: monetize
/// resolves the bundle into exactly the caps a typed order would have named, so
/// there is no second arithmetic anywhere on this path.
#[tokio::test]
async fn a_bundle_can_be_quoted_by_id_and_the_id_is_carried_unparsed() {
let rig = Rig::up().await;
let viewer = rig.cookie(Role::Viewer);
let (status, body, _) = rig
.call(Method::GET, "/api/quote?product=gunnar&tenant=team%2Fpaid&bundle=100gb", Auth::Cookie(&viewer), None)
.await;
assert_eq!(status, StatusCode::OK, "{body}");
let asked = rig.mock.quotes.lock().unwrap().clone();
assert_eq!(asked.len(), 1, "one quote, and it reached monetize");
assert_eq!(asked[0].bundle, "100gb", "the shop window's id, echoed back unread: {:?}", asked[0]);
assert!(asked[0].caps.is_empty(), "a request names a bundle OR caps, never both: {:?}", asked[0]);
// And it reserved nothing. A quote is a read wherever it is asked from.
assert_eq!(rig.mock.starts.lock().unwrap().len(), 0, "quoting a bundle placed an order");
}
// ── STEP-UP (5.4 §3b, owner: "passkeys where they fit") ────────────────
//
// A 12-hour session reads; it does not spend. Every mutation under `/api/`
// (an order, a refund, the operator `Entitlement.Set`, a backup, a growth
// approval, a repair button) and enrolling a passkey need a STRONG session:
// an admin passkey, user-verified, touched within `STEP_UP_SECS`. The
// refusal is a 403 with `code: "stepup"`, which the console answers by
// asking for the touch and sending the same request again. The break-glass
// bearer is the machine door and stays a token.
const MUTATIONS: &[&str] = &[
"/api/purchase",
"/api/purchase/abandon",
"/api/entitlement/gunnar/team%2Fpaid",
"/api/backup",
"/api/grow/gunnar/start",
"/api/grow/gunnar/resume",
"/api/guardian/acme/press",
"/api/guardian/acme/auto-fix",
];
fn stale(rig: &Rig) -> String {
let now = monetize_ui_server::auth::now_secs();
rig.webauthn.mint_session_at("bob", Role::Admin, true, now - monetize_ui_server::auth::STEP_UP_SECS - 1)
}
/// ★ **Money needs a fresh passkey touch.** An admin whose login is older
/// than the step-up window, or whose key did not verify the user, is refused
/// with `stepup` and NOTHING reaches monetize; the same admin a moment after a
/// user-verified touch places the order.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn money_needs_a_fresh_user_verified_passkey_touch() {
let rig = Rig::up().await;
let body = json!({ "product": "gunnar", "tenant": "team/paid", "caps": { "pack_bytes": 10u64 << 30 }, "vendor": "mockpay" });
let (status, out, _) = rig.call(Method::POST, "/api/purchase", Auth::Cookie(&stale(&rig)), Some(body.clone())).await;
assert_eq!(status, StatusCode::FORBIDDEN, "{out}");
assert_eq!(out["code"], monetize_ui_server::auth::STEP_UP_CODE, "the console must be able to tell a step-up from a role refusal: {out}");
assert!(out["message"].as_str().unwrap_or("").contains("passkey"), "the refusal says what to do: {out}");
let untouched = rig.webauthn.mint_session("bob", Role::Admin, false);
let (status, out, _) = rig.call(Method::POST, "/api/purchase", Auth::Cookie(&untouched), Some(body.clone())).await;
assert_eq!((status, out["code"].as_str()), (StatusCode::FORBIDDEN, Some("stepup")), "a tap without user verification is not a person: {out}");
assert_eq!(rig.mock.starts.lock().unwrap().len(), 0, "a step-up refusal must not reach monetize");
let (status, out, _) = rig.call(Method::POST, "/api/purchase", Auth::Cookie(&rig.cookie(Role::Admin)), Some(body.clone())).await;
assert_eq!(status, StatusCode::OK, "{out}");
assert_eq!(rig.mock.starts.lock().unwrap().len(), 1);
// The break-glass bearer is a token, not a person, and stays the machine door.
let (status, out, _) = rig.call(Method::POST, "/api/purchase", Auth::Token, Some(body)).await;
assert_eq!(status, StatusCode::OK, "{out}");
}
/// ★ **Every mutation is behind the step-up, and no read is.** An old admin
/// session still sees everything, and every POST under `/api/` is refused
/// with `stepup` before it reaches monetize.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn every_mutation_wants_a_fresh_touch_and_no_read_does() {
let rig = Rig::up().await;
let old = stale(&rig);
for p in MUTATIONS {
let (status, out, _) = rig.call(Method::POST, p, Auth::Cookie(&old), Some(json!({}))).await;
assert_eq!((status, out["code"].as_str()), (StatusCode::FORBIDDEN, Some("stepup")), "POST {p} with an old session: {out}");
}
assert!(rig.mock.starts.lock().unwrap().is_empty() && rig.mock.sets.lock().unwrap().is_empty() && rig.mock.exports.lock().unwrap().is_empty(), "nothing reached monetize");
for p in ["/api/products", "/api/tenants/gunnar/team%2Fpaid", "/api/ledger?unfinished=1"] {
let (status, out, _) = rig.call(Method::GET, p, Auth::Cookie(&old), None).await;
assert_eq!(status, StatusCode::OK, "GET {p} is a read and needs no touch: {out}");
}
}
/// ★ **Enrolling a passkey is a step-up too**: a stolen 12-hour cookie must
/// not be able to add its thief's key and outlive the theft.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn enrolling_a_passkey_wants_a_fresh_touch() {
let rig = Rig::up().await;
let (status, out, _) = rig.call(Method::GET, "/auth/passkey/register/start", Auth::Cookie(&stale(&rig)), None).await;
assert_eq!((status, out["code"].as_str()), (StatusCode::FORBIDDEN, Some("stepup")), "{out}");
let (status, out, _) = rig.call(Method::POST, "/auth/passkey/register/finish", Auth::Cookie(&stale(&rig)), Some(json!({}))).await;
assert_eq!((status, out["code"].as_str()), (StatusCode::FORBIDDEN, Some("stepup")), "{out}");
let (status, out, _) = rig.call(Method::GET, "/auth/passkey/register/start", Auth::Cookie(&rig.cookie(Role::Admin)), None).await;
assert_eq!(status, StatusCode::OK, "{out}");
assert!(out["publicKey"].is_object(), "{out}");
// Usernameless (owner 2026-10-11): the key keeps a discoverable,
// user-verified passkey, and the console names the operator by handle.
assert_eq!(out["publicKey"]["authenticatorSelection"]["residentKey"], "required", "{out}");
rig.webauthn.adopt(StoredCred { id: "k1".into(), x: vec![0; 32], y: vec![0; 32], sign_count: 0, label: "yubikey".into(), created: 0, user: "op-bob".into(), role: Role::Admin, last_used: None, disabled: false }).unwrap();
let (status, out, _) = rig.call(Method::GET, "/auth/passkey/login/start?user=bob", Auth::None, None).await;
assert_eq!(status, StatusCode::OK, "{out}");
assert!(out["publicKey"]["allowCredentials"].as_array().is_some_and(|a| a.is_empty()) && out["publicKey"]["userVerification"] == "required", "{out}");
}
/// ★ RED before U1 (owner 2026-10-19): **the console knows whether the signed-in principal
/// already holds a key here**, so the passkeys page never puts "Enrol as admin" in front of
/// someone whose key is enrolled.
#[tokio::test]
async fn the_status_says_whether_the_principal_holds_a_key() {
let rig = Rig::up().await;
let (_, body, _) = rig.call(Method::GET, "/auth/status", Auth::Cookie(&rig.cookie(Role::Admin)), None).await;
assert_eq!(body["has_passkey"], false, "bob holds no key yet: {body}");
rig.webauthn.adopt(StoredCred { id: "k1".into(), x: vec![0; 32], y: vec![0; 32], sign_count: 0, label: "yubikey".into(), created: 0, user: "bob".into(), role: Role::Admin, last_used: None, disabled: false }).unwrap();
let (_, body, _) = rig.call(Method::GET, "/auth/status", Auth::Cookie(&rig.cookie(Role::Admin)), None).await;
assert_eq!((body["has_passkey"].as_bool(), body["bootstrap"].as_bool()), (Some(true), Some(false)), "{body}");
let (_, body, _) = rig.call(Method::GET, "/auth/status", Auth::None, None).await;
assert_eq!(body["has_passkey"], false, "nobody signed in holds nothing: {body}");
}
/// The box's half for the certificate panel: one host, and the hosts "try now" was asked for.
struct CertBox(std::sync::Mutex<Vec<String>>);
impl monetize_ui_server::self_box::SelfBox for CertBox {
fn look(&self) -> Result<monetize_ui_server::self_box::SelfLook, String> {
Ok(Default::default())
}
fn ask_update(&self, _by: &str) -> Result<(), String> {
Ok(())
}
fn certs(&self) -> Result<Value, String> {
Ok(json!({"certificates": [{"host": "test2.example", "lines": ["valid until: no valid certificate", "last reason: dns: test2.example does not point at this box (it resolves to nothing)"], "try_now": true}]}))
}
fn try_cert(&self, host: &str, _by: &str) -> Result<String, String> {
if host == "v.example" {
return Err("REFUSED [tls-try-now-validation] the last ACME attempt failed validation".into());
}
self.0.lock().unwrap().push(host.to_string());
Ok(format!("tls: {host} is looked at now"))
}
}
/// ★ **The ONE box's certificates on its console** (owner 2026-10-25): the box's own report,
/// passed through as it is (a viewer may read it); "try now" is an admin's, and the box's
/// refusal is answered in its words.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn the_boxs_certificates_are_shown_as_the_box_says_and_try_now_is_an_admins() {
let the_box = Arc::new(CertBox(std::sync::Mutex::new(Vec::new())));
monetize_ui_server::self_box::install(the_box.clone());
let rig = Rig::up().await;
let viewer = rig.cookie(Role::Viewer);
let (status, v, _) = rig.call(Method::GET, "/api/self/certs", Auth::Cookie(&viewer), None).await;
assert_eq!(status, StatusCode::OK, "{v}");
assert_eq!(v["certificates"][0]["host"], "test2.example");
assert!(v["certificates"][0]["lines"][1].as_str().unwrap().starts_with("last reason: dns:"), "{v}");
let (status, _, _) = rig.call(Method::POST, "/api/self/certs/try", Auth::Cookie(&viewer), Some(json!({"host": "test2.example"}))).await;
assert_eq!(status, StatusCode::FORBIDDEN, "a viewer does not press try now");
let admin = rig.cookie(Role::Admin);
let (status, v, _) = rig.call(Method::POST, "/api/self/certs/try", Auth::Cookie(&admin), Some(json!({"host": "test2.example"}))).await;
assert_eq!(status, StatusCode::OK, "{v}");
assert_eq!(v["words"], "tls: test2.example is looked at now");
assert_eq!(*the_box.0.lock().unwrap(), vec!["test2.example".to_string()]);
let (status, v, _) = rig.call(Method::POST, "/api/self/certs/try", Auth::Cookie(&admin), Some(json!({"host": "v.example"}))).await;
assert!(status.is_client_error(), "{status}");
assert!(v.to_string().contains("tls-try-now-validation"), "{v}");
}