gunnar-ssh 3.0.0

gunnar's SSH server: a russh listener with publickey identity, a strict git exec dispatch, and the repository path sandbox.
Documentation
//! **The boot window**: what a client is told between the socket answering and
//! the stores being usable.
//!
//! # What this is written against — MEASURED 2026-08-30
//!
//! After a power cut the appliance answered ssh :2222 and grpc :50051
//! **0.6–0.7 s before its stores had finished their post-crash open**. A read in
//! that window came back `the repository is unavailable; try again` — which is
//! `gunnar_wire::Error::Store`'s sentence, arrives with `EXIT_DENIED`, and is
//! also what a client hears when a repository is genuinely broken. Nothing
//! could tell *not yet* from *gone*: the first power-cut door reported
//! **64 acked pushes missing** that a boot one second later found every one of,
//! and the same window wedged a whole bench run's clone and fetch phases.
//!
//! So the assertions here are about **words and an exit status**, not about
//! timing:
//!
//! 1. while the flag is clear, a git exec is answered with an `ERR` line
//!    containing `starting` and with [`EXIT_BUSY`] (75, `EX_TEMPFAIL` — retry);
//! 2. it does **not** say `unavailable`, which is the sentence that means gone;
//! 3. nothing is served — the transport is never handed the request;
//! 4. the instant the flag is set, the very same command is served, with no
//!    reconnection and no new session.
//!
//! # Seen red
//!
//! Run against a `GunnarSsh` built without `with_readiness` (which is ready by
//! construction) every assertion in `a_transfer_in_the_boot_window_is_told_to_retry`
//! fails: the banner arrives instead of the `ERR`, no exit status is sent, and
//! `served()` holds the request.

mod common;

use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;

use common::*;
use gunnar_ssh::{Access, EXIT_BUSY, STARTING_MESSAGE};

const UPLOAD: &str = "git-upload-pack 'nordisk/osm-katana.git'";

#[tokio::test]
async fn a_transfer_in_the_boot_window_is_told_to_retry_and_then_served() {
    let key = mint_key();
    let ready = Arc::new(AtomicBool::new(false));
    let fixture = start_with_readiness(&key, Access::Admin, Arc::clone(&ready)).await;
    let session = authenticate(fixture.server.local_addr(), &key, "git")
        .await
        .expect("connect")
        .expect("authenticate");

    let refused = exec(&session, UPLOAD).await.expect("exec");
    let (stdout, status) = match refused {
        ExecOutcome::Accepted {
            stdout,
            exit_status,
        } => (stdout, exit_status),
        other => panic!(
            "the boot window must ANSWER, in git's protocol, not drop the channel: {other:?}"
        ),
    };

    assert!(
        stdout.contains("starting"),
        "the word `starting` is what a script keys on to tell `not yet` from `gone`: {stdout:?}"
    );
    assert!(
        stdout.contains(STARTING_MESSAGE),
        "the sentence is the constant, so the server and its documentation cannot drift: {stdout:?}"
    );
    assert!(
        !stdout.contains("unavailable"),
        "`unavailable` is what a client is told about a repository that is GONE. Saying it here \
         is the defect that made the door report 64 present pushes as missing: {stdout:?}"
    );
    assert_eq!(
        status,
        Some(EXIT_BUSY),
        "EX_TEMPFAIL (75), not 128. A retry script has to be able to tell a booting server from \
         a permission error"
    );
    assert!(
        fixture.transport.served().is_empty(),
        "nothing may be SERVED in the boot window: the whole point is that the store is not \
         ready to be read, so a request that reaches it is the bug"
    );

    // The stores finish opening. No reconnection, no new session: the very next
    // exec on the SAME connection is served.
    ready.store(true, Ordering::Relaxed);
    let served = exec(&session, UPLOAD).await.expect("exec");
    match served {
        ExecOutcome::Accepted { stdout, .. } => assert!(
            stdout.contains("git-upload-pack"),
            "a ready server serves: {stdout:?}"
        ),
        other => panic!("the same command must be served once the flag is set: {other:?}"),
    }
    assert_eq!(
        fixture.transport.served().len(),
        1,
        "exactly the one transfer that arrived after readiness"
    );
}

/// A listener nobody handed a flag to is **ready**, and that is the safe
/// default rather than an oversight: every integration test in this workspace
/// builds one, and a default of "not ready" would make them all hang on a boot
/// that never happened.
#[tokio::test]
async fn a_listener_with_no_readiness_flag_serves_at_once() {
    let key = mint_key();
    let fixture = start_with_key(&key, Access::Admin).await;
    let session = authenticate(fixture.server.local_addr(), &key, "git")
        .await
        .expect("connect")
        .expect("authenticate");
    match exec(&session, UPLOAD).await.expect("exec") {
        ExecOutcome::Accepted { stdout, .. } => assert!(stdout.contains("git-upload-pack")),
        other => panic!("a server with no boot window serves from its first socket: {other:?}"),
    }
}

/// **A KEYED caller is queued through the boot window, not refused.**
///
/// gunnar.rs, 2026-10-25: a release restarts the main, and every CI lane is a
/// key. git-lfs's pure-SSH `git-lfs-transfer` cannot retry a `starting`
/// refusal — it falls back to `git-lfs-authenticate` and fails the push. So
/// with a keyed wait configured (30 s on the appliance) the key's exec waits
/// for the stores and is then SERVED, on its first try.
#[tokio::test]
async fn a_keyed_transfer_in_the_boot_window_waits_and_is_served() {
    let key = mint_key();
    let ready = Arc::new(AtomicBool::new(false));
    let config = gunnar_ssh::SshConfig {
        keyed_admission_wait: Some(std::time::Duration::from_secs(10)),
        ..gunnar_ssh::SshConfig::ephemeral_loopback()
    };
    let fixture =
        common::start_with_readiness_and_config(&key, Access::Admin, Arc::clone(&ready), config)
            .await;
    let session = authenticate(fixture.server.local_addr(), &key, "git")
        .await
        .expect("connect")
        .expect("authenticate");

    let flag = Arc::clone(&ready);
    let opener = tokio::spawn(async move {
        tokio::time::sleep(std::time::Duration::from_millis(700)).await;
        flag.store(true, Ordering::Relaxed);
    });
    let started = std::time::Instant::now();
    match exec(&session, UPLOAD).await.expect("exec") {
        ExecOutcome::Accepted { stdout, .. } => {
            assert!(
                !stdout.contains("starting"),
                "a key was REFUSED in the boot window although it may wait 10 s and the stores \
                 opened after 0.7 s: {stdout:?}"
            );
            assert!(
                stdout.contains("git-upload-pack"),
                "served once ready: {stdout:?}"
            );
        }
        other => panic!("the keyed exec must be served: {other:?}"),
    }
    assert!(
        started.elapsed() >= std::time::Duration::from_millis(600),
        "it waited for the flag"
    );
    assert_eq!(fixture.transport.served().len(), 1);
    opener.await.unwrap();
}