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

nordisk/monetize

uitest/src/mock.rs

//! **A monetize on the wire, serving the fixture.** The six generated server
//! traits over [`crate::fixture`], with every call an operator could make
//! RECORDED, so a test can prove not only what came back but what was asked —
//! and, for a refused call, that nothing reached monetize at all.
//!
//! It is the shape of the mock in `crates/monetize-ui-server/tests/api.rs`,
//! lifted out so it can be TWO things at once: an in-process gRPC server for
//! the BFF's own tests (`serve` on a tokio runtime the test owns), and a real
//! process the browser matrix starts (`monetize-uitest --mock-serve`) for the
//! real `monetize-ui-server` binary to talk to. One mock, both halves.
//!
//! The mock SIGNS facts with a real Ed25519 key (`sign`), and answers
//! `Entitlement.PublicKey` with its verifying half, so the console's "verified"
//! badge is checking a genuine signature — a fixture handing out `vec![7; 64]`
//! could not tell a working verification from a missing one.
//!
//! The mock also ENFORCES the suite bearer (`bearer_gate`): a BFF that forgot
//! to present it gets `Unauthenticated`, which is how the harness proves the
//! token travels.

// Shared by `#[path]` with the server tests: not every item is used by every includer.
#![allow(dead_code)]

use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, Mutex};

use monetize_proto as pb;
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::events_server::{Events, EventsServer};
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::tax_server::{Tax, TaxServer};
use monetize_proto::usage_server::{Usage, UsageServer};
use tonic::{Request as TReq, Response, Status};

use crate::fixture;

/// The signing key. Fixed bytes, so a fact signed by one mock process verifies
/// against the key another mock process publishes — the browser rig's mock and
/// the in-process one agree.
pub 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(&[5u8; 32]))
}

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,
        caps: f.caps.clone(),
        source: f.source.clone(),
        signature: f.signature.clone(),
        issued_unix_ms: f.issued_unix_ms,
        issued_signature: f.issued_signature.clone(),
    }
}

/// Sign a fact both ways, stamped — what monetize-server mints. The V1
/// signature over `fact_message` and the V2 one over `fact_message_issued`; see
/// `monetize_product::EntitlementFact`.
pub fn sign(mut f: pb::EntitlementFact) -> pb::EntitlementFact {
    use ed25519_dalek::Signer as _;
    if f.issued_unix_ms.is_none() {
        f.issued_unix_ms = Some(fixture::ISSUED_AT);
    }
    // Both forms drop both signature fields, so one conversion serves both.
    let rust = pb_to_embed(&f);
    f.signature = signing_key().sign(&monetize_embed::signing::fact_message(&rust)).to_bytes().to_vec();
    f.issued_signature = signing_key().sign(&monetize_embed::signing::fact_message_issued(&rust)).to_bytes().to_vec();
    f
}

/// What the BFF asked. Every vector starts empty; a refused route must leave
/// it empty, and an allowed one must not.
#[derive(Default)]
pub struct Calls {
    pub starts: Mutex<Vec<pb::StartPurchaseRequest>>,
    /// Every `Purchase.Quote` — a READ, not a mutation, so a viewer may leave
    /// it non-empty; recorded so a test can prove the console asked before it
    /// offered a confirm button.
    pub quotes: Mutex<Vec<pb::QuoteRequest>>,
    pub abandons: Mutex<Vec<pb::AbandonPurchaseRequest>>,
    pub sets: Mutex<Vec<pb::SetEntitlementRequest>>,
    pub exports: Mutex<Vec<()>>,
    pub trends_asked: Mutex<Vec<pb::TrendRequest>>,
    pub fleets_asked: Mutex<Vec<String>>,
    pub sales_asked: Mutex<Vec<pb::SalesRequest>>,
    pub ledger_queries: Mutex<Vec<pb::QueryLedgerRequest>>,
}

impl Calls {
    /// How many MONEY-moving or state-writing calls have reached monetize, in
    /// total. The number a Viewer must leave at zero.
    pub fn mutations(&self) -> usize {
        self.starts.lock().unwrap().len()
            + self.abandons.lock().unwrap().len()
            + self.sets.lock().unwrap().len()
            + self.exports.lock().unwrap().len()
    }
}

