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/pull_holger.rs

//! **Holger is a pulled product, like Gunnar** (5.4, G1; the lifecycle parity of 2026-10-17).
//!
//! `[products.holger] instance = "<estate>"` builds the product (here the Holger-shaped TEST
//! product, `monetize_server::testing::products::HOLGER_FACTORY`, which dials nothing; v2.0 P2
//! 12.0) and wraps it in `monetize_guardian::pulled::Pulled`: every read is answered from the
//! estate main's `Fleet.Report` (a product-generic `monetize_product::pull::ProductReport` — the
//! JSON holger's box-side Fleet client sends), and every change leaves as a signed order the box
//! pulls. The product's own verbs stay: its meters, its guardian and its buyers' tickets.

use monetize_embed::fleet::Report;
use monetize_embed::signing::base64_encode;
use monetize_product::guardian::{role, BoxView, EstateView};
use monetize_product::pull::{order, ProductReport};
use monetize_product::*;
use monetize_server::config::Config;
use monetize_server::registry::{Linked, Registry};
use monetize_server::state::AppState;

const ESTATE: &str = "holger-rs";
const GIB: u64 = 1 << 30;

fn linked() -> Linked {
    let mut l = Linked::open();
    l.product(monetize_server::testing::products::HOLGER_FACTORY);
    l
}

fn config() -> String {
    let key = base64_encode(
        ed25519_dalek::SigningKey::from_bytes(&[11u8; 32])
            .verifying_key()
            .as_bytes(),
    );
    format!("[products.holger]\nfree = true\ninstance = \"{ESTATE}\"\nfleet_keys = {{ main = \"{key}\" }}\n")
}

fn now_ms() -> u64 {
    std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .unwrap()
        .as_millis() as u64
}

/// **The ProductReport a holger main sends**, in the generic shape: its repositories are the
/// tenants, its own `ReadUsage` readings the usage (with the sold `disk_bytes`), its capacity
/// the servable, its metered door's answer, the room a raise would find in the sold meter.
fn holger_report(now: u64) -> String {
    let pr = ProductReport {
        tenants: vec![
            TenantOwner {
                tenant: TenantId("crates".into()),
                person: false,
                seller: false,
            },
            TenantOwner {
                tenant: TenantId("bundles".into()),
                person: false,
                seller: false,
            },
        ],
        usage: [
            (
                "crates".to_string(),
                Usage {
                    meters: [
                        ("disk_bytes".to_string(), 4_096),
                        ("artifacts".to_string(), 2),
                        ("archive_bytes".to_string(), 2_500),
                    ]
                    .into(),
                    measured_at_unix_ms: now,
                },
            ),
            (
                "bundles".to_string(),
                Usage {
                    meters: [
                        ("disk_bytes".to_string(), 8_192),
                        ("artifacts".to_string(), 2),
                    ]
                    .into(),
                    measured_at_unix_ms: now,
                },
            ),
        ]
        .into(),
        servable: Some(Servable::Measured {
            servable_bytes: 40 * GIB,
            total_bytes: 100 * GIB,
            growth: Growth::Unknown("holger sent no verdict".into()),
            ceilings: Default::default(),
        }),
        metered: Some(MeteredDoor::Enforcing(
            "holger EntitlementService.List answered (0 fact(s) held)".into(),
        )),
        room: [("disk_bytes".to_string(), 40 * GIB)].into(),
        promised: [(
            "crates".to_string(),
            [("disk_bytes".to_string(), 5 * GIB)].into(),
        )]
        .into(),
        names: vec!["crates".into(), "bundles".into()],
        measured_unix_ms: now,
        ..Default::default()
    };
    serde_json::to_string(&pr).unwrap()
}

