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

nordisk/monetize

crates/monetize-server/tests/accounts.rs

//! **`Accounts.List` / `Accounts.Rows`: the accounts ONE monetize holds.**
//!
//! An account is `(product, tenant)`. Its rows are the subscription (the signed
//! fact), every purchase attempt folded (`Ledger.Sales`' `Sale`), and the
//! private instances it owns. Each account names the vendor instance its
//! product bills through (`[products.<id>] vendor`). The console renders this
//! and decides nothing, so the server answers it read-only, behind the same
//! bearer as `Ledger`.

use std::sync::Arc;

use monetize::testing::{Answer, MockCloud, NOW};
use monetize_product::{EntitlementFact, State, TenantId};
use monetize_proto::accounts_client::AccountsClient;
use monetize_proto::purchase_client::PurchaseClient;
use monetize_proto::{self as pb, PurchaseState};
use monetize_server::registry::Registry;
use monetize_server::state::AppState;
use monetize_server::testing::estate::FakeEstate;
use monetize_server::testing::products::TestProduct;
use monetize_server::testing::{CallbackPay, TestServer};
use tonic::metadata::MetadataValue;
use tonic::transport::Channel;
use tonic::{Code, Request};

const BEARER: &str = "test-bearer-token-of-sufficient-length";
const GIB: u64 = 1 << 30;
const DAY: u64 = 86_400_000;

struct Rig {
    state: Arc<AppState>,
    _server: TestServer,
    channel: Channel,
}

impl Rig {
    async fn up() -> Rig {
        let pay = CallbackPay::new("mockpay-shared-secret");
        let cloud = MockCloud::new();
        cloud.answer_all(Answer::OnHand);
        let mut state = AppState::in_memory(Registry::new().pulled_product(TestProduct::gunnar(), "gunnar.rs").sells("gunnar", pay).cloud(cloud));
        FakeEstate::declare(&state, "gunnar", "gunnar.rs").report(|r| {
            r.servable = Some(monetize_product::Servable::Measured { servable_bytes: 300 * GIB, total_bytes: 400 * GIB, growth: monetize_product::Growth::Unknown("a test estate".into()), ceilings: Default::default() });
        });
        state.checkout_urls.get_mut().unwrap().insert("mockpay".into(), "https://pay.example/{handle}".into());
        let state = Arc::new(state);
        let server = TestServer::spawn(state.clone(), BEARER).await;
        let channel = Channel::from_shared(server.endpoint()).unwrap().connect().await.unwrap();
        Rig { state, _server: server, channel }
    }

    fn authed<T>(&self, req: T) -> Request<T> {
        let mut r = Request::new(req);
        r.metadata_mut().insert("authorization", MetadataValue::try_from(format!("Bearer {BEARER}")).unwrap());
        r
    }

    fn accounts(&self) -> AccountsClient<Channel> {
        AccountsClient::new(self.channel.clone())
    }

    fn holds(&self, tenant: &str, state: State, paid_until: u64) {
        let fact = self.state.signer_for("gunnar").sign_fact(EntitlementFact {
            tenant: TenantId(tenant.into()),
            plan: "gunnar-standard".into(),
            state,
            paid_until_unix_ms: Some(paid_until),
            caps: [("pack_bytes".to_string(), 10 * GIB)].into(),
            source: format!("payment:mockpay:gunnar/{tenant}/x"),
            signature: Vec::new(),
            issued_unix_ms: None,
            issued_signature: Vec::new(),
        });
        self.state.ledger.set_entitlement("gunnar", &fact).unwrap();
    }

    async fn buys(&self, tenant: &str, gib: u64) -> String {
        let started = PurchaseClient::new(self.channel.clone())
            .start(self.authed(pb::StartPurchaseRequest {
                product: "gunnar".into(),
                tenant: tenant.into(),
                caps: [("pack_bytes".to_string(), gib * GIB)].into(),
                vendor: "mockpay".into(),
                starts_unix_ms: NOW,
                market: pb::Market::Se as i32,
                ..Default::default()
            }))
            .await
            .expect("the order goes through")
            .into_inner();
        assert_eq!(started.state(), PurchaseState::Pending, "{started:?}");
        started.reference
    }