pub struct Mock {
    pub calls: Calls,
    /// What `Events.Watch` hands out. A test pushes into it.
    pub events: tokio::sync::broadcast::Sender<pb::Event>,
    /// How many `Events.Watch` streams are open right now.
    pub watchers: AtomicUsize,
    /// A monetize that cannot stream: `Watch` is refused outright.
    pub refuse_watch: bool,
    /// Overrides for one test: a product's reachability, when set.
    pub reachable: Mutex<Option<(String, pb::Reachability)>>,
    /// RA3-1 / PROD-4: a `Usage.Fleet` that never answers (the cloud listing stuck), when set.
    pub fleet_stuck: std::sync::atomic::AtomicBool,
}

impl Mock {
    pub fn new() -> Arc<Mock> {
        Arc::new(Mock { calls: Calls::default(), events: tokio::sync::broadcast::channel(64).0, watchers: AtomicUsize::new(0), refuse_watch: false, reachable: Mutex::new(None), fleet_stuck: Default::default() })
    }

    pub fn refusing_events() -> Arc<Mock> {
        Arc::new(Mock { calls: Calls::default(), events: tokio::sync::broadcast::channel(64).0, watchers: AtomicUsize::new(0), refuse_watch: true, reachable: Mutex::new(None), fleet_stuck: Default::default() })
    }

    fn products(&self) -> Vec<pb::ProductInfo> {
        let mut ps = fixture::products();
        if let Some((id, r)) = self.reachable.lock().unwrap().clone() {
            if let Some(p) = ps.iter_mut().find(|p| p.id == id) {
                p.reachable = Some(r);
            }
        }
        ps
    }
}

#[derive(Clone)]
pub struct Svc(pub Arc<Mock>);

#[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: self.0.products(),
            vendors: fixture::VENDORS.iter().map(|s| (*s).to_owned()).collect(),
            clouds: fixture::CLOUDS.iter().map(|s| (*s).to_owned()).collect(),
            spend: fixture::spend(),
            prices: fixture::prices(),
            policy: Some(fixture::policy()),
            hosts: fixture::hosts(),
            default_cloud: fixture::CLOUDS[0].to_owned(),
            vendor_modes: vec![],
            estate_vendors: Default::default(),
        }))
    }
}

/// **The estate's board**, as monetize-server's renderer draws the gunnar.rs
/// shape (`fixture::BOARD_SVG`, the `estate_svg` example's output): front,
/// main and twin. gunnar's board carries the three; every other product here
/// declares none — the honest empty board, counted 0.
#[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;
        if !fixture::PRODUCT_IDS.contains(&product.as_str()) {
            return Err(Status::not_found(format!("product {product:?} is not served here")));
        }
        let machines = Some(if product == "gunnar" { 3 } else { 0 });
        Ok(Response::new(pb::DrawEstateResponse { svg: fixture::BOARD_SVG.to_owned(), machines }))
    }
}

#[tonic::async_trait]
impl Usage for Svc {
    async fn read(&self, r: TReq<pb::ReadUsageRequest>) -> Result<Response<pb::ReadUsageResponse>, Status> {
        let r = r.into_inner();
        Ok(Response::new(pb::ReadUsageResponse { usage: fixture::usages(&r.product).into_iter().find(|u| u.tenant == r.tenant) }))
    }
    async fn read_all(&self, r: TReq<pb::ReadAllUsageRequest>) -> Result<Response<pb::ReadAllUsageResponse>, Status> {
        Ok(Response::new(pb::ReadAllUsageResponse { usages: fixture::usages(&r.into_inner().product) }))
    }
    /// Not a silent empty series: a route added later against a mock that
    /// answered "no readings" would look green while returning nothing.
    async fn history(&self, _: TReq<pb::UsageHistoryRequest>) -> Result<Response<pb::UsageHistoryResponse>, Status> {
        Err(Status::unimplemented("not in the mock"))
    }
    /// Unimplemented for [`Self::history`]'s reason, and the reason bites
    /// harder here: an empty series is what the console renders as *this
    /// customer has gone quiet*, so a mock that answered "no days" would make
    /// the Activity panel look green while saying something false about every
    /// tenant in the fixture.
    async fn activity(&self, _: TReq<pb::ActivityRequest>) -> Result<Response<pb::ActivityResponse>, Status> {
        Err(Status::unimplemented("not in the mock"))
    }
    async fn fleet(&self, r: TReq<pb::FleetRequest>) -> Result<Response<pb::FleetResponse>, Status> {
        let product = r.into_inner().product;
        self.0.calls.fleets_asked.lock().unwrap().push(product.clone());
        if self.0.fleet_stuck.load(std::sync::atomic::Ordering::SeqCst) {
            tokio::time::sleep(std::time::Duration::from_secs(600)).await;
        }
        if !fixture::PRODUCT_IDS.contains(&product.as_str()) {
            return Err(Status::not_found(format!("product {product:?} is not served by monetize")));
        }
        Ok(Response::new(fixture::fleet(&product)))
    }
    async fn trend(&self, r: TReq<pb::TrendRequest>) -> Result<Response<pb::TrendResponse>, Status> {
        let asked = r.into_inner();
        self.0.calls.trends_asked.lock().unwrap().push(asked.clone());
        if asked.meter.is_empty() {
            return Err(Status::invalid_argument("a rate needs a meter"));
        }
        Ok(Response::new(fixture::trend(&asked.product, &asked.tenant)))
    }
}