#[test]
fn holger_with_an_instance_is_pulled_and_never_dialled() {
    let cfg = Config::from_toml(&config()).expect("a pulled holger loads");
    let reg = Registry::from_config_with(&cfg, &linked())
        .expect("the Holger-shaped product builds and is never dialled");
    let state = AppState::in_memory(reg);
    monetize_server::fleet::declare_pulled(&state, &cfg).expect("declared");
    let g = state.fleet.guardian().expect("the in-memory guardian");
    assert_eq!(g.store.product_of(ESTATE).as_deref(), Some("holger"));

    let p = state
        .registry
        .products()
        .find(|p| p.id() == "holger")
        .expect("served");
    assert!(
        matches!(p.list_tenants(), Err(ProductError::Unavailable(w)) if w.contains(ESTATE)),
        "before a report: unavailable, never a dial"
    );
    assert_eq!(
        p.meters()
            .iter()
            .find(|m| m.backing.is_some())
            .map(|m| m.name),
        Some("disk_bytes"),
        "the plugin's own meters: disk_bytes is the sold one"
    );
    assert!(p.guardian().is_some(), "the plugin's own guardian");

    let now = now_ms();
    let r = Report {
        product: Some(holger_report(now)),
        measured_unix_ms: now,
        ..Default::default()
    };
    g.store.record_report(ESTATE, role::MAIN, now, &r).unwrap();

    assert_eq!(
        p.list_tenants().unwrap(),
        vec![TenantId("crates".into()), TenantId("bundles".into())]
    );
    assert_eq!(
        p.read_usage(&TenantId("crates".into())).unwrap().meters["disk_bytes"],
        4_096
    );
    assert_eq!(p.metered_door().name(), "enforcing", "the box's own answer");
    assert!(
        matches!(p.servable(), Servable::Measured { servable_bytes, .. } if servable_bytes == 40 * GIB)
    );
    let raise = |gib: u64| {
        p.can_absorb(
            &TenantId("crates".into()),
            &[("disk_bytes".to_string(), gib * GIB)].into(),
        )
    };
    assert!(
        matches!(raise(40), Ok(Absorb::OnHand)),
        "5 GiB promised: a raise to 40 needs 35 of the 40 GiB room"
    );
    assert!(
        raise(50).is_err(),
        "45 GiB beyond what is held does not fit the room"
    );

    // The entitlement and the purge leave as signed orders the main pulls.
    let fact = EntitlementFact {
        tenant: TenantId("crates".into()),
        plan: "holger/crates/10gb-1m".into(),
        state: State::Paid,
        paid_until_unix_ms: None,
        caps: [("disk_bytes".to_string(), 10 * GIB)].into(),
        source: "test".into(),
        signature: Vec::new(),
        issued_unix_ms: Some(now),
        issued_signature: Vec::new(),
    };
    p.push_entitlement(&fact).expect("queued, not dialled");
    let purged = p
        .purge_tenant(&TenantId("bundles".into()), "nonpay day 30")
        .expect("queued, not dialled");
    assert!(purged.still_locked, "{}", purged.detail);
    let kinds: Vec<String> = g
        .store
        .orders_for(ESTATE, role::MAIN, 0)
        .into_iter()
        .map(|o| o.kind)
        .collect();
    assert_eq!(kinds, [order::ENTITLEMENT, order::PURGE_TENANT]);
}

/// **The guardian reads holger's boxes from the same reports**: a silent main behind a whole
/// twin that cannot see it either is Gunnar's rebuild case (New server), and auto-fix may run
/// it; a held main the rebuild laid is no case of its own (W3, Gunnar's rule).
#[test]
fn the_pulled_holger_hands_its_guardian_to_the_runner() {
    let cfg = Config::from_toml(&config()).unwrap();
    let reg = Registry::from_config_with(&cfg, &linked()).unwrap();
    let p = reg.products().find(|p| p.id() == "holger").unwrap();
    let now = now_ms();
    let b = |r: &str, words: &[(&str, &str)]| {
        (
            r.to_string(),
            BoxView {
                role: r.into(),
                heard_unix_ms: now,
                health: words
                    .iter()
                    .map(|(k, v)| (k.to_string(), v.to_string()))
                    .collect(),
                ..Default::default()
            },
        )
    };
    let v = EstateView {
        product: "holger".into(),
        instance: ESTATE.into(),
        now_unix_ms: now,
        silent_after_ms: 90_000,
        boxes: [
            b("main", &[("serving", "no"), ("data", "fresh")]),
            b("twin", &[("primary", "live"), ("complete", "yes")]),
        ]
        .into_iter()
        .collect(),
    };
    let g = p.guardian().expect("holger's guardian");
    assert!(
        g.diagnose(&v).is_empty(),
        "a held main is the rebuild plan's box, no finding of its own"
    );
    let mut lost = v.clone();
    lost.boxes.get_mut("main").unwrap().heard_unix_ms = now - 600_000;
    // GONE at the provider (the runner's word, lane G11): silence alone lays nothing.
    lost.boxes.get_mut("main").unwrap().at_provider = "gone".into();
    lost.boxes
        .get_mut("twin")
        .unwrap()
        .health
        .insert("primary".into(), "silent".into());
    let f = g.diagnose(&lost);
    assert_eq!(
        f.iter()
            .map(|f| (f.code.as_str(), f.auto))
            .collect::<Vec<_>>(),
        [(
            "main_lost",
            Some(monetize_product::guardian::Remedy::NewServer)
        )]
    );
}

