Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/monetize

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}");
}