#[tonic::async_trait]
impl Entitlement for Svc {
    async fn get(&self, r: TReq<pb::GetEntitlementRequest>) -> Result<Response<pb::GetEntitlementResponse>, Status> {
        let r = r.into_inner();
        Ok(Response::new(pb::GetEntitlementResponse { fact: fixture::facts(&r.product).into_iter().find(|f| f.tenant == r.tenant).map(sign), pending_push: None }))
    }
    async fn list(&self, r: TReq<pb::ListEntitlementsRequest>) -> Result<Response<pb::ListEntitlementsResponse>, Status> {
        Ok(Response::new(pb::ListEntitlementsResponse { facts: fixture::facts(&r.into_inner().product).into_iter().map(sign).collect() }))
    }
    async fn set(&self, r: TReq<pb::SetEntitlementRequest>) -> Result<Response<pb::SetEntitlementResponse>, Status> {
        let r = r.into_inner();
        let f = sign(r.fact.clone().ok_or_else(|| Status::invalid_argument("no fact"))?);
        self.0.calls.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() }))
    }
    async fn held(&self, r: TReq<pb::HeldRequest>) -> Result<Response<pb::HeldResponse>, Status> {
        let r = r.into_inner();
        Ok(Response::new(pb::HeldResponse { holdings: fixture::holdings(&r.product, &r.tenant) }))
    }
}

