use base64::Engine;
use ed25519_dalek::{Signer, SigningKey};
use keylight::http::{HttpResponse, Transport, TransportOutcome};
use keylight::store::device::FixedDeviceIdentity;
use keylight::store::encrypted_file::EncryptedFileStore;
use keylight::store::{account, LicenseStore};
use keylight::{Keylight, KeylightConfig, LicenseLifecycleEvent};
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, Mutex};
const KID: &str = "k1";
fn now() -> i64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs() as i64
}
fn signed_lease(signing: &SigningKey, status: &str, expires_at: i64) -> String {
let ents = ["pro"]; let payload = format!(
"v3|{KID}|hash|i1|0|{expires_at}|{status}|{}",
ents.join(",")
);
let sig = signing.sign(payload.as_bytes());
let sig_b64 = base64::engine::general_purpose::STANDARD.encode(sig.to_bytes());
serde_json::json!({
"kid": KID, "licenseKeyHash": "hash", "instanceId": "i1",
"issuedAt": 0, "expiresAt": expires_at, "status": status,
"signature": sig_b64, "entitlements": ents,
})
.to_string()
}
struct Scripted {
bodies: Vec<String>,
idx: AtomicUsize,
}
impl Transport for Scripted {
fn post_json(&self, _: &str, _: &[(String, String)], _: &str) -> TransportOutcome {
let i = self
.idx
.fetch_add(1, Ordering::SeqCst)
.min(self.bodies.len() - 1);
TransportOutcome::Response(HttpResponse {
status: 200,
body: self.bodies[i].clone(),
retry_after: None,
})
}
fn get(&self, _: &str, _: &[(String, String)]) -> TransportOutcome {
TransportOutcome::Response(HttpResponse {
status: 200,
body: "{}".into(),
retry_after: None,
})
}
}
#[test]
fn validate_fires_cancelled_on_licensed_to_limited() {
let signing = SigningKey::from_bytes(&[7u8; 32]);
let pub_b64 =
base64::engine::general_purpose::STANDARD.encode(signing.verifying_key().to_bytes());
let exp = now() + 100_000; let active = format!(
r#"{{"valid":true,"lease":{}}}"#,
signed_lease(&signing, "active", exp)
);
let fallback = format!(
r#"{{"valid":false,"lease":{}}}"#,
signed_lease(&signing, "fallback", exp)
);
let dir = std::env::temp_dir().join("kl-lifecycle-e2e");
let _ = std::fs::remove_dir_all(&dir);
let store =
Arc::new(EncryptedFileStore::at_dir(dir, &FixedDeviceIdentity("dev".into())).unwrap());
store.set_string(account::LICENSE_KEY, "PRO-KEY").unwrap();
store.set_string(account::INSTANCE_ID, "i1").unwrap();
let cfg = KeylightConfig::builder("t", "p")
.trusted_key(KID, pub_b64)
.build();
let transport = Arc::new(Scripted {
bodies: vec![active, fallback],
idx: AtomicUsize::new(0),
});
let events: Arc<Mutex<Vec<LicenseLifecycleEvent>>> = Arc::new(Mutex::new(Vec::new()));
let sink = events.clone();
let kl = Keylight::with_parts(cfg, store, transport)
.with_event_handler(move |ev| sink.lock().unwrap().push(ev));
let r1 = kl.validate().unwrap();
assert!(r1.valid);
assert!(kl.has_entitlement("pro"));
let r2 = kl.validate().unwrap();
assert!(!r2.valid);
let got = events.lock().unwrap().clone();
assert!(
got.contains(&LicenseLifecycleEvent::Cancelled),
"expected Cancelled in fired events, got {got:?}"
);
assert!(
got.contains(&LicenseLifecycleEvent::Restored),
"expected Restored (Expired->Licensed) in fired events, got {got:?}"
);
}