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/src/testing/products.rs

//! **Gunnar- and Holger-shaped TEST products** (monetize v2.0 P2 step 2, item 12.0; A28 H6).
//!
//! The server's tests that drove the registry, the purchase path and the pull channel through
//! the NATIVE product plugins (`monetize-product-gunnar`, `monetize-product-holger`) drive these
//! instead, so the natives can go (A25 2.1, 2.2). A test product dials nothing:
//!
//! * [`TestProduct`]: a product's meters, the [`super::guardian::TestGuardian`] (its factory
//!   links it), and a main that mints its buyers' tickets (the HOST checks them,
//!   `crate::site_ticket`). Its estate is a [`super::estate::FakeEstate`] under `Pulled`
//!   (A32-13 step 2): tenants, usage and room are the fake main's report, a pushed entitlement an
//!   order it pulls.
//! * [`HOLGER_FACTORY`] / [`GUNNAR_FACTORY`]: `[products.holger]` / `[products.gunnar]` built as
//!   a [`TestProduct`] (with `instance = …` the registry wraps it in `Pulled`, as any product).

use monetize_product::{Backing, Enforcement, Growth, Meter, ProductBuild, ProductFactory, Servable};

/// Holger's meters: `disk_bytes` is the one backed meter.
pub const HOLGER_METERS: &[Meter] = &[
    Meter::new("artifacts", "artifacts in the repository", Enforcement::Measured),
    Meter::new("artifact_bytes", "sum of the repository's artifact sizes, uncompressed", Enforcement::Measured),
    Meter::new("disk_bytes", "what the repository occupies on the disk", Enforcement::Enforced).with_backing(Backing::DiskBytes),
    Meter::new("archive_bytes", "uncompressed bytes in the archive behind the repository", Enforcement::Measured),
    Meter::new("archive_files", "files in that archive", Enforcement::Measured),
];

/// Gunnar's meters, as the native plugin declared them: the sold `pack_bytes` and `lfs_bytes`
/// are backed by disk; the cache budget is policed inside the pack's space.
pub const GUNNAR_METERS: &[Meter] = &[
    Meter::new("pack_bytes", "object bytes on disk that are not re-derivable; what is sold", Enforcement::Enforced).with_backing(Backing::DiskBytes),
    Meter::new("cache_bytes", "the exploded read cache; re-derivable", Enforcement::Enforced),
    Meter::new("cold_bytes", "the cold tier alone", Enforcement::Measured),
    Meter::new("lfs_bytes", "LFS objects under the namespace's git dirs", Enforcement::Enforced).with_backing(Backing::DiskBytes),
    Meter::new("tombstoned_bytes", "soft-deleted repositories not yet purged", Enforcement::Measured),
    Meter::new("repos", "live repositories in the namespace", Enforcement::Measured),
];

/// **A product's fixed facts and nothing else** (A32-13 step 2): its id, meters, the fact that
/// its main mints tickets, and the test guardian. Its estate is not its own: the registry serves
/// it under `Pulled` ([`crate::registry::Registry::pulled_product`], or `[products.<id>]
/// instance = …`), and the test's [`super::estate::FakeEstate`] is that estate's main. Every
/// estate verb asked of the product itself is refused by name.
pub struct TestProduct {
    id: &'static str,
    meters: &'static [Meter],
}

impl TestProduct {
    /// A test product `id` with `meters`.
    pub fn new(id: &'static str, meters: &'static [Meter]) -> TestProduct {
        TestProduct { id, meters }
    }
    /// The Holger-shaped one.
    pub fn holger() -> TestProduct {
        TestProduct::new("holger", HOLGER_METERS)
    }
    /// The Gunnar-shaped one.
    pub fn gunnar() -> TestProduct {
        TestProduct::new("gunnar", GUNNAR_METERS)
    }
}

impl TestProduct {
    pub fn facts(&self) -> monetize_product::ProductFacts {
        monetize_product::ProductFacts { tickets: true, guardian: true, ..monetize_product::ProductFacts::new(self.id, self.meters) }
    }
}

/// Served with no estate of its own: every box verb is refused by name.
impl crate::registry::IntoServed for TestProduct {
    fn into_served(self) -> std::sync::Arc<monetize_product::pulled::Pulled> {
        let facts = self.facts();
        std::sync::Arc::new(monetize_product::pulled::Pulled::new(std::sync::Arc::new(move || facts.clone()), std::sync::Arc::new(monetize_product::pulled::Unplaced(self.id))))
    }
}

/// A measured box of `free` of `total` GiB that an operator grows (the canned `servable`).
pub fn measured(free_gib: u64, total_gib: u64) -> Servable {
    const GIB: u64 = 1 << 30;
    Servable::Measured { servable_bytes: free_gib * GIB, total_bytes: total_gib * GIB, growth: Growth::Unknown("a test box".into()), ceilings: Default::default() }
}

fn build_holger(_: &ProductBuild<'_>) -> Result<monetize_product::pulled::FactsSource, String> {
    Ok(std::sync::Arc::new(|| TestProduct::holger().facts()))
}