/// **The key holger's main signs its buyers' tickets with reaches holger's plugin by itself**
/// (2026-10-17): its owner's install notice delivers it (`ticket_key`, the main's fleet key),
/// the guardian takes the joined record, and the HOST checks the tickets of the product whose
/// HOME estate it is under that key (A32-13).
/// Nobody copies it into `[products.holger] appliance_key`.
#[test]
fn the_delivered_ticket_key_vouches_for_a_buyer_of_the_pulled_holger() {
    use ed25519_dalek::Signer as _;
    use monetize_embed::ticket::{ticket_message, ActorTicket, MAX_LIFE_MS, PURPOSE_ORDER};
    let cfg = Config::from_toml(&config()).unwrap();
    let reg = Registry::from_config_with(&cfg, &linked()).unwrap();
    let state = AppState::in_memory(reg);
    monetize_server::fleet::declare_pulled(&state, &cfg).unwrap();
    let main = ed25519_dalek::SigningKey::from_bytes(&[12u8; 32]);
    let hex: String = main
        .verifying_key()
        .as_bytes()
        .iter()
        .map(|b| format!("{b:02x}"))
        .collect();
    let mint = |nonce: &str| {
        let now = now_ms() as i64;
        let mut t = ActorTicket::unsigned(
            "holger",
            "holger",
            "crates",
            PURPOSE_ORDER,
            nonce,
            now,
            MAX_LIFE_MS,
        )
        .covering([("disk_bytes".to_string(), 10 * GIB)].into());
        t.signature = base64_encode(&main.sign(&ticket_message(&t)).to_bytes());
        serde_json::to_vec(&t).unwrap()
    };
    let plan: std::collections::BTreeMap<String, u64> =
        [("disk_bytes".to_string(), 10 * GIB)].into();
    let p = state
        .registry
        .products()
        .find(|p| p.id() == "holger")
        .unwrap();
    assert!(
        matches!(
            monetize_server::site_ticket::may_act(
                &state,
                p,
                None,
                "crates",
                "order",
                &plan,
                &mint("k-0")
            )
            .unwrap(),
            ActorVerdict::Refused(_)
        ),
        "before the key is delivered: nobody is vouched for"
    );

    let dir = std::env::temp_dir().join(format!("pull-holger-joined-{}", std::process::id()));
    std::fs::create_dir_all(&dir).unwrap();
    let now = now_ms();
    let record = serde_json::json!({ "estate": ESTATE, "product": "holger", "box_name": "main", "endpoint": "10.6.0.10:50051", "estate_ca_pem": "", "owner_key": "", "enrolled_unix_ms": now, "roles": ["main"], "installed_unix_ms": now, "appliance_key": hex, "installed_with_ticket_key": true });
    std::fs::write(dir.join(format!("{ESTATE}.json")), record.to_string()).unwrap();
    let lines = monetize_server::fleet::take_joined(&state, &dir);
    let _ = std::fs::remove_dir_all(&dir);
    assert_eq!(
        monetize_server::site_ticket::may_act(
            &state,
            p,
            None,
            "crates",
            "order",
            &plan,
            &mint("k-1")
        )
        .unwrap(),
        ActorVerdict::Allowed,
        "{lines:?}"
    );
}