crates/gunnar-server/tests/session_restart.rs
//! **GUN-SESSION-RESTART** (owner 2026-11-01, after the UpCloud restart): a person's
//! console session lives through a gunnar-server restart. Over the REAL stores and the
//! identity core built as the server builds it (`identity::try_build` on the data root): a
//! passkey login's session is still the same person after every store is closed and the
//! core is built again from `identity.json` — which holds the session's hash and expiry,
//! never the token.
use std::sync::Arc;
use gunnar_grpc::{ControlPlane, ServerConfig};
use gunnar_proto::*;
use gunnar_server::control_plane::CoreControlPlane;
use gunnar_server::stores::ServerStores;
use nornir_identity::test_host::SoftPasskey;
use nornir_identity::Relay;
const RP_ID: &str = "console.example";
const ORIGIN: &str = "https://console.example";
fn config() -> ServerConfig {
ServerConfig {
web_relying_parties: vec![format!("{RP_ID}={ORIGIN}")
.parse()
.expect("the relying party")],
..ServerConfig::default()
}
}
#[test]
fn a_session_lives_through_a_server_restart_and_only_its_hash_is_on_disk() {
let dir = tempfile::tempdir().expect("tempdir");
let mut key = SoftPasskey::new(31);
let token = {
let stores = Arc::new(ServerStores::open(dir.path()).expect("stores"));
let plane = Arc::new(CoreControlPlane::new(Arc::clone(&stores)));
plane
.add_principal("ada", "ada", PrincipalKind::User)
.expect("the person");
plane
.add_credential("ada", &key.id, &key.public_key(), "ada's passkey", 0)
.expect("the passkey");
let control: Arc<dyn ControlPlane> = plane;
let identity = gunnar_server::identity::try_build(dir.path(), control, &config(), false)
.expect("the core");
let c = identity
.login_start(&Relay {
rp_id: RP_ID.into(),
origin: ORIGIN.into(),
client: None,
})
.expect("a ceremony");
let made = identity
.login_finish(key.login(&c, ORIGIN, true))
.expect("a login");
assert_eq!(identity.session(&made.token).expect("live").user_id, "ada");
made.token
// Every store and the core are dropped here: the server stopped.
};
let on_disk = std::fs::read_to_string(dir.path().join(gunnar_server::identity::FILE))
.expect("identity.json");
assert!(
!on_disk.contains(&token),
"the token is never on disk, only its hash"
);
assert!(
on_disk.contains("\"expires\""),
"the session's expiry is kept: {on_disk}"
);
let stores = Arc::new(ServerStores::open(dir.path()).expect("the stores again"));
let control: Arc<dyn ControlPlane> = Arc::new(CoreControlPlane::new(Arc::clone(&stores)));
let identity = gunnar_server::identity::try_build(dir.path(), control, &config(), false)
.expect("the core again");
let who = identity
.session(&token)
.expect("the same session after the restart");
assert_eq!(who.user_id, "ada");
assert!(
identity.session("nis1.forged").is_err(),
"a token it never minted is nobody"
);
}