crates/gunnar-server/tests/product_report.rs
//! **`Report.product`** (5.4, ONE monetize, G1 pull): monetize never dials an estate, so
//! what it used to ask a main for — its tenants and who each is, their usage, what the box
//! can still serve, its paid door, the room a raise would find, what each tenant is
//! promised, and every name the box holds — leaves the main in its own `Fleet.Report`,
//! as JSON text of `monetize_product::pull::ProductReport`. Read in-process from the
//! box's own control plane and entitlement slot. A box that does not serve sends none.
#![cfg(all(feature = "replication", feature = "metered"))]
use std::collections::BTreeMap;
use std::sync::Arc;
use ed25519_dalek::{Signer, SigningKey};
use gunnar_common::monetize_key::MonetizePubkey;
use gunnar_core::{Principal, PrincipalKind};
use gunnar_server::control_plane::CoreControlPlane;
use gunnar_server::entitlement::{EntitlementSlot, MeteredConfig};
use gunnar_server::repair::Entitlements;
use gunnar_server::stores::ServerStores;
use gunnar_ssh::{DiskFloor, DiskHeadroomGate};
use monetize_embed::{EntitlementFact, State, TenantId};
use serde_json::Value;
const T: u64 = 1_789_000_000_000;
const GIB: u64 = 1 << 30;
struct Rig {
_dir: tempfile::TempDir,
stores: Arc<ServerStores>,
slot: Arc<EntitlementSlot>,
}
impl Rig {
/// A main that serves: alice (a person), team (no principal), ci (a machine's).
fn serving() -> Rig {
let r = Rig::fresh();
let auth = r.stores.auth();
auth.add_principal(Principal {
id: "alice".into(),
name: "alice".into(),
kind: PrincipalKind::User,
created: 0,
})
.expect("alice");
auth.add_principal(Principal {
id: "ci".into(),
name: "ci".into(),
kind: PrincipalKind::Machine,
created: 0,
})
.expect("ci");
for ns in ["alice", "team", "ci"] {
r.stores.catalog().create_namespace(ns).expect("namespace");
}
let key = SigningKey::from_bytes(&[7u8; 32]);
let mut f = EntitlementFact {
tenant: TenantId("alice".into()),
plan: "10gb".into(),
state: State::Paid,
paid_until_unix_ms: None,
caps: BTreeMap::from([("pack_bytes".to_owned(), 5 * GIB)]),
source: "payment:alice:1".into(),
signature: vec![],
issued_unix_ms: Some(T),
issued_signature: vec![],
};
f.signature = key
.sign(&monetize_embed::signing::fact_message(&f))
.to_bytes()
.to_vec();
f.issued_signature = key
.sign(&monetize_embed::signing::fact_message_issued(&f))
.to_bytes()
.to_vec();
r.slot.push(&f).expect("alice's fact");
r
}
fn fresh() -> Rig {
let dir = tempfile::tempdir().expect("tempdir");
let stores = Arc::new(ServerStores::open(dir.path()).expect("stores"));
let key = SigningKey::from_bytes(&[7u8; 32]);
let pubkey = dir.path().join("monetize.pub");
std::fs::write(&pubkey, key.verifying_key().to_bytes()).expect("key");
let config = MeteredConfig::load(
&MonetizePubkey::Path(pubkey),
Some("https://gunnar.rs/billing/{tenant}"),
)
.expect("config");
let slot = EntitlementSlot::open(Arc::clone(&stores), &config).expect("slot");
Rig {
_dir: dir,
stores,
slot,
}
}
fn box_of(&self, plane: CoreControlPlane) -> Entitlements {
Entitlements::of(Arc::clone(&self.stores))
.with_slot(Some(Arc::clone(&self.slot)))
.with_control(Arc::new(plane))
}
fn report(&self, plane: CoreControlPlane) -> Value {
let text = self
.box_of(plane)
.product(T)
.expect("a main that serves reports its product");
serde_json::from_str(&text).expect("JSON")
}
}
#[test]
fn a_serving_main_reports_its_tenants_names_usage_promises_and_paid_door() {
let r = Rig::serving();
let p = r.report(CoreControlPlane::new(Arc::clone(&r.stores)));
let tenants = p["tenants"].as_array().expect("tenants");
let alice = tenants
.iter()
.find(|t| t["tenant"] == "alice")
.expect("alice is a tenant");
assert_eq!(
(alice["person"].as_bool(), alice["seller"].as_bool()),
(Some(true), Some(false)),
"{alice}"
);
let team = tenants
.iter()
.find(|t| t["tenant"] == "team")
.expect("team is a tenant");
assert_eq!(team["person"], false, "{team}");
assert!(
!tenants.iter().any(|t| t["tenant"] == "ci"),
"a machine's namespace is no tenant: {tenants:?}"
);
let names: Vec<&str> = p["names"]
.as_array()
.expect("names")
.iter()
.filter_map(Value::as_str)
.collect();
for n in ["alice", "ci", "team"] {
assert!(names.contains(&n), "{n} is a name the box holds: {names:?}");
}
assert!(
p["usage"]["alice"]["meters"]["pack_bytes"].is_u64(),
"alice's usage: {}",
p["usage"]
);
assert_eq!(p["promised"]["alice"]["pack_bytes"], 5 * GIB);
assert!(
p["metered"]["Enforcing"].is_string(),
"a metered build with a key enforces: {}",
p["metered"]
);
assert_eq!(p["measured_unix_ms"], T);
// No disk-headroom gate: the box has not measured what it can serve, and that is
// not a number. Nothing is sold on it: every byte meter has no room, and no iron.
assert!(p["servable"]["Unmeasured"].is_string(), "{}", p["servable"]);
for m in ["pack_bytes", "lfs_bytes", "cache_bytes"] {
assert_eq!(p["room"][m], 0, "{m}: {}", p["room"]);
}
assert!(p["with_iron"].is_null(), "{}", p["with_iron"]);
}
#[test]
fn a_measured_main_reports_what_it_can_serve_as_the_room_a_raise_finds() {
let r = Rig::serving();
let gate = Arc::new(DiskHeadroomGate::new(
r.stores.root(),
DiskFloor {
percent: 0,
bytes: 1,
},
));
let p = r.report(CoreControlPlane::new(Arc::clone(&r.stores)).with_headroom(gate));
let measured = &p["servable"]["Measured"];
let servable = measured["servable_bytes"].as_u64().expect("servable_bytes");
assert!(
servable > 0 && measured["total_bytes"].as_u64() > Some(servable),
"{measured}"
);
let growth = measured["growth"].as_object().expect("growth");
assert!(
growth.len() == 1
&& [
"Sealed",
"OperatorOnly",
"Unknown",
"AtRestart",
"AtRuntime"
]
.iter()
.any(|g| growth.contains_key(*g)),
"{growth:?}"
);
for m in ["pack_bytes", "lfs_bytes", "cache_bytes"] {
assert_eq!(p["room"][m], servable, "{m}: {}", p["room"]);
}
}
/// ★ UW-10: **each tenant's activity rides the report** (the last 30 days, the plane's own
/// `Usage.Activity`, meter for meter as the plugin names them, with how far back the box
/// remembers), so monetize's tenant page is read without dialling the box. A machine's
/// namespace is no tenant and carries none.
#[test]
fn a_serving_main_reports_each_tenants_activity() {
let r = Rig::serving();
r.stores.flow().record(
"alice",
gunnar_core::now_ms(),
&gunnar_core::Transfer {
bytes_in: 100,
pushed: true,
..Default::default()
},
);
let p = r.report(CoreControlPlane::new(Arc::clone(&r.stores)));
let alice = &p["activity"]["alice"];
let days = alice["days"].as_array().expect("alice's days");
assert_eq!(
days.iter()
.map(|d| d["meters"]["pushes"].as_u64().unwrap_or(0))
.sum::<u64>(),
1,
"{alice}"
);
assert!(alice["retained_days"].as_u64() > Some(0), "{alice}");
assert!(
p["activity"]["team"]["days"]
.as_array()
.is_some_and(|d| d.is_empty()),
"no traffic is an empty series: {}",
p["activity"]
);
assert!(p["activity"].get("ci").is_none(), "{}", p["activity"]);
}
#[test]
fn a_box_that_does_not_serve_sends_no_product_part() {
let fresh = Rig::fresh();
assert_eq!(
fresh
.box_of(CoreControlPlane::new(Arc::clone(&fresh.stores)))
.product(T),
None,
"a re-laid main nobody restored"
);
let r = Rig::serving();
assert_eq!(
Entitlements::of(Arc::clone(&r.stores)).product(T),
None,
"no control plane, nothing to read"
);
}