    async fn list(&self, product: &str) -> Result<pb::ListAccountsResponse, tonic::Status> {
        self.accounts().list(self.authed(pb::ListAccountsRequest { product: product.into(), limit: 0 })).await.map(|r| r.into_inner())
    }

    async fn rows(&self, product: &str, tenant: &str) -> Result<pb::AccountRowsResponse, tonic::Status> {
        self.accounts().rows(self.authed(pb::AccountRowsRequest { product: product.into(), tenant: tenant.into() })).await.map(|r| r.into_inner())
    }
}

/// Every account, one line each, with the vendor instance its product bills
/// through and the product → vendor map; an account that only holds a fact is
/// an account too.
#[tokio::test]
async fn list_answers_every_account_with_its_vendor_instance() {
    let rig = Rig::up().await;
    rig.holds("acme", State::Paid, NOW + 30 * DAY);
    rig.holds("bob", State::Grace, NOW + DAY);
    let reference = rig.buys("acme", 20).await;

    let got = rig.list("").await.expect("Accounts.List answers");
    assert_eq!(got.vendors.get("gunnar").map(String::as_str), Some("mockpay"), "{:?}", got.vendors);
    let keys: Vec<(String, String)> = got.accounts.iter().map(|a| (a.product.clone(), a.tenant.clone())).collect();
    assert_eq!(keys.len(), 2, "{keys:?}");
    assert!(keys.contains(&("gunnar".into(), "acme".into())) && keys.contains(&("gunnar".into(), "bob".into())), "{keys:?}");

    let acme = got.accounts.iter().find(|a| a.tenant == "acme").unwrap();
    assert_eq!(acme.vendor, "mockpay");
    assert_eq!(acme.state(), pb::State::Paid);
    assert_eq!(acme.paid_until_unix_ms, Some(NOW + 30 * DAY));
    assert_eq!(acme.rows, 2, "the subscription and the purchase {reference}");
    assert_eq!(acme.instances, 0);
    assert!(acme.needs_you, "an open attempt is waiting");
    let bob = got.accounts.iter().find(|a| a.tenant == "bob").unwrap();
    assert_eq!((bob.state(), bob.rows, bob.needs_you), (pb::State::Grace, 1, false));
    assert_eq!(got.accounts[0].tenant, "acme", "newest activity first");

    assert_eq!(rig.list("gunnar").await.unwrap().accounts.len(), 2);
    assert_eq!(rig.list("nope").await.unwrap_err().code(), Code::NotFound, "a product not served here is named");
}

/// One account's rows: the subscription first, then the purchases newest
/// first, each naming the vendor instance it was billed through.
#[tokio::test]
async fn rows_answer_the_subscription_first_then_the_purchases() {
    let rig = Rig::up().await;
    rig.holds("acme", State::Paid, NOW + 30 * DAY);
    let reference = rig.buys("acme", 20).await;

    let got = rig.rows("gunnar", "acme").await.expect("Accounts.Rows answers");
    let account = got.account.expect("the account's own line");
    assert_eq!((account.product.as_str(), account.tenant.as_str(), account.rows), ("gunnar", "acme", 2));
    let kinds: Vec<&str> = got.rows.iter().map(|r| r.kind.as_str()).collect();
    assert_eq!(kinds, vec!["subscription", "purchase"]);

    let sub = &got.rows[0];
    assert_eq!(sub.id, "");
    assert_eq!(sub.vendor, "mockpay");
    assert_eq!(sub.fact.as_ref().map(|f| f.tenant.as_str()), Some("acme"));
    assert!(sub.words.contains("Paid"), "{}", sub.words);

    let buy = &got.rows[1];
    assert_eq!(buy.id, reference);
    assert_eq!(buy.vendor, "mockpay");
    let sale = buy.sale.as_ref().expect("the attempt, folded");
    assert_eq!((sale.reference.as_str(), sale.tenant.as_str()), (reference.as_str(), "acme"));
    assert!(buy.needs_you && sale.needs_you, "an open attempt");
}

