use std::sync::Arc;
use super::tests_common::*;
use super::*;
const DAY: u64 = 86_400;
fn put(fx: &Fx, g: &SignedGrant) -> Result<Outcome, LicenceError> {
fx.store.accept_grant(g)
}
fn covered(fx: &Fx, arch: &str) -> bool {
fx.store.valid_grant_covering(&b3_of(arch), "fall-detect", "1.2.0").is_some()
}
fn open_grants(dir: &std::path::Path) -> Result<CheckoutGrantStore, LicenceError> {
CheckoutGrantStore::open(dir, anchors(), LocalMeshId::new(mesh()), system_clock())
}
#[test]
fn a_same_seq_conflict_does_not_undo_a_withdrawal() {
let fx = Fx::new();
fx.bind();
put(&fx, &grant(1, T0, DAY, &["aarch64"])).unwrap();
put(&fx, &grant(2, T0, DAY, &["aarch64"])).unwrap();
put(&fx, &grant(3, T0 + 10, 0, &["aarch64"])).unwrap(); assert!(!covered(&fx, "aarch64"));
let rival = grant(3, T0 + 11, DAY, &["aarch64"]);
assert_eq!(put(&fx, &rival), Err(LicenceError::Conflict(3)));
assert!(!covered(&fx, "aarch64"), "the withdrawal tombstone stays");
assert_eq!(put(&fx, &grant(2, T0, DAY, &["aarch64"])), Ok(Outcome::Ignored));
assert!(!covered(&fx, "aarch64"));
}
#[test]
fn a_rival_withdrawal_at_a_live_grants_seq_wins_the_conflict() {
let fx = Fx::new();
fx.bind();
put(&fx, &grant(1, T0, DAY, &["aarch64"])).unwrap();
put(&fx, &grant(2, T0, DAY, &["aarch64"])).unwrap();
let rival = grant(2, T0 + 10, 0, &["aarch64"]);
assert_eq!(put(&fx, &rival), Err(LicenceError::Conflict(2)));
assert!(!covered(&fx, "aarch64"), "fail closed, not back to seq 1");
}
fn block_saves(fx: &Fx) {
let p = fx.dir.path().join("checkout_grants.json");
std::fs::remove_file(&p).unwrap();
std::fs::create_dir(&p).unwrap(); }
fn unblock_saves(fx: &Fx) {
std::fs::remove_dir(fx.dir.path().join("checkout_grants.json")).unwrap();
}
#[test]
fn a_failed_save_leaves_memory_unchanged() {
let fx = Fx::new();
fx.bind();
block_saves(&fx);
let g = grant(1, T0, DAY, &["aarch64"]);
assert!(matches!(put(&fx, &g), Err(LicenceError::Persist(_))));
assert!(!covered(&fx, "aarch64"));
let rebind = binding_rec(2, BindState::Bound, &sk(40), &mesh());
let r = fx.store.accept_binding(
&sign_binding(&rebind, &op()).unwrap(),
posture(),
&NoExtraChecks,
);
assert!(matches!(r, Err(LicenceError::Persist(_))));
assert_eq!(fx.store.active_binding().unwrap().grant_pubkey, pk_hex(&grant_key()));
unblock_saves(&fx);
assert_eq!(put(&fx, &g), Ok(Outcome::Applied), "not a duplicate: nothing was kept");
}
#[test]
fn restrictive_records_apply_even_when_the_save_fails_and_tick_retries() {
let mut fx = Fx::new();
fx.bind();
put(&fx, &grant(1, T0, DAY, &["aarch64"])).unwrap();
block_saves(&fx);
let w = grant(2, T0 + 10, 0, &["aarch64"]);
assert_eq!(put(&fx, &w), Ok(Outcome::AppliedUnsaved));
assert!(!covered(&fx, "aarch64"), "a disk fault must not keep a withdrawn grant alive");
unblock_saves(&fx);
fx.store.tick(); fx.restart();
assert!(!covered(&fx, "aarch64"));
}
#[test]
fn reads_never_write_the_file() {
let fx = Fx::new();
fx.bind();
put(&fx, &grant(1, T0, DAY, &["aarch64"])).unwrap();
let p = fx.dir.path().join("checkout_grants.json");
std::fs::remove_file(&p).unwrap();
fx.set_now(T0 + 3600);
assert!(covered(&fx, "aarch64"));
let _ = fx.store.floor();
let _ = fx.store.verified_grants();
assert!(!p.exists(), "only tick() persists the high-water mark");
fx.store.tick();
assert!(p.exists());
}
#[test]
fn a_binding_naming_a_trust_anchor_as_grant_or_steward_key_is_refused() {
let fx = Fx::new();
for anchor in [op(), sk(5)] {
let mut r = binding_rec(1, BindState::Bound, &grant_key(), &mesh());
r.grant_pubkey = pk_hex(&anchor);
let s = sign_binding(&r, &op()).unwrap();
let got = fx.store.accept_binding(&s, posture(), &NoExtraChecks);
assert!(matches!(got, Err(LicenceError::Malformed(_))), "grant key");
let mut r = binding_rec(1, BindState::Bound, &grant_key(), &mesh());
r.steward_pubkey = pk_hex(&anchor);
let s = sign_binding(&r, &op()).unwrap();
let got = fx.store.accept_binding(&s, posture(), &NoExtraChecks);
assert!(matches!(got, Err(LicenceError::Malformed(_))), "steward key");
}
}
#[test]
fn grant_shape_bounds_are_enforced() {
let pk = grant_key().verifying_key().to_bytes();
let check = |f: &dyn Fn(&mut CheckoutGrant)| {
let mut r = grant_rec(1, T0, DAY, &["aarch64", "x86_64"]);
f(&mut r);
verify_grant(&sign_grant(&r, &grant_key()).unwrap(), &pk, &mesh())
};
assert!(check(&|_| {}).is_ok());
let dup_sha = check(&|r| r.artifacts[1].sha256 = r.artifacts[0].sha256.clone());
assert!(matches!(dup_sha, Err(LicenceError::Malformed(_))));
let dup_b3 = check(&|r| r.artifacts[1].blake3 = r.artifacts[0].blake3.clone());
assert!(matches!(dup_b3, Err(LicenceError::Malformed(_))));
assert!(check(&|r| r.artifacts[0].size = 0).is_err());
assert!(check(&|r| r.artifacts[0].size = MAX_ARTIFACT_BYTES + 1).is_err());
assert!(check(&|r| r.registry = "reg\u{7f}istry".into()).is_err());
assert!(check(&|r| r.registry = "r\u{e9}gistry".into()).is_err());
assert!(check(&|r| r.licence.expires = MAX_UNIX_TIME + 1).is_err());
assert!(check(&|r| r.licence.expires = 0).is_err());
}
#[test]
fn a_tampered_floors_map_fails_the_load_and_old_key_floors_are_pruned() {
let fx = Fx::new();
fx.bind();
put(&fx, &grant(1, T0, DAY, &["aarch64"])).unwrap();
let path = fx.dir.path().join("checkout_grants.json");
let mut v: serde_json::Value = serde_json::from_slice(&std::fs::read(&path).unwrap()).unwrap();
v["floors"][pk_hex(&sk(41))] = serde_json::json!({"max_issued": 1, "hw": 1});
std::fs::write(&path, serde_json::to_vec(&v).unwrap()).unwrap();
assert!(matches!(open_grants(fx.dir.path()), Err(LicenceError::Malformed(_))));
v["floors"].as_object_mut().unwrap().remove(&pk_hex(&sk(41)));
v["floors"][pk_hex(&grant_key())]["hw"] = serde_json::json!(MAX_UNIX_TIME + 1);
std::fs::write(&path, serde_json::to_vec(&v).unwrap()).unwrap();
assert!(open_grants(fx.dir.path()).is_err());
let fx = Fx::new();
fx.bind();
put(&fx, &grant(1, T0, DAY, &["aarch64"])).unwrap();
let r = binding_rec(2, BindState::Bound, &sk(40), &mesh());
fx.store.accept_binding(&sign_binding(&r, &op()).unwrap(), posture(), &NoExtraChecks).unwrap();
let v: serde_json::Value = serde_json::from_slice(
&std::fs::read(fx.dir.path().join("checkout_grants.json")).unwrap(),
)
.unwrap();
assert!(v["floors"].as_object().unwrap().keys().all(|k| *k == pk_hex(&sk(40))));
}
#[test]
fn events_are_emitted_after_the_lock_is_released() {
struct Reenter(std::sync::Mutex<Option<Arc<CheckoutGrantStore>>>);
impl LicenceEventSink for Reenter {
fn emit(&self, _: LicenceEvent) {
if let Some(s) = self.0.lock().unwrap().as_ref() {
let _ = s.active_binding();
}
}
}
let fx = Fx::new();
let sink = Arc::new(Reenter(Default::default()));
*sink.0.lock().unwrap() = Some(fx.store.clone());
fx.store.set_sink(sink);
let open = AdmissionPosture { open_membership: true, ..posture() };
let _ = fx.store.accept_binding(&binding(1, BindState::Bound), open, &NoExtraChecks);
}
#[test]
fn an_unsaved_unbind_is_retried_by_tick_even_though_no_binding_is_in_effect() {
let mut fx = Fx::new();
fx.bind();
put(&fx, &grant(1, T0, DAY, &["aarch64"])).unwrap();
block_saves(&fx);
let unbind = binding(2, BindState::Unbound);
let r = fx.store.accept_binding(&unbind, posture(), &NoExtraChecks);
assert_eq!(r, Ok(Outcome::AppliedUnsaved));
assert!(!covered(&fx, "aarch64"), "applied in memory");
unblock_saves(&fx);
fx.store.tick();
fx.restart();
assert_eq!(fx.store.binding_status(), Err(LicenceError::Unbound), "the unbind reached disk");
}