crates/monetize-ui-server/tests/one_ledger.rs
//! **The console reads the ONE monetize, and nothing else** (ONE monetize,
//! owner 2026-10-10): no estate runs a monetize of its own, so the console has
//! no estate to dial. It is given ONE address — the ONE monetize — and every
//! estate it shows is one the FLEET has heard call home (`Guardian.Cases`, the
//! same box records `fleet::heard_since` reads), every customer an ACCOUNT on
//! the ONE ledger (`Accounts.List`), named by its path: `gunnar/<t>`,
//! `gunnar/@<company>/<ns>` (a sold estate's tenant), `holger/<t>`.
//!
//! In-process: one mock ONE monetize behind the REAL router the binary serves.
//! The fleet names three estates and no address for any of them: there is
//! nothing to dial. The binary refuses the old `--estates` file by name.
use std::sync::{Arc, Mutex};
use std::time::Duration;
use axum::body::Body;
use axum::Router;
use http::{Request, StatusCode};
use http_body_util::BodyExt;
use monetize_proto as pb;
use monetize_proto::accounts_server::{Accounts, AccountsServer};
use monetize_proto::entitlement_server::{Entitlement, EntitlementServer};
use monetize_proto::estate_admin_server::{EstateAdmin, EstateAdminServer};
use monetize_proto::estates_server::{Estates, EstatesServer};
use monetize_proto::guardian_server::{Guardian, GuardianServer};
use monetize_proto::ledger_server::{Ledger, LedgerServer};
use monetize_proto::products_server::{Products, ProductsServer};
use monetize_ui_server::app::App;
use monetize_ui_server::grpc::Monetize;
use monetize_ui_server::serve;
use monetize_ui_server::sse::EventHub;
use serde_json::{json, Value};
use tonic::{Request as TReq, Response, Status};
use tower::ServiceExt;
const BEARER: &str = "the-one-ledger-bearer";
const ADMIN: &str = "break-glass-one-ledger";
const DAY: u64 = 24 * 60 * 60 * 1000;
const GIB: u64 = 1 << 30;
fn now_ms() -> u64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_millis() as u64
}
/// The ONE monetize: its fleet, its ledger, its accounts.
#[derive(Default)]
struct One {
now: u64,
draws: Mutex<Vec<String>>,
}
#[derive(Clone)]
struct Svc(Arc<One>);
fn boxed(role: &str, heard: u64, silent: bool) -> pb::GuardianBox {
pb::GuardianBox {
role: role.into(),
heard_unix_ms: heard,
silent,
..Default::default()
}
}
fn fact(tenant: &str, state: pb::State, until: Option<u64>, room: u64) -> pb::EntitlementFact {
pb::EntitlementFact {
tenant: tenant.into(),
state: state as i32,
paid_until_unix_ms: until,
caps: [("pack_bytes".to_string(), room)].into(),
..Default::default()
}
}
fn account(product: &str, tenant: &str, state: pb::State, instances: u32) -> pb::AccountSummary {
pb::AccountSummary {
product: product.into(),
tenant: tenant.into(),
vendor: format!("stripe/{product}"),
state: state as i32,
paid_until_unix_ms: Some(1_900_000_000_000),
rows: 1 + instances,
instances,
last_unix_ms: 1,
..Default::default()
}
}
fn sale(
product: &str,
tenant: &str,
minor: u64,
currency: pb::Currency,
livemode: Option<bool>,
started: u64,
) -> pb::Sale {
pb::Sale {
reference: format!("{product}/{tenant}/{started}"),
product: product.into(),
tenant: tenant.into(),
price: Some(pb::Money {
minor,
currency: currency as i32,
}),
started_unix_ms: started,
at_unix_ms: started,
drawn: true,
outcome: "granted".into(),
livemode,
..Default::default()
}
}
#[tonic::async_trait]
impl Guardian for Svc {
async fn cases(
&self,
_: TReq<pb::GuardianCasesRequest>,
) -> Result<Response<pb::GuardianCasesResponse>, Status> {
let n = self.0.now;
Ok(Response::new(pb::GuardianCasesResponse {
estates: vec![
// gunnar's own estate: its main has not called home.
pb::GuardianEstate {
instance: "gunnar-rs".into(),
product: "gunnar".into(),
boxes: vec![boxed("main", 0, true)],
..Default::default()
},
// A sold Private Gunnar: declared under gunnar at its install, both boxes heard.
pb::GuardianEstate {
instance: "acme".into(),
product: "gunnar".into(),
boxes: vec![
boxed("main", n - 10_000, false),
boxed("twin", n - 20_000, false),
],
..Default::default()
},
pb::GuardianEstate {
instance: "holger-rs".into(),
product: "holger".into(),
boxes: vec![boxed("main", n - 5_000, false)],
..Default::default()
},
],
}))
}
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"))
}
}
#[tonic::async_trait]
impl Accounts for Svc {
async fn list(
&self,
_: TReq<pb::ListAccountsRequest>,
) -> Result<Response<pb::ListAccountsResponse>, Status> {
Ok(Response::new(pb::ListAccountsResponse {
accounts: vec![
account("holger", "bob", pb::State::Paid, 0),
account("gunnar", "alice", pb::State::Paid, 1),
account("gunnar", "@acme/ns1", pb::State::Paid, 0),
account("gunnar", "@acme/ns2", pb::State::Free, 0),
],
vendors: [
("gunnar".to_string(), "stripe/gunnar".to_string()),
("holger".to_string(), "stripe/holger".to_string()),
]
.into(),
}))
}
async fn rows(
&self,
r: TReq<pb::AccountRowsRequest>,
) -> Result<Response<pb::AccountRowsResponse>, Status> {
let r = r.into_inner();
if (r.product.as_str(), r.tenant.as_str()) != ("gunnar", "alice") {
return Err(Status::not_found("only alice owns an instance here"));
}
let instance = pb::AccountInstance {
kind: "estate".into(),
company: "acme".into(),
host: "acme.gunnar.rs".into(),
phase: "laid".into(),
state: "ready".into(),
paid_until: "2027-04-07".into(),
..Default::default()
};
Ok(Response::new(pb::AccountRowsResponse {
account: Some(account("gunnar", "alice", pb::State::Paid, 1)),
rows: vec![pb::AccountRow {
kind: "instance".into(),
id: "acme".into(),
vendor: "stripe/gunnar".into(),
instance: Some(instance),
..Default::default()
}],
}))
}
async fn instance_log(
&self,
r: TReq<pb::InstanceLogRequest>,
) -> Result<Response<pb::InstanceLogResponse>, Status> {
if r.into_inner().company != "acme" {
return Err(Status::not_found("only acme is an instance here"));
}
let line = |at: u64, source: &str, line: &str| pb::InstanceLogLine {
at_unix_ms: at,
source: source.into(),
line: line.into(),
};
Ok(Response::new(pb::InstanceLogResponse {
lines: vec![
line(10, "order", "held: the money is held"),
line(20, "installer", "front up"),
],
}))
}
}
#[tonic::async_trait]
impl Products for Svc {
async fn list(
&self,
_: TReq<pb::ListProductsRequest>,
) -> Result<Response<pb::ListProductsResponse>, Status> {
Ok(Response::new(pb::ListProductsResponse {
products: vec![
pb::ProductInfo {
id: "gunnar".into(),
..Default::default()
},
pb::ProductInfo {
id: "holger".into(),
..Default::default()
},
],
vendors: vec!["stripe/gunnar".into(), "stripe/holger".into()],
vendor_modes: vec![
pb::VendorMode {
vendor: "stripe/gunnar".into(),
livemode: Some(true),
},
pb::VendorMode {
vendor: "stripe/holger".into(),
livemode: Some(false),
},
],
..Default::default()
}))
}
}
#[tonic::async_trait]
impl Entitlement for Svc {
async fn get(
&self,
_: TReq<pb::GetEntitlementRequest>,
) -> Result<Response<pb::GetEntitlementResponse>, Status> {
Err(Status::unimplemented("not in this mock"))
}
async fn list(
&self,
r: TReq<pb::ListEntitlementsRequest>,
) -> Result<Response<pb::ListEntitlementsResponse>, Status> {
let facts = match r.into_inner().product.as_str() {
"gunnar" => vec![
fact("alice", pb::State::Paid, Some(1_900_000_000_000), 10 * GIB),
fact(
"@acme/ns1",
pb::State::Paid,
Some(1_900_000_000_000),
5 * GIB,
),
// An account without room there: monetize's one-byte cap.
fact("@acme/ns2", pb::State::Free, None, 1),
],
"holger" => vec![fact(
"bob",
pb::State::Paid,
Some(1_900_000_000_000),
2 * GIB,
)],
p => {
return Err(Status::not_found(format!(
"product {p:?} is not served here"
)))
}
};
Ok(Response::new(pb::ListEntitlementsResponse { facts }))
}
async fn set(
&self,
_: TReq<pb::SetEntitlementRequest>,
) -> Result<Response<pb::SetEntitlementResponse>, Status> {
Err(Status::unimplemented("not in this mock"))
}
async fn public_key(
&self,
_: TReq<pb::PublicKeyRequest>,
) -> Result<Response<pb::PublicKeyResponse>, Status> {
Err(Status::unimplemented("not in this mock"))
}
async fn held(&self, _: TReq<pb::HeldRequest>) -> Result<Response<pb::HeldResponse>, Status> {
Err(Status::unimplemented("not in this mock"))
}
}
#[tonic::async_trait]
impl Ledger for Svc {
async fn query(
&self,
_: TReq<pb::QueryLedgerRequest>,
) -> Result<Response<pb::QueryLedgerResponse>, Status> {
Err(Status::unimplemented("not in this mock"))
}
async fn export(
&self,
_: TReq<pb::ExportBackupRequest>,
) -> Result<Response<pb::ExportBackupResponse>, Status> {
Err(Status::unimplemented("not in this mock"))
}
async fn sales(
&self,
_: TReq<pb::SalesRequest>,
) -> Result<Response<pb::SalesResponse>, Status> {
let n = self.0.now;
Ok(Response::new(pb::SalesResponse {
sales: vec![
sale(
"gunnar",
"@acme/ns1",
1_000,
pb::Currency::Eur,
Some(true),
n - DAY,
),
sale(
"gunnar",
"alice",
5_000,
pb::Currency::Eur,
Some(true),
n - DAY,
),
sale(
"holger",
"bob",
300,
pb::Currency::Sek,
Some(false),
n - DAY,
),
],
}))
}
}
#[tonic::async_trait]
impl EstateAdmin for Svc {
async fn draw(
&self,
r: TReq<pb::DrawEstateRequest>,
) -> Result<Response<pb::DrawEstateResponse>, Status> {
let r = r.into_inner();
let p = r.product;
self.0
.draws
.lock()
.unwrap()
.push(format!("{p} {}", r.estate));
Ok(Response::new(pb::DrawEstateResponse {
svg: format!("<svg id=\"{p}\" data-estate=\"{}\"/>", r.estate),
machines: Some(1),
}))
}
}
/// ★ OVW-CLEAN: **the ONE monetize classifies its estates**; the console carries it.
#[tonic::async_trait]
impl Estates for Svc {
async fn overview(
&self,
_: TReq<pb::EstatesOverviewRequest>,
) -> Result<Response<pb::EstatesOverviewResponse>, Status> {
let line = |name: &str,
product: &str,
standing: &str,
words: &str,
in_fleet: bool,
domain: &str| pb::EstateStanding {
name: name.into(),
product: product.into(),
standing: standing.into(),
words: words.into(),
in_fleet,
domain: domain.into(),
};
Ok(Response::new(pb::EstatesOverviewResponse {
estates: vec![
line(
"acme",
"gunnar",
"installed",
"installed: its machines have reported in",
true,
"acme.gunnar.rs",
),
line(
"holger-rs",
"holger",
"installed",
"installed: its machines have reported in",
true,
"holger.rs",
),
line(
"test4-gunnar-rs",
"gunnar",
"laying",
"ordered: waiting for its payment",
false,
"test4.gunnar.rs",
),
],
ended: vec![
line(
"gunnar-rs",
"gunnar",
"never_installed",
"never installed: no machine of it ever reported in",
true,
"gunnar.rs",
),
line(
"polka",
"gunnar",
"ended",
"terminated on 2026-10-27",
true,
"",
),
],
slot: Some(pb::NewEstateSlot {
products: vec!["gunnar".into(), "holger".into()],
declared: vec!["test2-gunnar-rs".into()],
}),
}))
}
async fn new_estate(
&self,
_: TReq<pb::NewEstateRequest>,
) -> Result<Response<pb::NewEstateResponse>, Status> {
Err(Status::permission_denied(
"the console's service bearer is not the operator's",
))
}
}
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(one: Arc<One>) -> String {
let l = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let at = l.local_addr().unwrap();
let s = || Svc(one.clone());
tokio::spawn(
tonic::transport::Server::builder()
.add_service(GuardianServer::with_interceptor(s(), bearer_gate))
.add_service(AccountsServer::with_interceptor(s(), bearer_gate))
.add_service(ProductsServer::with_interceptor(s(), bearer_gate))
.add_service(EntitlementServer::with_interceptor(s(), bearer_gate))
.add_service(LedgerServer::with_interceptor(s(), bearer_gate))
.add_service(EstateAdminServer::with_interceptor(s(), bearer_gate))
.add_service(EstatesServer::with_interceptor(s(), bearer_gate))
.serve_with_incoming(tonic::transport::server::TcpIncoming::from(l)),
);
at.to_string()
}
/// The console the ONE box serves: ONE app over the ONE monetize, nothing else.
async fn console() -> (Router, Arc<One>) {
let one = Arc::new(One {
now: now_ms(),
..Default::default()
});
let at = spawn(one.clone()).await;
let monetize = Monetize::connect_lazy(&at, BEARER).unwrap();
let app = Arc::new(App {
operators: None,
admin_token: Some(ADMIN.into()),
events: EventHub::spawn(monetize.clone(), Duration::from_secs(3600)),
monetize,
operator: None,
ui_dir: None,
});
(serve::router(app), one)
}
async fn get(router: &Router, path: &str) -> (StatusCode, http::HeaderMap, Value) {
let req = Request::builder()
.uri(path)
.header("authorization", format!("Bearer {ADMIN}"))
.body(Body::empty())
.unwrap();
let res = router.clone().oneshot(req).await.unwrap();
let (status, headers) = (res.status(), res.headers().clone());
let bytes = res.into_body().collect().await.unwrap().to_bytes();
let v = serde_json::from_slice(&bytes)
.unwrap_or_else(|_| Value::String(String::from_utf8_lossy(&bytes).into_owned()));
(status, headers, v)
}
fn h(headers: &http::HeaderMap, name: &str) -> String {
headers
.get(name)
.and_then(|v| v.to_str().ok())
.unwrap_or_default()
.to_owned()
}
/// ★ **The estates are the FLEET's**: every estate the ONE monetize has
/// declared, with its product and which of its boxes called home; one that
/// has not is named so, in the pull model's words. Nobody is dialled: there is
/// no `control` to dial.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn the_estates_are_the_fleets_seen_by_calling_home() {
let (router, _) = console().await;
let (st, _, v) = get(&router, "/api/estates").await;
assert_eq!(st, StatusCode::OK, "{v}");
let names: Vec<&str> = v["estates"]
.as_array()
.unwrap()
.iter()
.filter_map(|e| e["name"].as_str())
.collect();
assert_eq!(names, ["acme", "gunnar-rs", "holger-rs"], "{v}");
let acme = &v["estates"][0];
assert_eq!(acme["product"], "gunnar", "{v}");
assert_eq!(acme["serving"], true, "{v}");
assert_eq!(acme["heard"], json!(["main", "twin"]), "{v}");
assert!(acme["last_heard_unix_ms"].as_u64().unwrap() > 0, "{v}");
let quiet = &v["estates"][1];
assert_eq!(quiet["serving"], false, "{v}");
assert_eq!(quiet["heard"], json!([]), "{v}");
let words = quiet["error"].as_str().unwrap_or_default();
assert!(
words.contains("is not installed yet"),
"an estate nothing of which reported in says so plainly: {v}"
);
assert_eq!(quiet["installed"], false, "{v}");
for e in v["estates"].as_array().unwrap() {
assert!(
e.get("control").is_none_or(Value::is_null),
"an estate has no control to dial: {v}"
);
}
// ★ OVW-CLEAN: the Overview is the ONE monetize's classification, carried field for field:
// the rows that exist, the quiet line, the slot. gunnar-rs, never installed, is no row.
let (st, _, o) = get(&router, "/api/overview").await;
assert_eq!(st, StatusCode::OK, "{o}");
let names = |k: &str| {
o[k].as_array()
.unwrap()
.iter()
.map(|e| e["name"].as_str().unwrap_or_default().to_owned())
.collect::<Vec<_>>()
};
assert_eq!(
names("estates"),
["acme", "holger-rs", "test4-gunnar-rs"],
"{o}"
);
assert_eq!(
o["estates"][2],
json!({ "name": "test4-gunnar-rs", "product": "gunnar", "standing": "laying", "words": "ordered: waiting for its payment", "in_fleet": false, "domain": "test4.gunnar.rs" }),
"{o}"
);
assert_eq!(names("ended"), ["gunnar-rs", "polka"], "{o}");
assert_eq!(o["ended"][1]["words"], "terminated on 2026-10-27", "{o}");
assert_eq!(
o["slot"],
json!({ "products": ["gunnar", "holger"], "declared": ["test2-gunnar-rs"] }),
"{o}"
);
assert!(o["stale_secs"].is_null(), "{o}");
}
/// ★ OVW-CLEAN: **the slot is a real purchase**: unconfirmed it is refused here; confirmed, it is
/// the operator's — a console that holds no operator credential sends nothing.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn the_new_estate_slot_is_confirmed_and_the_operators() {
let (router, _) = console().await;
let post = |body: Value| {
let router = router.clone();
async move {
let req = Request::builder()
.method("POST")
.uri("/api/estates/new")
.header("authorization", format!("Bearer {ADMIN}"))
.header("content-type", "application/json")
.body(Body::from(body.to_string()))
.unwrap();
let res = router.oneshot(req).await.unwrap();
let st = res.status();
let bytes = res.into_body().collect().await.unwrap().to_bytes();
(st, String::from_utf8_lossy(&bytes).into_owned())
}
};
let ask =
json!({ "name": "test4-gunnar-rs", "product": "gunnar", "email": "ops@example.invalid" });
let (st, body) = post(ask.clone()).await;
assert_eq!(st, StatusCode::BAD_REQUEST, "{body}");
assert!(body.contains("confirm"), "{body}");
let mut confirmed = ask;
confirmed["confirm"] = json!(true);
let (st, body) = post(confirmed).await;
assert_eq!(st, StatusCode::SERVICE_UNAVAILABLE, "{body}");
assert!(body.contains("no operator credential"), "{body}");
}
/// ★ UW-13: **a private instance's setup log** is `Accounts.InstanceLog`, line for line; an
/// instance monetize does not hold is its 404.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn a_private_instances_setup_log_is_read_from_the_one() {
let (router, _) = console().await;
let (st, _, v) = get(&router, "/api/instances/acme/log").await;
assert_eq!(st, StatusCode::OK, "{v}");
assert_eq!(
v["lines"],
json!([{ "at_unix_ms": 10, "source": "order", "line": "held: the money is held" }, { "at_unix_ms": 20, "source": "installer", "line": "front up" }]),
"{v}"
);
let (st, _, v) = get(&router, "/api/instances/ghost/log").await;
assert_eq!(st, StatusCode::NOT_FOUND, "{v}");
}
/// ★ UW-8: **an account's private instance is last heard when its estate last called
/// home**: the Accounts row of alice's instance `acme` carries the fleet's newest report of
/// estate `acme` (its main, 10 s ago), never "never".
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn an_accounts_private_instance_is_last_heard_from_the_fleet() {
let (router, _) = console().await;
let before = now_ms();
let (st, _, v) = get(&router, "/api/accounts/gunnar/alice").await;
assert_eq!(st, StatusCode::OK, "{v}");
let acme = v["rows"]
.as_array()
.unwrap()
.iter()
.find(|r| r["id"] == "acme")
.cloned()
.unwrap_or_else(|| panic!("acme's row: {v}"));
let heard = acme["last_heard_unix_ms"]
.as_u64()
.unwrap_or_else(|| panic!("heard, not never: {acme}"));
assert!(
heard + 60_000 >= before && heard + 10_000 <= now_ms(),
"the main's report, 10 s before the rig's now: {heard} vs {before}"
);
}
/// ★ **One Overview row, from the ONE ledger and the fleet**: a sold
/// estate's customers and money are its `@<estate>/` accounts', its machines
/// the boxes the fleet knows; an estate that has not called home is not
/// "reached", and its ledger still answers.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn an_estates_summary_is_the_one_ledgers_and_the_fleets() {
let (router, _) = console().await;
let (st, _, acme) = get(&router, "/api/estates/acme/summary").await;
assert_eq!(st, StatusCode::OK, "{acme}");
assert_eq!(acme["estate"], "acme");
assert_eq!(acme["reached"], true, "{acme}");
assert_eq!(acme["products"], json!(["gunnar"]), "{acme}");
assert_eq!(acme["machines"], 2, "{acme}");
assert_eq!(
acme["machine_roles"],
json!(["main", "twin"]),
"the machines that reported in: {acme}"
);
assert_eq!(acme["customers"], 2, "{acme}");
assert_eq!(acme["revenue"]["live"], json!({ "EUR": 1_000 }), "{acme}");
let (st, _, own) = get(&router, "/api/estates/gunnar-rs/summary").await;
assert_eq!(st, StatusCode::OK, "{own}");
assert_eq!(own["reached"], false, "{own}");
assert!(
own["error"]
.as_str()
.unwrap_or_default()
.contains("is not installed yet"),
"{own}"
);
assert_eq!(own["installed"], false, "{own}");
assert_eq!(own["customers"], 1, "{own}");
assert_eq!(own["revenue"]["live"], json!({ "EUR": 5_000 }), "{own}");
let (st, _, none) = get(&router, "/api/estates/nowhere/summary").await;
assert_eq!(
st,
StatusCode::NOT_FOUND,
"an estate the fleet does not know: {none}"
);
}
/// **The map of an estate is drawn by the ONE monetize** for that estate's
/// product, and names the estate it drew.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn an_estates_map_is_drawn_by_the_one_monetize_for_its_product() {
let (router, one) = console().await;
let (st, headers, body) = get(&router, "/api/estates/holger-rs/map").await;
assert_eq!(st, StatusCode::OK);
assert_eq!(
(
h(&headers, "x-monetize-estate"),
h(&headers, "x-monetize-product")
),
("holger-rs".into(), "holger".into())
);
assert_eq!(
h(&headers, "content-type"),
"image/svg+xml",
"the page's <img> takes it"
);
// ★ The SITE is asked for (owner 2026-10-12: "the svg is gone"): the ONE box
// draws that site's own reported machines, not the product's empty board.
assert_eq!(*one.draws.lock().unwrap(), ["holger holger-rs"]);
assert!(
body.as_str()
.is_some_and(|b| b.starts_with("<svg") && b.contains("data-estate=\"holger-rs\"")),
"{body}"
);
let (st, _, _) = get(&router, "/api/estates/nowhere/map").await;
assert_eq!(st, StatusCode::NOT_FOUND);
}
/// ★ **The old `--estates` file is LEGACY**, refused by name: the console is
/// given the ONE monetize and nothing else.
#[test]
fn the_binary_refuses_an_estates_file_as_legacy() {
let dir = std::path::Path::new(env!("CARGO_TARGET_TMPDIR")).join("one-ledger-legacy");
std::fs::create_dir_all(&dir).unwrap();
let file = dir.join("estates.toml");
std::fs::write(
&file,
"[[estate]]\nname = \"gunnar.rs\"\ncontrol = \"https://monetize.gunnar.rs\"\nca = \"x\"\n",
)
.unwrap();
let out = std::process::Command::new(env!("CARGO_BIN_EXE_monetize-ui-server"))
.args([
"--bind",
"127.0.0.1:0",
"--estates",
&file.display().to_string(),
"--data",
&dir.display().to_string(),
])
.env("MONETIZE_UI_ADMIN_TOKEN", ADMIN)
.env_remove("MONETIZE_UI_WEBAUTHN_RP_ID")
.output()
.unwrap();
let err = String::from_utf8_lossy(&out.stderr);
assert!(!out.status.success(), "{err}");
assert!(err.contains("--estates") && err.contains("LEGACY"), "{err}");
}
/// A console outside the ONE box signs every call: an https `--control`
/// without `--control-key` is refused by name.
#[test]
fn an_https_control_without_a_key_is_refused() {
let dir = std::path::Path::new(env!("CARGO_TARGET_TMPDIR")).join("one-ledger-https");
std::fs::create_dir_all(&dir).unwrap();
let ca = dir.join("one.pem");
std::fs::write(
&ca,
"-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----\n",
)
.unwrap();
let out = std::process::Command::new(env!("CARGO_BIN_EXE_monetize-ui-server"))
.args([
"--bind",
"127.0.0.1:0",
"--control",
"https://monetize.one.test",
"--control-ca",
&ca.display().to_string(),
"--data",
&dir.display().to_string(),
])
.env("MONETIZE_UI_ADMIN_TOKEN", ADMIN)
.env_remove("MONETIZE_UI_WEBAUTHN_RP_ID")
.output()
.unwrap();
let err = String::from_utf8_lossy(&out.stderr);
assert!(!out.status.success(), "{err}");
assert!(err.contains("--control-key"), "{err}");
}