use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{mpsc, Arc, Mutex};
use std::time::{Duration, Instant};
use gunnar_grpc::auth::{AuthFailure, AuthGate, MAX_CLOCK_SKEW_MS};
use gunnar_grpc::{
sign_credential_for, AuthMode, ControlPlane, MemoryControlPlane, CREDENTIAL_METADATA_KEY,
};
fn now_ms() -> i64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("after 1970")
.as_millis() as i64
}
fn metadata_for(
key: &ssh_key::PrivateKey,
issued_at_ms: i64,
nonce: &str,
) -> tonic::metadata::MetadataMap {
let credential = sign_credential_for(key, issued_at_ms, nonce, "Admin.Health").expect("sign");
let mut metadata = tonic::metadata::MetadataMap::new();
metadata.insert(
CREDENTIAL_METADATA_KEY,
credential.to_header_value().parse().unwrap(),
);
metadata
}
#[test]
fn stale_refusals_start_one_background_sync_a_minute_carry_nothing_and_are_not_delayed() {
let plane: Arc<dyn ControlPlane> = Arc::new(MemoryControlPlane::with_repo("nordisk", "gunnar"));
let gate = AuthGate::new(plane, AuthMode::Required);
let stranger = ssh_key::PrivateKey::random(
&mut rand::rand_core::UnwrapErr(rand::rngs::SysRng),
ssh_key::Algorithm::Ed25519,
)
.expect("mint");
let syncs = Arc::new(AtomicUsize::new(0));
let (release, gate_rx) = mpsc::channel::<()>();
let gate_rx = Mutex::new(gate_rx);
let counter = Arc::clone(&syncs);
estate_clock::trigger::install_skew_handler(move || {
counter.fetch_add(1, Ordering::SeqCst);
let _ = gate_rx
.lock()
.unwrap()
.recv_timeout(Duration::from_secs(10));
});
let suppressed_before = estate_clock::trigger::suppressed();
let started = Instant::now();
let refused = gate.authenticate_bound(
&metadata_for(&stranger, now_ms() + 7_200_000, "nonce-clock-00001"),
now_ms(),
Some("Admin.Health"),
);
let took = started.elapsed();
assert!(
matches!(refused, Err(AuthFailure::Stale { .. })),
"{refused:?}"
);
assert!(
took < Duration::from_millis(500),
"the refusal waited {took:?} — the sync (still blocked) must not delay it"
);
let deadline = Instant::now() + Duration::from_secs(5);
while syncs.load(Ordering::SeqCst) == 0 && Instant::now() < deadline {
std::thread::sleep(Duration::from_millis(5));
}
assert_eq!(
syncs.load(Ordering::SeqCst),
1,
"a stale refusal starts exactly one sync"
);
for (i, issued) in [
i64::MIN / 2,
0,
now_ms() - MAX_CLOCK_SKEW_MS - 1,
now_ms() + 86_400_000,
]
.into_iter()
.cycle()
.take(12)
.enumerate()
{
let refused = gate.authenticate_bound(
&metadata_for(&stranger, issued, &format!("nonce-burst-{i:06}")),
now_ms(),
Some("Admin.Health"),
);
assert!(
matches!(refused, Err(AuthFailure::Stale { .. })),
"{refused:?}"
);
}
std::thread::sleep(Duration::from_millis(50));
assert_eq!(
syncs.load(Ordering::SeqCst),
1,
"a burst inside 60 s starts no second sync"
);
assert_eq!(estate_clock::trigger::suppressed() - suppressed_before, 12);
let _ = gate.authenticate_bound(
&metadata_for(&stranger, now_ms(), "nonce-fresh-00001"),
now_ms(),
Some("Admin.Health"),
);
assert_eq!(estate_clock::trigger::suppressed() - suppressed_before, 12);
let _ = release.send(());
}