/// Rows refuses an empty key and names an account it does not hold.
#[tokio::test]
async fn rows_refuse_an_empty_key_and_name_an_unknown_account() {
    let rig = Rig::up().await;
    rig.holds("acme", State::Paid, NOW + 30 * DAY);
    assert_eq!(rig.rows("", "acme").await.unwrap_err().code(), Code::InvalidArgument);
    assert_eq!(rig.rows("gunnar", "").await.unwrap_err().code(), Code::InvalidArgument);
    let err = rig.rows("gunnar", "nobody").await.unwrap_err();
    assert_eq!(err.code(), Code::NotFound);
    assert!(err.message().contains("nobody"), "{}", err.message());
}

/// Read-only, but not public: the same bearer as `Ledger`.
#[tokio::test]
async fn accounts_need_the_bearer() {
    let rig = Rig::up().await;
    let err = rig.accounts().list(Request::new(pb::ListAccountsRequest::default())).await.unwrap_err();
    assert_eq!(err.code(), Code::Unauthenticated);
}

/// ★ **Every product's private instances are on the Accounts page** (T-OWNERFLOW, the owner's
/// walk on both sites, 2026-10-26): a ONE box that sells Gunnar AND Holger books a Holger
/// instance on the ledger under `holger` (the run's own product), and the page read only the
/// box's own estate product, so a Holger buyer's instance never showed on their account. Each
/// instance is a row on its own product's account, counted there, and on no other product's.
#[tokio::test]
async fn every_products_instances_are_rows_on_their_own_accounts() {
    let reg = Registry::new().pulled_product(TestProduct::gunnar(), "gunnar.rs").pulled_product(TestProduct::holger(), "holger.rs");
    let state = Arc::new(AppState::in_memory(reg.sells("gunnar", CallbackPay::new("mockpay-shared-secret"))));
    let server = TestServer::spawn(state.clone(), BEARER).await;
    let channel = Channel::from_shared(server.endpoint()).unwrap().connect().await.unwrap();
    let rig = Rig { state, _server: server, channel };
    for (product, company) in [("gunnar", "rickardgit"), ("holger", "rickardtest")] {
        let step = monetize::Step::Instance {
            reference: monetize::ledger::instance_reference(product, "alica", NOW, company),
            product: product.into(),
            tenant: "alica".into(),
            company: company.into(),
            host: format!("{company}.{product}.mock.invalid"),
            phase: "captured".into(),
            monthly: monetize_pay::Money { minor: 9_620, currency: monetize_pay::Currency::Eur },
            paid_until_unix_ms: NOW + 180 * DAY,
            vendor: format!("stripe/{product}"),
            operator_test: false,
            note: String::new(),
        };
        rig.state.ledger.append(NOW, step).expect("the ledger books the instance");
    }
    for (product, company, other) in [("gunnar", "rickardgit", "rickardtest"), ("holger", "rickardtest", "rickardgit")] {
        let listed = rig.list(product).await.expect("Accounts.List");
        let acct = listed.accounts.iter().find(|a| a.tenant == "alica").unwrap_or_else(|| panic!("alica is not on {product}'s Accounts page: {listed:?}"));
        assert_eq!(acct.instances, 1, "{product}: alica's own instance: {acct:?}");
        let rows = rig.rows(product, "alica").await.expect("Accounts.Rows").rows;
        assert!(rows.iter().any(|r| r.kind == "instance" && r.id.contains(company)), "{product}: {company} is a row: {rows:?}");
        assert!(!rows.iter().any(|r| r.id.contains(other)), "{product}: the other product's {other} is not: {rows:?}");
    }
}