crates/gunnar-monetize-guest/tests/published_embed_agrees_with_the_head.rs
//! ★ **The monetize-embed gunnar-server links is the monetize-embed monetize signs with**
//! (gunnar.rs audit 2026-10-12, S14).
//!
//! gunnar-server and gunnar-grpc resolve `monetize-embed` from crates.io (0.1.3, the
//! `version` requirement their manifests explain), while this guest — and monetize-server —
//! build the HEAD by path. The two copies sit side by side in gunnar's `Cargo.lock`. Today the
//! signed forms agree; the day monetize changes `ticket_message`, `fact_message`, the snapshot
//! or the go-ahead form and nobody publishes, every ticket gunnar mints and every fact it
//! checks would fail SILENTLY in the field.
//!
//! This test builds every signed form both ways from the same values and asserts the bytes
//! are equal, and that the purpose words gunnar spells locally (gunnar-grpc
//! `service/billing.rs`: `PURPOSE_CANCEL`, `PURPOSE_ESTATE`, because 0.1.3 predates them)
//! are the head's. It fails on the first drift: publish monetize-embed from the head (and
//! `cargo update -p monetize-embed` here) before such a change merges.
use std::collections::BTreeMap;
use monetize_embed as head;
use monetize_embed_published as published;
fn fact_json(issued: Option<u64>) -> serde_json::Value {
serde_json::json!({
"tenant": "@test2-gunnar-rs/nina",
"plan": "gunnar/nina/2026-10-12+pack_bytes=10737418240",
"state": "Paid",
"paid_until_unix_ms": 1_793_000_000_000u64,
"caps": { "pack_bytes": 10_737_418_240u64, "lfs_bytes": 1_073_741_824u64 },
"source": "payment:stripe/gunnar-test2:pi_123",
"signature": [1, 2, 3],
"issued_unix_ms": issued,
})
}
fn both_facts(issued: Option<u64>) -> (head::EntitlementFact, published::EntitlementFact) {
let h: head::EntitlementFact =
serde_json::from_value(fact_json(issued)).expect("the head reads the fact");
// Through the head's own serialisation: a field the head added and 0.1.3 lacks shows up
// as a different signed form below, never as a quiet drop here.
let p: published::EntitlementFact = serde_json::from_value(serde_json::to_value(&h).unwrap())
.expect("0.1.3 reads the head's fact");
(h, p)
}
#[test]
fn a_fact_signs_the_same_bytes_in_both_copies() {
for issued in [None, Some(1_792_000_000_000u64)] {
let (h, p) = both_facts(issued);
assert_eq!(
head::signing::fact_message(&h),
published::signing::fact_message(&p),
"V1 fact form drifted (issued {issued:?})"
);
if issued.is_some() {
assert_eq!(
head::signing::fact_message_issued(&h),
published::signing::fact_message_issued(&p),
"V2 fact form drifted"
);
}
assert_eq!(
head::signing::snapshot_message(7, std::slice::from_ref(&h)),
published::signing::snapshot_message(7, std::slice::from_ref(&p)),
"snapshot form drifted"
);
}
}
#[test]
fn a_ticket_signs_the_same_bytes_in_both_copies() {
let caps = BTreeMap::from([("pack_bytes".to_string(), 10_737_418_240u64)]);
for purpose in [
head::ticket::PURPOSE_RENEW,
head::ticket::PURPOSE_ORDER,
head::ticket::PURPOSE_READ,
head::ticket::PURPOSE_CANCEL,
head::ticket::PURPOSE_ESTATE,
] {
let h = head::ticket::ActorTicket::unsigned(
"gunnar",
"gunnar",
"nina",
purpose,
"nonce-1",
1_792_000_000_000,
60_000,
)
.covering(caps.clone());
let p = published::ticket::ActorTicket::unsigned(
"gunnar",
"gunnar",
"nina",
purpose,
"nonce-1",
1_792_000_000_000,
60_000,
)
.covering(caps.clone());
assert_eq!(
head::ticket::ticket_message(&h),
published::ticket::ticket_message(&p),
"ticket form drifted ({purpose})"
);
}
assert_eq!(
head::ticket::MAX_LIFE_MS,
published::ticket::MAX_LIFE_MS,
"a ticket's life drifted"
);
}
/// gunnar-grpc `service/billing.rs` spells these two because 0.1.3 has no constant for them;
/// the verifier compares the word, so the literal is the contract.
#[test]
fn the_purpose_words_gunnar_spells_itself_are_the_heads() {
assert_eq!(
head::ticket::PURPOSE_CANCEL,
"cancel",
"gunnar-grpc billing.rs PURPOSE_CANCEL"
);
assert_eq!(
head::ticket::PURPOSE_ESTATE,
"estate",
"gunnar-grpc billing.rs PURPOSE_ESTATE"
);
assert_eq!(
(
head::ticket::PURPOSE_RENEW,
head::ticket::PURPOSE_ORDER,
head::ticket::PURPOSE_READ
),
(
published::ticket::PURPOSE_RENEW,
published::ticket::PURPOSE_ORDER,
published::ticket::PURPOSE_READ
)
);
}
#[test]
fn a_go_ahead_and_the_canonical_form_agree() {
let go = serde_json::json!({ "v": 1, "signer": head::signing::GO_AHEAD_SIGNER_MONETIZE, "target_sectors": 41_943_040u64, "nonce": "n-1", "issued_unix_ms": 1_792_000_000_000i64, "signature": "" });
let h: head::signing::GoAhead = serde_json::from_value(go.clone()).unwrap();
let p: published::signing::GoAhead =
serde_json::from_value(serde_json::to_value(&h).unwrap()).unwrap();
assert_eq!(
head::signing::go_ahead_message(&h),
published::signing::go_ahead_message(&p),
"go-ahead form drifted"
);
assert_eq!(
head::signing::GO_AHEAD_SIGNER_MONETIZE,
published::signing::GO_AHEAD_SIGNER_MONETIZE
);
let v = serde_json::json!({ "b": [3, {"z": 1, "a": "x"}], "a": null, "c": "\u{e5}\"\n" });
let (mut hs, mut ps) = (String::new(), String::new());
head::signing::canonical_json(&v, &mut hs);
published::signing::canonical_json(&v, &mut ps);
assert_eq!(hs, ps, "canonical JSON drifted");
let bytes = [0u8, 1, 2, 250, 251, 252, 253, 254, 255];
assert_eq!(
head::signing::base64_encode(&bytes),
published::signing::base64_encode(&bytes),
"the signature alphabet drifted"
);
}