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