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::{LicenseStore, account};
use keylight::{Keylight, KeylightConfig, LicenseState};
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
const KID: &str = "k1";
fn now() -> i64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs() as i64
}
fn signing_key() -> SigningKey {
SigningKey::from_bytes(&[7u8; 32])
}
fn lease_json(signing: &SigningKey, status: &str, expires_at: i64) -> String {
let payload = format!("v3|{KID}|hash|i1|0|{expires_at}|{status}|pro");
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": ["pro"],
})
.to_string()
}
fn config_trusting(signing: &SigningKey) -> KeylightConfig {
let pub_b64 =
base64::engine::general_purpose::STANDARD.encode(signing.verifying_key().to_bytes());
KeylightConfig::builder("t", "p", "sdk_live_test")
.trusted_key(KID, pub_b64)
.build()
}
fn empty_store(dir: &str) -> Arc<EncryptedFileStore> {
let d = std::env::temp_dir().join(dir);
let _ = std::fs::remove_dir_all(&d);
Arc::new(EncryptedFileStore::at_dir(d, &FixedDeviceIdentity("dev".into())).unwrap())
}
fn store_with_active_lease(dir: &str, signing: &SigningKey) -> Arc<EncryptedFileStore> {
let store = empty_store(dir);
store.set_string(account::LICENSE_KEY, "PRO-KEY").unwrap();
store.set_string(account::INSTANCE_ID, "i1").unwrap();
store
.set_string(
account::LEASE,
&lease_json(signing, "active", now() + 100_000),
)
.unwrap();
store
.set_string(account::LAST_SEEN, &now().to_string())
.unwrap();
store
.set_string(account::LAST_VALIDATED_ONLINE, &now().to_string())
.unwrap();
store
}
struct Counting {
response: Option<(u16, String)>,
calls: AtomicUsize,
}
impl Counting {
fn new(status: u16, body: &str) -> Arc<Self> {
Arc::new(Self {
response: Some((status, body.into())),
calls: AtomicUsize::new(0),
})
}
fn offline() -> Arc<Self> {
Arc::new(Self {
response: None,
calls: AtomicUsize::new(0),
})
}
fn count(&self) -> usize {
self.calls.load(Ordering::SeqCst)
}
fn outcome(&self) -> TransportOutcome {
match &self.response {
Some((status, body)) => TransportOutcome::Response(HttpResponse {
status: *status,
body: body.clone(),
retry_after: None,
}),
None => TransportOutcome::Terminal("offline".into()),
}
}
}
impl Transport for Counting {
fn post_json(&self, _: &str, _: &[(String, String)], _: &str) -> TransportOutcome {
self.calls.fetch_add(1, Ordering::SeqCst);
self.outcome()
}
fn get(&self, _: &str, _: &[(String, String)]) -> TransportOutcome {
self.outcome()
}
}
#[test]
fn no_stored_license_is_a_noop() {
let signing = signing_key();
let store = empty_store("kl-active-noop");
let transport = Counting::new(200, r#"{"valid":true}"#);
let kl = Keylight::with_parts(config_trusting(&signing), store, transport.clone());
assert!(
kl.active_revalidate().is_none(),
"no stored license must return None"
);
assert_eq!(
transport.count(),
0,
"no stored license must not hit the network"
);
}
#[test]
fn forces_a_validate_then_debounces_the_second_call() {
let signing = signing_key();
let store = store_with_active_lease("kl-active-debounce", &signing);
let transport = Counting::new(200, r#"{"valid":true}"#);
let kl = Keylight::with_parts(config_trusting(&signing), store, transport.clone());
assert!(
kl.refresh_if_needed().unwrap().is_none(),
"precondition: refresh_if_needed must skip a just-validated license"
);
assert_eq!(transport.count(), 0, "precondition: no call yet");
let first = kl.active_revalidate();
assert!(
first.is_some_and(|r| r.valid),
"active_revalidate must bypass the staleness gate and validate"
);
assert_eq!(transport.count(), 1);
assert!(
kl.active_revalidate().is_none(),
"a second call inside the 60s window must be suppressed"
);
assert_eq!(
transport.count(),
1,
"the debounced call must not hit the network"
);
}
#[test]
fn debounce_is_in_memory_and_does_not_survive_a_restart() {
let signing = signing_key();
let store = store_with_active_lease("kl-active-restart", &signing);
let transport = Counting::new(200, r#"{"valid":true}"#);
let first = Keylight::with_parts(config_trusting(&signing), store.clone(), transport.clone());
assert!(first.active_revalidate().is_some());
assert!(first.active_revalidate().is_none(), "debounced in-process");
assert_eq!(transport.count(), 1);
drop(first);
let restarted = Keylight::with_parts(config_trusting(&signing), store, transport.clone());
assert!(
restarted.active_revalidate().is_some(),
"the debounce must not be persisted across a restart"
);
assert_eq!(transport.count(), 2);
}
#[test]
fn revoke_downgrades_immediately() {
let signing = signing_key();
let store = store_with_active_lease("kl-active-revoke", &signing);
let transport = Counting::new(
422,
r#"{"valid":false,"reason":"revoked","error":"License revoked"}"#,
);
let kl = Keylight::with_parts(config_trusting(&signing), store.clone(), transport.clone());
assert_eq!(
kl.state(),
LicenseState::Licensed,
"precondition: cached lease starts out valid"
);
let result = kl
.active_revalidate()
.expect("a 422 rejection is an outcome");
assert!(!result.valid, "the revoke must be reported as invalid");
assert_ne!(
kl.state(),
LicenseState::Licensed,
"a revoke must downgrade the live session immediately"
);
assert!(
store.get_string(account::LEASE).is_none(),
"the stale 'active' lease must be cleared"
);
}
#[test]
fn transient_failure_does_not_downgrade() {
let signing = signing_key();
let store = store_with_active_lease("kl-active-transient", &signing);
let lease_before = store.get_string(account::LEASE);
let transport = Counting::offline();
let kl = Keylight::with_parts(config_trusting(&signing), store.clone(), transport.clone());
assert!(
kl.active_revalidate().is_none(),
"a transient failure yields no outcome"
);
assert!(transport.count() >= 1, "it did try");
assert_eq!(
kl.state(),
LicenseState::Licensed,
"a network blip must never downgrade a live session"
);
assert_eq!(
store.get_string(account::LEASE),
lease_before,
"a transient failure must not mutate the cached lease"
);
}