fn build_gunnar(_: &ProductBuild<'_>) -> Result<monetize_product::pulled::FactsSource, String> {
    Ok(std::sync::Arc::new(|| TestProduct::gunnar().facts()))
}

fn no_identity(_: &ProductBuild<'_>) -> Result<String, String> {
    Err("a test product has no identity to mint".into())
}

/// `[products.holger]` built as the Holger-shaped [`TestProduct`].
pub const HOLGER_FACTORY: ProductFactory = ProductFactory { name: "holger", build: build_holger, mint_identity: no_identity, guardian: Some(&super::guardian::GUARDIAN) };

/// `[products.gunnar]` built as the Gunnar-shaped [`TestProduct`].
pub const GUNNAR_FACTORY: ProductFactory = ProductFactory { name: "gunnar", build: build_gunnar, mint_identity: no_identity, guardian: Some(&super::guardian::GUARDIAN) };

#[cfg(test)]
mod tests {
    use super::*;

    /// ★ The test guardian comes with the factories, and Holger's one backed meter is disk_bytes.
    #[test]
    fn the_factories_link_the_test_guardian_and_holger_sells_disk_bytes() {
        assert!(TestProduct::gunnar().facts().guardian && HOLGER_FACTORY.guardian.is_some() && GUNNAR_FACTORY.guardian.is_some(), "the test guardian comes with the factory");
        assert_eq!(TestProduct::holger().facts().meters.iter().filter(|m| m.backing.is_some()).map(|m| m.name).collect::<Vec<_>>(), ["disk_bytes"]);
    }

    /// ★ **A32-13 step 2: a test product is served as production serves every product**: under
    /// `Pulled`, its tenants, usage and room its fake estate's report, its entitlements orders its
    /// fake main pulls. The product itself answers none of it (no canned estate of its own).
    #[test]
    fn a_test_product_is_served_under_pulled_from_its_fake_estates_report() {
        use monetize_product::pull::order;
        use monetize_product::{EntitlementFact, ProductError, TenantId};
        let state = crate::state::AppState::in_memory(crate::registry::Registry::new().pulled_product(TestProduct::gunnar(), "gunnar.rs"));
        let estate = crate::testing::estate::FakeEstate::declare(&state, "gunnar", "gunnar.rs");
        let p = state.registry.find_product("gunnar").unwrap();
        assert!(matches!(p.list_tenants(), Err(ProductError::Unavailable(_))), "a silent main: nothing is read");
        estate.tenant("acme", true, &[("pack_bytes", 7)]);
        assert_eq!(p.list_tenants().unwrap(), [TenantId("acme".into())]);
        assert_eq!(p.read_usage(&TenantId("acme".into())).unwrap().meters["pack_bytes"], 7);
        assert!(p.tenant_owners().unwrap().unwrap()[0].person);
        let fact = EntitlementFact { tenant: TenantId("acme".into()), plan: "p".into(), state: monetize_product::State::Paid, paid_until_unix_ms: None, caps: [("pack_bytes".to_string(), 9)].into(), source: "test".into(), signature: Vec::new(), issued_unix_ms: None, issued_signature: Vec::new() };
        p.push_entitlement(&fact).unwrap();
        assert_eq!(estate.queued(order::ENTITLEMENT).len(), 1, "queued for the main, never dialled");
        assert_eq!(estate.call_home().len(), 1);
        assert_eq!(estate.held(), std::slice::from_ref(&fact));
        assert!(estate.orders().is_empty(), "acked in the main's report");
    }

    /// ★ A buyer's ticket is checked by the HOST under the key the main delivered, and refused
    /// before one is (A32-13: the product checks none).
    #[test]
    fn a_ticket_is_checked_under_the_delivered_key() {
        use ed25519_dalek::Signer as _;
        use monetize_embed::ticket::{ticket_message, ActorTicket, MAX_LIFE_MS, PURPOSE_ORDER};
        use monetize_product::ActorVerdict;
        let state = crate::state::AppState::in_memory(crate::registry::Registry::new().product(TestProduct::holger()));
        let p = state.registry.find_product("holger").unwrap();
        let main = ed25519_dalek::SigningKey::from_bytes(&[12u8; 32]);
        let mut t = ActorTicket::unsigned("holger", "holger", "crates", PURPOSE_ORDER, "n-1", state.now() as i64, MAX_LIFE_MS);
        t.signature = monetize_embed::signing::base64_encode(&main.sign(&ticket_message(&t)).to_bytes());
        let ticket = serde_json::to_vec(&t).unwrap();
        let ask = || crate::site_ticket::may_act(&state, p, None, "crates", "order", &std::collections::BTreeMap::new(), &ticket).unwrap();
        assert!(matches!(ask(), ActorVerdict::Refused(_)));
        let hex: String = main.verifying_key().as_bytes().iter().map(|b| format!("{b:02x}")).collect();
        state.registry.deliver_product_key("holger", &hex).unwrap();
        assert_eq!(ask(), ActorVerdict::Allowed);
        assert!(matches!(ask(), ActorVerdict::Refused(w) if w.contains("already been spent")), "a ticket is good once");
        assert!(state.registry.deliver_product_key("holger", "zz").is_err());
        assert_eq!(state.registry.tickets_checked("holger"), 3);
    }
}