use std::collections::BTreeMap;
pub mod factory;
pub use factory::{body_secret_placeholder, GuestSpec, HttpAuth, ProductBuild, ProductFactory, PULLED_NEVER_DIALLED};
pub mod flow;
pub mod guardian;
pub mod pull;
#[derive(Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Debug, serde::Serialize, serde::Deserialize)]
#[derive(Default)]
pub struct TenantId(pub String);
#[derive(Clone, Copy, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
pub enum Enforcement {
Enforced,
Measured,
Provisioned,
}
impl Enforcement {
pub fn describe(self) -> &'static str {
match self {
Enforcement::Enforced => "the product refuses when over",
Enforcement::Measured => "counted and reported; nothing refuses",
Enforcement::Provisioned => "provisioned at the cloud provider; not policed by the product",
}
}
pub fn is_a_wall(self) -> bool {
matches!(self, Enforcement::Enforced)
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
pub enum Backing {
DiskBytes,
CpuMillicores,
RamBytes,
Units,
}
impl Backing {
pub fn unit(self) -> &'static str {
match self {
Backing::DiskBytes => "disk_gib_month",
Backing::CpuMillicores => "cpu_core_month",
Backing::RamBytes => "ram_gib_month",
Backing::Units => "unit_month",
}
}
pub const ALL: [Backing; 4] = [Backing::DiskBytes, Backing::CpuMillicores, Backing::RamBytes, Backing::Units];
pub fn from_unit(unit: &str) -> Option<Backing> {
Backing::ALL.into_iter().find(|b| b.unit() == unit)
}
pub fn price_month(self, delta: u64, minor_per_unit_month: u64) -> u64 {
const GIB: u64 = 1 << 30;
match self {
Backing::DiskBytes | Backing::RamBytes => delta.div_ceil(GIB).saturating_mul(minor_per_unit_month),
Backing::CpuMillicores => u64::try_from(u128::from(delta) * u128::from(minor_per_unit_month) / 1000).unwrap_or(u64::MAX),
Backing::Units => delta.saturating_mul(minor_per_unit_month),
}
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
pub enum Scope {
Tenant,
Product,
}
impl Scope {
pub fn sums_across_tenants(self) -> bool {
matches!(self, Scope::Tenant)
}
pub fn name(self) -> &'static str {
match self {
Scope::Tenant => "tenant",
Scope::Product => "product",
}
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct Meter {
pub name: &'static str,
pub meaning: &'static str,
pub enforcement: Enforcement,
pub reports_usage: bool,
pub backing: Option<Backing>,
pub scope: Scope,
}
impl Meter {
pub const fn new(name: &'static str, meaning: &'static str, enforcement: Enforcement) -> Meter {
Meter { name, meaning, enforcement, reports_usage: true, backing: None, scope: Scope::Tenant }
}
pub const fn cap_only(name: &'static str, meaning: &'static str, enforcement: Enforcement) -> Meter {
Meter { name, meaning, enforcement, reports_usage: false, backing: None, scope: Scope::Tenant }
}
pub const fn with_backing(self, backing: Backing) -> Meter {
Meter { backing: Some(backing), ..self }
}
pub const fn per_product(self) -> Meter {
Meter { scope: Scope::Product, ..self }
}
}
#[derive(Clone, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
pub enum Growth {
Sealed(String),
OperatorOnly(String),
Unknown(String),
AtRestart(String),
AtRuntime(String),
}
impl Growth {
pub fn name(&self) -> &'static str {
match self {
Growth::Sealed(_) => "sealed",
Growth::OperatorOnly(_) => "operator_only",
Growth::Unknown(_) => "unknown",
Growth::AtRestart(_) => "at_restart",
Growth::AtRuntime(_) => "at_runtime",
}
}
pub fn detail(&self) -> &str {
match self {
Growth::Sealed(s) | Growth::OperatorOnly(s) | Growth::Unknown(s) | Growth::AtRestart(s) | Growth::AtRuntime(s) => s,
}
}
}
#[derive(Clone, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
pub enum Servable {
Measured {
servable_bytes: u64,
total_bytes: u64,
growth: Growth,
ceilings: BTreeMap<String, u64>,
},
Unmeasured(String),
}
#[derive(Clone, PartialEq, Eq, Debug, Default, serde::Serialize, serde::Deserialize)]
pub struct Usage {
pub meters: BTreeMap<String, u64>,
pub measured_at_unix_ms: u64,
}
#[derive(Clone, PartialEq, Eq, Debug, Default, serde::Serialize, serde::Deserialize)]
#[serde(default)]
pub struct TenantOwner {
pub tenant: TenantId,
pub person: bool,
pub seller: bool,
}
#[derive(Clone, PartialEq, Eq, Debug, Default, serde::Serialize, serde::Deserialize)]
pub struct UsagePageReq {
#[serde(default)]
pub page_token: String,
#[serde(default)]
pub page_size: u32,
#[serde(default)]
pub changed_since: String,
}
#[derive(Clone, PartialEq, Eq, Debug, Default, serde::Serialize, serde::Deserialize)]
pub struct TenantUsage {
pub tenant: TenantId,
pub usage: Usage,
}
pub const KEEP_BOOT_WITHIN: std::time::Duration = std::time::Duration::from_secs(120 * 60);
pub const ESTATE_MARK: char = '@';
impl TenantId {
pub fn on_estate(estate: &str, namespace: &str) -> Self {
TenantId(format!("{ESTATE_MARK}{estate}/{namespace}"))
}
pub fn estate(&self) -> Option<(&str, &str)> {
let (estate, namespace) = self.0.strip_prefix(ESTATE_MARK)?.split_once('/')?;
(!estate.is_empty() && !namespace.is_empty()).then_some((estate, namespace))
}
}
#[derive(Clone, PartialEq, Eq, Debug, Default, serde::Serialize, serde::Deserialize)]
pub struct UsagePage {
#[serde(default)]
pub rows: Vec<TenantUsage>,
#[serde(default)]
pub next_page_token: String,
#[serde(default)]
pub cursor: String,
#[serde(default)]
pub changed_only: bool,
#[serde(default)]
pub as_of_unix_ms: u64,
}
#[derive(Clone, PartialEq, Eq, Debug, Default, serde::Serialize, serde::Deserialize)]
pub struct ActivityDay {
pub day_unix_ms: u64,
pub meters: BTreeMap<String, u64>,
}
#[derive(Clone, PartialEq, Eq, Debug, Default, serde::Serialize, serde::Deserialize)]
pub struct Activity {
pub days: Vec<ActivityDay>,
pub retained_days: u64,
}
#[derive(Clone, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
pub struct EntitlementFact {
pub tenant: TenantId,
pub plan: String,
pub state: State,
pub paid_until_unix_ms: Option<u64>,
pub caps: BTreeMap<String, u64>,
pub source: String,
pub signature: Vec<u8>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub issued_unix_ms: Option<u64>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub issued_signature: Vec<u8>,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
pub enum State {
Free,
Paid,
Grace,
Suspended,
Retention,
}
#[derive(Clone, Debug)]
pub enum ProductError {
Refused(String),
Unavailable(String),
}
impl std::fmt::Display for ProductError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ProductError::Refused(r) => write!(f, "product refused: {r}"),
ProductError::Unavailable(r) => write!(f, "product unavailable: {r}"),
}
}
}
impl std::error::Error for ProductError {}
#[derive(Clone, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
pub enum Absorb {
OnHand,
WithIron { note: String },
}
#[derive(Clone, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
pub enum MeteredDoor {
Enforcing(String),
Absent(String),
Unproven(String),
}
impl MeteredDoor {
pub fn name(&self) -> &'static str {
match self {
MeteredDoor::Enforcing(_) => "enforcing",
MeteredDoor::Absent(_) => "absent",
MeteredDoor::Unproven(_) => "unproven",
}
}
pub fn detail(&self) -> &str {
match self {
MeteredDoor::Enforcing(s) | MeteredDoor::Absent(s) | MeteredDoor::Unproven(s) => s,
}
}
pub fn proven(&self) -> bool {
matches!(self, MeteredDoor::Enforcing(_))
}
}
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct TestAction {
pub id: String,
pub label: String,
pub kind: TestActionKind,
}
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum TestActionKind {
HostedPlan { cap_gib: u64 },
PrivateInstance {
#[serde(default)]
front_plan: String,
#[serde(default)]
main_plan: String,
#[serde(default)]
main_data_gib: u64,
},
}
pub trait Product: Send + Sync {
fn id(&self) -> &'static str;
fn meters(&self) -> &[Meter];
fn list_tenants(&self) -> Result<Vec<TenantId>, ProductError>;
fn read_usage(&self, tenant: &TenantId) -> Result<Usage, ProductError>;
fn read_usage_page(&self, req: &UsagePageReq) -> Result<Option<UsagePage>, ProductError> {
let _ = req;
Ok(None)
}
fn tenant_owners(&self) -> Result<Option<Vec<TenantOwner>>, ProductError> {
Ok(None)
}
fn read_activity(
&self,
tenant: &TenantId,
since_unix_ms: u64,
until_unix_ms: u64,
) -> Result<Activity, ProductError> {
let _ = (tenant, since_unix_ms, until_unix_ms);
Err(ProductError::Refused(format!(
"the {} plugin cannot report a tenant's activity: it has no flow series, so an empty answer here would read as a customer who had gone quiet rather than as a question nobody asked the product.",
self.id()
)))
}
fn push_entitlement(&self, fact: &EntitlementFact) -> Result<(), ProductError>;
fn can_absorb(&self, tenant: &TenantId, caps: &BTreeMap<String, u64>) -> Result<Absorb, String>;
fn servable(&self) -> Servable;
fn servable_on(&self, estate: &str) -> Servable {
let _ = estate;
self.servable()
}
fn metered_door(&self) -> MeteredDoor {
MeteredDoor::Unproven(format!(
"the {} plugin has no metered-door probe, so nothing has asked this box whether it can enforce an entitlement at all",
self.id()
))
}
fn users_fit(&self, tenant: &TenantId, ask: &UsersAsk) -> Result<UsersAnswer, ProductError> {
let _ = (tenant, ask);
Err(ProductError::Refused(format!("the {} plugin cannot add users to a tenant", self.id())))
}
fn add_users(&self, tenant: &TenantId, ask: &UsersAsk, order: &str) -> Result<UsersAnswer, ProductError> {
let _ = (tenant, ask, order);
Err(ProductError::Refused(format!("the {} plugin cannot add users to a tenant", self.id())))
}
fn purge_tenant(&self, tenant: &TenantId, reason: &str) -> Result<Purged, ProductError> {
let _ = (tenant, reason);
Err(ProductError::Refused(format!(
"the {} plugin cannot purge a tenant's data: it has no purge verb, so nothing here \
can promise the tenant's bytes are gone. Empty the tenant in the product itself \
before releasing its resources.",
self.id()
)))
}
fn queues_room(&self) -> bool {
false
}
fn queue_room(&self, tenant: &TenantId, need_bytes: u64, why: &str) -> Result<(), ProductError> {
let _ = (tenant, need_bytes, why);
Err(ProductError::Refused(format!("the {} plugin queues no room", self.id())))
}
fn grow(&self) -> Option<&dyn ApplianceGrow> {
None
}
fn twin_grow(&self) -> Option<&dyn ApplianceGrow> {
None
}
fn guardian(&self) -> Option<&dyn guardian::EstateGuardian> {
None
}
fn twins(&self) -> Result<Vec<TwinFill>, ProductError> {
Ok(Vec::new())
}
fn tenant_renewal(&self) -> bool {
false
}
fn test_actions(&self) -> Vec<TestAction> {
Vec::new()
}
fn deliver_appliance_key(&self, hex: &str) -> Result<String, String> {
let _ = hex;
Err(format!("{} takes no delivered appliance key", self.id()))
}
fn may_act_for(
&self,
_tenant: &TenantId,
_purpose: &str,
_caps: &std::collections::BTreeMap<String, u64>,
_ticket: &[u8],
) -> Result<ActorVerdict, ProductError> {
Ok(ActorVerdict::Refused("this product cannot verify a tenant-presented ticket".into()))
}
}
pub struct ServedAs {
id: &'static str,
inner: Box<dyn Product>,
}
impl ServedAs {
pub fn new(id: &'static str, inner: Box<dyn Product>) -> Self {
Self { id, inner }
}
pub fn plugin_id(&self) -> &'static str {
self.inner.id()
}
}
impl Product for ServedAs {
fn id(&self) -> &'static str {
self.id
}
fn meters(&self) -> &[Meter] {
self.inner.meters()
}
fn list_tenants(&self) -> Result<Vec<TenantId>, ProductError> {
self.inner.list_tenants()
}
fn read_usage(&self, tenant: &TenantId) -> Result<Usage, ProductError> {
self.inner.read_usage(tenant)
}
fn read_usage_page(&self, req: &UsagePageReq) -> Result<Option<UsagePage>, ProductError> {
self.inner.read_usage_page(req)
}
fn tenant_owners(&self) -> Result<Option<Vec<TenantOwner>>, ProductError> {
self.inner.tenant_owners()
}
fn read_activity(&self, tenant: &TenantId, since_unix_ms: u64, until_unix_ms: u64) -> Result<Activity, ProductError> {
self.inner.read_activity(tenant, since_unix_ms, until_unix_ms)
}
fn push_entitlement(&self, fact: &EntitlementFact) -> Result<(), ProductError> {
self.inner.push_entitlement(fact)
}
fn can_absorb(&self, tenant: &TenantId, caps: &BTreeMap<String, u64>) -> Result<Absorb, String> {
self.inner.can_absorb(tenant, caps)
}
fn servable(&self) -> Servable {
self.inner.servable()
}
fn servable_on(&self, estate: &str) -> Servable {
self.inner.servable_on(estate)
}
fn metered_door(&self) -> MeteredDoor {
self.inner.metered_door()
}
fn users_fit(&self, tenant: &TenantId, ask: &UsersAsk) -> Result<UsersAnswer, ProductError> {
self.inner.users_fit(tenant, ask)
}
fn add_users(&self, tenant: &TenantId, ask: &UsersAsk, order: &str) -> Result<UsersAnswer, ProductError> {
self.inner.add_users(tenant, ask, order)
}
fn purge_tenant(&self, tenant: &TenantId, reason: &str) -> Result<Purged, ProductError> {
self.inner.purge_tenant(tenant, reason)
}
fn grow(&self) -> Option<&dyn ApplianceGrow> {
self.inner.grow()
}
fn twin_grow(&self) -> Option<&dyn ApplianceGrow> {
self.inner.twin_grow()
}
fn queues_room(&self) -> bool {
self.inner.queues_room()
}
fn queue_room(&self, tenant: &TenantId, need_bytes: u64, why: &str) -> Result<(), ProductError> {
self.inner.queue_room(tenant, need_bytes, why)
}
fn guardian(&self) -> Option<&dyn guardian::EstateGuardian> {
self.inner.guardian()
}
fn twins(&self) -> Result<Vec<TwinFill>, ProductError> {
self.inner.twins()
}
fn tenant_renewal(&self) -> bool {
self.inner.tenant_renewal()
}
fn deliver_appliance_key(&self, hex: &str) -> Result<String, String> {
self.inner.deliver_appliance_key(hex)
}
fn may_act_for(&self, tenant: &TenantId, purpose: &str, caps: &BTreeMap<String, u64>, ticket: &[u8]) -> Result<ActorVerdict, ProductError> {
self.inner.may_act_for(tenant, purpose, caps, ticket)
}
fn test_actions(&self) -> Vec<TestAction> {
self.inner.test_actions()
}
}
#[derive(Clone, PartialEq, Eq, Debug)]
pub enum ActorVerdict {
Allowed,
Refused(String),
}
#[derive(Clone, PartialEq, Eq, Debug, Default, serde::Serialize, serde::Deserialize)]
pub struct TwinFill {
pub principal: String,
pub address: String,
pub state: String,
pub last_seen_unix_ms: i64,
pub lag_entries: u64,
pub disk_total_bytes: u64,
pub disk_used_bytes: u64,
pub fill_permille: u32,
}
impl TwinFill {
pub fn visible(&self) -> bool {
matches!(self.state.as_str(), "ok" | "behind")
}
}
pub trait ApplianceGrow: Send + Sync {
fn flush(&self, timeout_secs: u32) -> Result<FlushReport, ProductError>;
fn start(&self, target_sectors_per_member: u64, go_ahead: &[u8]) -> Result<GrowStatus, ProductError>;
fn status(&self) -> Result<GrowStatus, ProductError>;
fn resume(&self, give_up: bool) -> Result<GrowStatus, ProductError>;
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum GrowPhase {
Idle,
Draining,
Unmounted,
WaitingMembers,
Growing,
Mounting,
Done,
Failed,
Other,
}
impl GrowPhase {
pub fn parse(word: &str) -> GrowPhase {
match word {
"idle" => GrowPhase::Idle,
"draining" => GrowPhase::Draining,
"unmounted" => GrowPhase::Unmounted,
"waiting-members" => GrowPhase::WaitingMembers,
"growing" => GrowPhase::Growing,
"mounting" => GrowPhase::Mounting,
"done" => GrowPhase::Done,
"failed" => GrowPhase::Failed,
_ => GrowPhase::Other,
}
}
pub fn volumes_free(self) -> bool {
matches!(self, GrowPhase::Unmounted | GrowPhase::WaitingMembers)
}
pub fn is_terminal(self) -> bool {
matches!(self, GrowPhase::Done | GrowPhase::Failed | GrowPhase::Idle)
}
}
#[derive(Clone, PartialEq, Eq, Debug, Default, serde::Serialize, serde::Deserialize)]
pub struct GrowMember {
pub index: u32,
pub device: String,
pub disk_bytes: u64,
pub set_bytes: u64,
pub larger: bool,
}
#[derive(Clone, PartialEq, Eq, Debug, Default, serde::Serialize, serde::Deserialize)]
pub struct GrowStatus {
pub phase: String,
pub why: String,
pub epoch: u64,
pub target_sectors_per_member: u64,
pub members: Vec<GrowMember>,
pub members_verdict: String,
pub engine_present: bool,
pub serving: bool,
pub in_flight: u64,
pub since_unix_ms: i64,
pub detail: String,
pub go_ahead_signer: String,
pub set_uuid: String,
pub total_bytes: u64,
}
impl GrowStatus {
pub fn phase(&self) -> GrowPhase {
GrowPhase::parse(&self.phase)
}
}
#[derive(Clone, PartialEq, Eq, Debug, Default, serde::Serialize, serde::Deserialize)]
pub struct FlushReport {
pub caught_up: bool,
pub in_flight: u64,
pub pending: u64,
pub bus_gaps: u64,
pub needs_full_resync: bool,
pub last_success_unix_ms: i64,
pub flushed_at_unix_ms: i64,
pub waited_ms: u64,
pub catalog_repos: u64,
pub standbys: u64,
pub verdict: String,
}
#[derive(Clone, PartialEq, Eq, Debug, Default, serde::Serialize, serde::Deserialize)]
#[serde(default)]
pub struct UsersAsk {
pub count: u32,
pub room_bytes: u64,
#[serde(skip_serializing_if = "String::is_empty")]
pub name: String,
#[serde(skip_serializing_if = "String::is_empty")]
pub email: String,
#[serde(skip_serializing_if = "String::is_empty")]
pub invite_origin: String,
#[serde(skip_serializing_if = "String::is_empty")]
pub principal_id: String,
}
impl UsersAsk {
pub fn ticket_caps(&self) -> BTreeMap<String, u64> {
[("users".to_owned(), u64::from(self.count.max(1))), ("user_room_bytes".to_owned(), self.room_bytes)].into_iter().collect()
}
pub fn fingerprint(&self) -> String {
match self.principal_id.as_str() {
"" => format!("{}x{}", self.count.max(1), self.room_bytes),
id => format!("{}x{}:{id}", self.count.max(1), self.room_bytes),
}
}
pub fn without_names(&self) -> UsersAsk {
UsersAsk { name: String::new(), email: String::new(), ..self.clone() }
}
pub fn is_named(&self) -> bool {
!self.name.is_empty() || !self.principal_id.is_empty()
}
}
#[derive(Clone, PartialEq, Eq, Debug, Default, serde::Serialize, serde::Deserialize)]
#[serde(default)]
pub struct UsersAnswer {
pub principal: String,
pub room_bytes: u64,
pub need_bytes: u64,
pub unused_bytes: Option<u64>,
pub shortfall_bytes: u64,
pub seats: u32,
pub seats_open: u32,
pub unchanged: bool,
#[serde(skip_serializing_if = "String::is_empty")]
pub invite_secret: String,
}
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct Purged {
pub tenant: TenantId,
pub detail: String,
pub bytes_reclaimed: Option<u64>,
pub still_locked: bool,
pub failures: Vec<String>,
}
pub fn bytes(n: u64) -> String {
const KIB: u64 = 1 << 10;
const MIB: u64 = 1 << 20;
const GIB: u64 = 1 << 30;
const TIB: u64 = 1 << 40;
let (unit, per) = match n {
n if n >= TIB => ("TiB", TIB),
n if n >= GIB => ("GiB", GIB),
n if n >= MIB => ("MiB", MIB),
n if n >= KIB => ("KiB", KIB),
n => return format!("{n} B"),
};
format!("{}.{} {unit}", n / per, ((n % per) * 10) / per)
}
#[cfg(test)]
mod backing_tests {
use super::Backing;
#[test]
fn prices_round_bytes_up_to_the_unit_and_cpu_by_the_millicore() {
const GIB: u64 = 1 << 30;
assert_eq!(Backing::DiskBytes.price_month(90 * GIB, 2200), 198_000, "90 GiB × 22.00 SEK");
assert_eq!(Backing::DiskBytes.price_month(90 * GIB + 1, 2200), 200_200, "one byte over is the 91st GiB");
assert_eq!(Backing::DiskBytes.price_month(0, 2200), 0);
assert_eq!(Backing::RamBytes.price_month(3 * GIB, 700), 2100);
assert_eq!(Backing::CpuMillicores.price_month(1500, 30_000), 45_000, "1.5 cores at 300.00");
assert_eq!(Backing::CpuMillicores.price_month(1, 30_000), 30, "one millicore is not free and not a core");
assert_eq!(Backing::Units.price_month(5, 20_000), 100_000, "5 × 200.00");
assert_eq!(Backing::Units.price_month(1, 20_000), 20_000);
assert_eq!(Backing::Units.price_month(0, 20_000), 0);
assert_eq!(Backing::Units.unit(), "unit_month");
for b in Backing::ALL {
assert_eq!(b.price_month(u64::MAX / 4, 0), 0, "{b:?}");
}
for b in Backing::ALL {
assert_eq!(Backing::from_unit(b.unit()), Some(b));
}
assert_eq!(Backing::from_unit("moon_month"), None);
}
}
#[cfg(test)]
mod bytes_tests {
use super::bytes;
#[test]
fn only_a_genuine_zero_reads_as_zero() {
for n in [1u64, 512, 1 << 20, 67_108_864, (1 << 30) - 1] {
let s = bytes(n);
assert!(!s.starts_with("0.0 ") && !s.starts_with("0 "), "{n} bytes rendered as {s:?}, which reads as nothing");
}
assert_eq!(bytes(0), "0 B");
}
#[test]
fn the_unit_scales_and_the_value_truncates() {
assert_eq!(bytes(999), "999 B");
assert_eq!(bytes(1536), "1.5 KiB");
assert_eq!(bytes(67_108_864), "64.0 MiB");
assert_eq!(bytes((1 << 30) - 1), "1023.9 MiB", "truncates, never rounds up past the cap");
assert_eq!(bytes(10 << 30), "10.0 GiB");
assert_eq!(bytes(3 << 40), "3.0 TiB");
assert_eq!(bytes(u64::MAX), "16777215.9 TiB", "no overflow at the top of the range");
}
}
#[cfg(test)]
mod actor_tests {
use super::*;
struct OldPlugin;
impl Product for OldPlugin {
fn id(&self) -> &'static str {
"old"
}
fn meters(&self) -> &[Meter] {
&[]
}
fn list_tenants(&self) -> Result<Vec<TenantId>, ProductError> {
Ok(Vec::new())
}
fn read_usage(&self, _tenant: &TenantId) -> Result<Usage, ProductError> {
Err(ProductError::Refused("no".into()))
}
fn push_entitlement(&self, _fact: &EntitlementFact) -> Result<(), ProductError> {
Ok(())
}
fn can_absorb(&self, _tenant: &TenantId, _caps: &BTreeMap<String, u64>) -> Result<Absorb, String> {
Ok(Absorb::OnHand)
}
fn servable(&self) -> Servable {
Servable::Unmeasured("a test double measures nothing".to_owned())
}
}
#[test]
fn a_product_that_never_heard_of_a_ticket_refuses_every_one() {
let p = OldPlugin;
assert!(!p.tenant_renewal(), "a console must not be told to offer self-service here");
let verdict = p.may_act_for(&TenantId("alice".into()), "renew", &Default::default(), b"anything at all").expect("the default answers rather than erroring");
let ActorVerdict::Refused(why) = verdict else { panic!("the default must DENY") };
assert!(!why.trim().is_empty(), "a refusal says why");
}
}
#[cfg(test)]
mod estate_tenant_tests {
use super::*;
#[test]
fn a_sold_estates_tenant_carries_the_estate_in_its_path_and_no_own_tenant_reads_as_one() {
let t = TenantId::on_estate("acme", "bob");
assert_eq!(t.0, "@acme/bob");
assert_eq!(t.estate(), Some(("acme", "bob")));
assert_eq!(TenantId::on_estate("acme", "team/sub").estate(), Some(("acme", "team/sub")));
for own in ["bob", "team/sub", "acme/bob", "", "@acme", "@/bob", "@acme/", "x@acme/bob"] {
assert_eq!(TenantId(own.into()).estate(), None, "{own:?}");
}
}
}