Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/gunnar

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"
    );
}