crates/gunnar-grpc/tests/identity_conformance.rs
//! **The identity conformance suite against Gunnar** (IDENTITY-HOST): the same
//! cases edda's TestHost passes, over Gunnar's control plane and tables, and
//! the reference transcript, so Gunnar answers as every other product does.
use std::sync::{Arc, Mutex};
use gunnar_grpc::identity::{
Clock, GunnarHost, GunnarStore, IdentityTables, SERVER_ADMIN, SERVER_WRITE,
};
use gunnar_grpc::MemoryControlPlane;
use nornir_identity::conformance::{self, Harness};
use nornir_identity::test_host::Kit;
use nornir_identity::Site;
struct GunnarHarness;
fn kit_clock(kit: &Kit) -> Clock {
let clock = Arc::clone(&kit.clock);
Arc::new(move || clock.now_ms())
}
impl Harness for GunnarHarness {
type Host = GunnarHost;
fn host(&self, site: Site, kit: &Kit) -> GunnarHost {
let plane = Arc::new(MemoryControlPlane::new());
// Every change is persisted and must read back as what was written.
let persist: gunnar_grpc::identity::Persist =
Box::new(|json: &str| GunnarStore::read_tables(json).map(|_| ()));
let store = GunnarStore::new(
plane,
IdentityTables::default(),
Some(persist),
kit_clock(kit),
);
GunnarHost::new(site, store, kit.mail.clone(), kit_clock(kit))
}
fn admin_groups(&self) -> Vec<String> {
vec![SERVER_ADMIN.to_owned()]
}
fn member_groups(&self) -> Vec<String> {
vec![SERVER_WRITE.to_owned()]
}
}
nornir_identity::conformance_suite!(GunnarHarness);
/// A session, an invite and a floor survive a restart: the server reads the
/// tables it persisted, over the same control plane.
#[test]
fn the_tables_survive_a_restart() {
let kit = Kit::default();
let plane = Arc::new(MemoryControlPlane::new());
let saved = Arc::new(Mutex::new(String::new()));
let keep = Arc::clone(&saved);
let persist: gunnar_grpc::identity::Persist = Box::new(move |json: &str| {
*keep.lock().unwrap() = json.to_owned();
Ok(())
});
let store = GunnarStore::new(
plane.clone(),
IdentityTables::default(),
Some(persist),
kit_clock(&kit),
);
let id = nornir_identity::Identity::new(
GunnarHost::new(Site::Private, store, kit.mail.clone(), kit_clock(&kit)),
conformance::config(),
);
let (admin, _) = conformance::first_admin(&GunnarHarness, &id);
let json = saved.lock().unwrap().clone();
assert!(
!json.contains(&admin.token),
"the token is never persisted, only its hash"
);
let tables = GunnarStore::read_tables(&json).unwrap();
let again = GunnarStore::new(plane, tables, None, kit_clock(&kit));
let id = nornir_identity::Identity::new(
GunnarHost::new(Site::Private, again, kit.mail.clone(), kit_clock(&kit)),
conformance::config(),
);
let who = id
.session(&admin.token)
.expect("the session after a restart");
assert!(
who.admin,
"the first admin holds Gunnar's server-wide Admin grant"
);
}
/// ★ **RA5-36: a change whose persist fails changes nothing.** `GunnarStore::change` changed
/// the tables in memory before `identity.json` was written and kept that on a failed write: a
/// logout (or an end-all) answered Err while memory had already dropped the session, and the
/// disk copy brought it back after a restart (a stolen token alive again). Memory and disk
/// now agree: the failed logout ended nothing anywhere, and the person can log out again.
#[test]
fn a_change_whose_persist_fails_changes_nothing() {
use std::sync::atomic::{AtomicBool, Ordering};
let kit = Kit::default();
let plane = Arc::new(MemoryControlPlane::new());
let saved = Arc::new(Mutex::new(String::new()));
let fail = Arc::new(AtomicBool::new(false));
let (keep, failing) = (Arc::clone(&saved), Arc::clone(&fail));
let persist: gunnar_grpc::identity::Persist = Box::new(move |json: &str| {
if failing.load(Ordering::SeqCst) {
return Err("the disk is full".to_owned());
}
*keep.lock().unwrap() = json.to_owned();
Ok(())
});
let store = GunnarStore::new(
plane,
IdentityTables::default(),
Some(persist),
kit_clock(&kit),
);
let id = nornir_identity::Identity::new(
GunnarHost::new(Site::Private, store, kit.mail.clone(), kit_clock(&kit)),
conformance::config(),
);
let (admin, _) = conformance::first_admin(&GunnarHarness, &id);
fail.store(true, Ordering::SeqCst);
assert!(
id.logout(&admin.token, false).is_err(),
"the logout's write failed"
);
let on_disk = GunnarStore::read_tables(&saved.lock().unwrap()).unwrap();
let in_memory = id.host().gunnar_store().tables();
assert_eq!(
in_memory.sessions.len(),
on_disk.sessions.len(),
"memory and identity.json agree after a failed write"
);
fail.store(false, Ordering::SeqCst);
id.logout(&admin.token, false).expect("the logout, again");
assert!(id.session(&admin.token).is_err());
}