#[tonic::async_trait]
impl Purchase for Svc {
    /// The quote the real server would give: the delta, the list's price, the
    /// provider's cost, and the sealed refusal for alice's 100 GiB.
    async fn quote(&self, r: TReq<pb::QuoteRequest>) -> Result<Response<pb::QuoteResponse>, Status> {
        let r = r.into_inner();
        if !fixture::PRODUCT_IDS.contains(&r.product.as_str()) {
            return Err(Status::not_found(format!("product {:?} is not served here", r.product)));
        }
        let q = fixture::quote(&r.product, &r.tenant, &r.caps).map_err(Status::failed_precondition)?;
        self.0.calls.quotes.lock().unwrap().push(r);
        Ok(Response::new(q))
    }
    /// The same quote, then the order — refused where the quote said no, in
    /// the same words, with nothing recorded; PENDING at mockpay otherwise,
    /// or PAID at once when the price is 0 (the free product).
    async fn start(&self, r: TReq<pb::StartPurchaseRequest>) -> Result<Response<pb::StartPurchaseResponse>, Status> {
        let r = r.into_inner();
        if !fixture::PRODUCT_IDS.contains(&r.product.as_str()) {
            return Err(Status::not_found(format!("product {:?} is not served here", r.product)));
        }
        let q = fixture::quote(&r.product, &r.tenant, &r.caps).map_err(Status::failed_precondition)?;
        if !q.can_serve {
            return Err(Status::failed_precondition(q.reason));
        }
        let free = q.price.as_ref().map(|m| m.minor == 0).unwrap_or(true);
        if !free && !r.vendor.is_empty() && !fixture::VENDORS.contains(&r.vendor.as_str()) {
            return Err(Status::not_found(format!("vendor {} is not linked into this server", r.vendor)));
        }
        let reference = q.reference.clone();
        let fact = free.then(|| {
            sign(pb::EntitlementFact {
                tenant: r.tenant.clone(),
                plan: reference.clone(),
                state: pb::State::Paid as i32,
                paid_until_unix_ms: Some(fixture::PAID_UNTIL),
                caps: q.caps.clone(),
                source: format!("payment:none:{reference}"),
                signature: vec![],
                issued_unix_ms: None,
                issued_signature: vec![],
            })
        });
        self.0.calls.starts.lock().unwrap().push(r);
        Ok(Response::new(pb::StartPurchaseResponse {
            reference,
            state: if free { pb::PurchaseState::Paid as i32 } else { pb::PurchaseState::Pending as i32 },
            handle_vendor: if free { "none".into() } else { "mockpay".into() },
            handle_id: if free { String::new() } else { "h-1".into() },
            redirect_url: if free { String::new() } else { "https://pay.example/h-1".into() },
            reason: if free { String::new() } else { "the customer has not authorized".into() },
            fact,
            // Only a granted purchase carries the customer's sentence. The
            // free path here is the one that lands PAID; the vendor path is
            // still PENDING and must say nothing about room it has not given.
            // A stand-in, not the real constant: this crate does not depend on
            // `monetize`, and what the sentence SAYS is asserted in
            // `monetize-server/tests/granted_words.rs`. What matters here is
            // only that a granted purchase carries one and a pending one does
            // not.
            words: if free { "the room is yours now".into() } else { String::new() },
            users: None,
        }))
    }
    /// **The OPERATOR console has no shop window**, so this double answers an
    /// empty list — which is also the real server's answer on a deployment that
    /// declared no bundles, and the state the customer console must fall back
    /// from to its typed order form. `Purchase.Bundles` is proven over the real
    /// service in `monetize-server/tests/bundles.rs`; a second fixture here
    /// would only be something to drift.
    async fn bundles(&self, _r: TReq<pb::ListBundlesRequest>) -> Result<Response<pb::ListBundlesResponse>, Status> {
        Ok(Response::new(pb::ListBundlesResponse { bundles: vec![], reading: false, plan: 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"));
        }
        let reference = r.reference.clone();
        self.0.calls.abandons.lock().unwrap().push(r);
        Ok(Response::new(pb::AbandonPurchaseResponse { reference, released: vec![] }))
    }
    /// **The operator console has no tenant history pane**, so this double says
    /// so rather than inventing a fixture. `Purchase.History` is the CUSTOMER
    /// pane's call; it is proven over the real service in
    /// `monetize-server/tests/tenant_history.rs`, and a second fold here would
    /// only be something to drift.
    async fn history(&self, _r: TReq<pb::PurchaseHistoryRequest>) -> Result<Response<pb::PurchaseHistoryResponse>, Status> {
        Err(Status::unimplemented("this mock 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;
        let rows: Vec<pb::LedgerRow> = fixture::rows().into_iter().filter(|x| x.reference == reference).collect();
        let open = fixture::unfinished().contains(&reference);
        let state = if rows.is_empty() {
            pb::PurchaseState::Unknown
        } else if open {
            pb::PurchaseState::Pending
        } else if rows.iter().any(|x| x.kind == "Declined") {
            pb::PurchaseState::Declined
        } else if rows.iter().any(|x| x.kind == "Released") {
            pb::PurchaseState::Released
        } else {
            pb::PurchaseState::Paid
        };
        Ok(Response::new(pb::PurchaseStatusResponse {
            reference: reference.clone(),
            state: state as i32,
            reason: String::new(),
            fact: None,
            rows,
            redirect_url: if open { format!("https://mockpay.example/checkout/{}", reference.replace('/', "-")) } else { String::new() },
            ..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 mock 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, Ordering::SeqCst);
        let opened = tokio_stream::once(Ok(pb::Event::new(0, 1, pb::event::Payload::StreamOpened(pb::StreamOpened { epoch: "uitest-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 r = r.into_inner();
        self.0.calls.ledger_queries.lock().unwrap().push(r.clone());
        let all = fixture::rows();
        let (rows, references) = match r.by.ok_or_else(|| Status::invalid_argument("no query"))? {
            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(_) => {
                let open = fixture::unfinished();
                (all.into_iter().filter(|r| open.contains(&r.reference)).collect(), open)
            }
            By::Recent(limit) => {
                let mut rows = all;
                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 }))
    }
    async fn sales(&self, r: TReq<pb::SalesRequest>) -> Result<Response<pb::SalesResponse>, Status> {
        let r = r.into_inner();
        self.0.calls.sales_asked.lock().unwrap().push(r.clone());
        let mut sales = fixture::sales(&r.product);
        if r.limit > 0 {
            sales.truncate(r.limit as usize);
        }
        Ok(Response::new(pb::SalesResponse { sales }))
    }
    async fn export(&self, _: TReq<pb::ExportBackupRequest>) -> Result<Response<pb::ExportBackupResponse>, Status> {
        self.0.calls.exports.lock().unwrap().push(());
        Ok(Response::new(pb::ExportBackupResponse { dir: "/var/lib/monetize/backup".into(), ledger_rows: fixture::rows().len() as u64, usage_readings: 11, signing_public_key: signing_key().verifying_key().to_bytes().iter().map(|b| format!("{b:02x}")).collect() }))
    }
}

/// The bearer gate the real monetize-server has: the BFF must present the token.
fn bearer_gate(expected: String) -> impl Fn(TReq<()>) -> Result<TReq<()>, Status> + Clone {
    move |req: TReq<()>| match req.metadata().get("authorization").and_then(|v| v.to_str().ok()) {
        Some(v) if v == format!("Bearer {expected}") => Ok(req),
        _ => Err(Status::unauthenticated("bearer missing or wrong")),
    }
}

/// Bind and serve the mock on `bind` (`127.0.0.1:0` picks a port), on the
/// CURRENT tokio runtime. Returns the bound address once the listener exists,
/// so a caller can connect without a race.
/// [`serve`] behind TLS with `cert_pem`/`key_pem` — an estate's
/// `monetize.<apex>` as the console meets it (the front terminates TLS for
/// monetize's control API). The pinned-control tests dial it.
pub async fn serve_tls(mock: Arc<Mock>, bind: &str, bearer: &str, cert_pem: &str, key_pem: &str) -> std::io::Result<std::net::SocketAddr> {
    let listener = tokio::net::TcpListener::bind(bind).await?;
    let addr = listener.local_addr()?;
    let s = || Svc(mock.clone());
    let gate = bearer_gate(bearer.to_owned());
    let tls = tonic::transport::ServerTlsConfig::new().identity(tonic::transport::Identity::from_pem(cert_pem, key_pem));
    let server = tonic::transport::Server::builder().tls_config(tls).map_err(std::io::Error::other)?;
    let mut server = server;
    tokio::spawn(
        server
            .add_service(ProductsServer::with_interceptor(s(), gate.clone()))
            .add_service(EntitlementServer::with_interceptor(s(), gate))
            .serve_with_incoming(tonic::transport::server::TcpIncoming::from(listener)),
    );
    Ok(addr)
}

pub async fn serve(mock: Arc<Mock>, bind: &str, bearer: &str) -> std::io::Result<std::net::SocketAddr> {
    let listener = tokio::net::TcpListener::bind(bind).await?;
    let addr = listener.local_addr()?;
    let s = || Svc(mock.clone());
    let gate = bearer_gate(bearer.to_owned());
    tokio::spawn(
        tonic::transport::Server::builder()
            .add_service(ProductsServer::with_interceptor(s(), gate.clone()))
            .add_service(UsageServer::with_interceptor(s(), gate.clone()))
            .add_service(EntitlementServer::with_interceptor(s(), gate.clone()))
            .add_service(LedgerServer::with_interceptor(s(), gate.clone()))
            .add_service(PurchaseServer::with_interceptor(s(), gate.clone()))
            .add_service(EventsServer::with_interceptor(s(), gate.clone()))
            .add_service(EstateAdminServer::with_interceptor(s(), gate.clone()))
            .add_service(EstatesServer::with_interceptor(s(), gate.clone()))
            .add_service(GuardianServer::with_interceptor(s(), gate.clone()))
            .add_service(TaxServer::with_interceptor(s(), gate))
            .serve_with_incoming(tonic::transport::server::TcpIncoming::from(listener)),
    );
    Ok(addr)
}

/// **The tax return, from the fixture.** It computes nothing: the arithmetic is
/// tested where it lives (`monetize::tax` and the jurisdiction impl). What this
/// exists for is the WIRING — that `/api/tax/periods` and `/api/tax/report`
/// reach the right RPC, that an absent amount survives every hop as absent, and
/// that a period the deployment does not file is refused rather than answered.
#[tonic::async_trait]
impl Tax for Svc {
    async fn periods(&self, r: TReq<pb::TaxPeriodsRequest>) -> Result<Response<pb::TaxPeriodsResponse>, Status> {
        let r = r.into_inner();
        if r.year != fixture::TAX_YEAR {
            return Err(Status::invalid_argument(format!("the fixture files for {} only", fixture::TAX_YEAR)));
        }
        Ok(Response::new(pb::TaxPeriodsResponse {
            jurisdiction: fixture::TAX_JURISDICTION.into(),
            periods: fixture::tax_periods(),
            turnover: Some(pb::Money { minor: fixture::TAX_TURNOVER_MINOR, currency: pb::Currency::Sek as i32 }),
            turnover_year: fixture::TAX_TURNOVER_YEAR,
        }))
    }

    async fn report(&self, r: TReq<pb::TaxReportRequest>) -> Result<Response<pb::TaxReportResponse>, Status> {
        let r = r.into_inner();
        let period = fixture::tax_periods()
            .into_iter()
            .find(|p| p.label == r.period)
            .ok_or_else(|| Status::not_found(format!("{:?} is not a period the fixture files for", r.period)))?;
        Ok(Response::new(pb::TaxReportResponse {
            jurisdiction: fixture::TAX_JURISDICTION.into(),
            period: Some(period),
            lines: fixture::tax_lines(),
            notes: fixture::tax_notes(),
            csv: fixture::tax_csv(),
        }))
    }
}

/// **The estates that exist** (OVW-4): the ONE monetize's Overview of the fixture's
/// [`fixture::ESTATES`], each installed and known to the fleet. The slot makes nothing here.
#[tonic::async_trait]
impl Estates for Svc {
    async fn overview(&self, _: TReq<pb::EstatesOverviewRequest>) -> Result<Response<pb::EstatesOverviewResponse>, Status> {
        let estates = fixture::ESTATES
            .iter()
            .map(|(name, product, domain)| pb::EstateStanding { name: (*name).into(), product: (*product).into(), standing: "installed".into(), words: "installed: its machines report in".into(), in_fleet: true, domain: (*domain).into() })
            .collect();
        Ok(Response::new(pb::EstatesOverviewResponse { estates, ended: Vec::new(), slot: Some(pb::NewEstateSlot::default()) }))
    }
    async fn new_estate(&self, _: TReq<pb::NewEstateRequest>) -> Result<Response<pb::NewEstateResponse>, Status> {
        Err(Status::unimplemented("the mock makes no estate"))
    }
}

/// **The fleet** (OVW-4): each of [`fixture::ESTATES`] with its main reporting in now and no
/// case; a press or an auto-fix change is refused (the mock heals nothing).
#[tonic::async_trait]
impl Guardian for Svc {
    async fn cases(&self, r: TReq<pb::GuardianCasesRequest>) -> Result<Response<pb::GuardianCasesResponse>, Status> {
        let only = r.into_inner().instance;
        let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map(|d| d.as_millis() as u64).unwrap_or(fixture::T0);
        let estates = fixture::ESTATES
            .iter()
            .filter(|(name, ..)| only.is_empty() || only == *name)
            .map(|(name, product, domain)| pb::GuardianEstate {
                instance: (*name).into(),
                product: (*product).into(),
                boxes: vec![pb::GuardianBox { role: "main".into(), heard_unix_ms: now, silent: false, health: [("serves_at".to_string(), (*domain).to_string())].into_iter().collect(), laid_ipv4: String::new() }],
                ..Default::default()
            })
            .collect();
        Ok(Response::new(pb::GuardianCasesResponse { estates }))
    }
    async fn press(&self, _: TReq<pb::GuardianPressRequest>) -> Result<Response<pb::GuardianCase>, Status> {
        Err(Status::unimplemented("the mock heals nothing"))
    }
    async fn set_auto_fix(&self, _: TReq<pb::GuardianAutoFixRequest>) -> Result<Response<pb::GuardianAutoFixResponse>, Status> {
        Err(Status::unimplemented("the mock heals nothing"))
    }
    async fn set_hold(&self, _: TReq<pb::GuardianHoldRequest>) -> Result<Response<pb::GuardianHoldResponse>, Status> {
        Err(Status::unimplemented("not in this mock"))
    }
}