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

nordisk/holger

server/lib/tests/grpc_door_refuses_a_dead_address.rs

//! **A gRPC door that cannot open REFUSES THE START — it does not print
//! "listening" and carry on.**
//!
//! MEASURED 2026-09-21, lane H2, against the shipped 2026-09-20 binary with
//! another process already on the declared port:
//!
//! ```text
//! Starting Holger "…/holger.ron"
//! gRPC server listening on 0.0.0.0:50051 [h2c (cleartext)] (gRPC-web enabled)
//! Holger is running. Press Ctrl+C to stop.
//! gRPC server error: transport error
//! ```
//!
//! Three claims and one truth: the door never opened, and the process stayed up
//! answering `/health` with `status=ok` and nothing on its product door. The
//! "listening" line was printed BEFORE the listener existed
//! (`server/lib/src/lib.rs`) and the bind error was swallowed by an `eprintln!`
//! inside a spawned task nobody joins.
//!
//! Both tests below are RED before that commit: the first because `start`
//! returned `Ok(())`, the second because there was no named refusal to read.

use std::net::TcpListener;

use holger_server_lib::{read_ron_config, wire_holger};

/// Take a real port, then declare it in the RON — the exact collision the
/// measurement above hit, with no sleep and no guessing.
#[test]
fn an_address_already_taken_is_returned_from_start_not_printed_and_survived() {
    let squatter = TcpListener::bind("127.0.0.1:0").expect("take a port");
    let taken = squatter.local_addr().unwrap();

    let tmp = tempfile::tempdir().unwrap();
    let ron = format!(
        r#"(
    repositories: [
        ( ron_name: "bundles", ron_repo_type: "raw", ron_upstreams: [], ron_in: None,
          ron_out: Some(( ron_storage_endpoint: "store", ron_exposed_endpoint: "doors" )),
          ron_data_dir: Some("{store}/bundles") ),
    ],
    exposed_endpoints: [ ( ron_name: "doors", ron_url: "{taken}" ) ],
    storage_endpoints: [ ( ron_name: "store", ron_storage_type: "file", ron_path: "{store}" ) ],
    audit: (dir: "{store}/audit", enabled: true),
)"#,
        store = tmp.path().display(),
    );
    let path = tmp.path().join("h.ron");
    std::fs::write(&path, &ron).unwrap();
    let mut holger = read_ron_config(&path).expect("the RON parses");
    holger
        .instantiate_backends()
        .expect("the raw backend stands up");
    wire_holger(&mut holger).expect("the repository is wired to the door");

    // `start` spawns onto a runtime, so it needs one to be entered.
    let rt = tokio::runtime::Builder::new_current_thread()
        .enable_all()
        .build()
        .unwrap();
    let verdict = rt.block_on(async { holger.start() });

    let err = match verdict {
        Ok(()) => panic!(
            "holger started on {taken}, which is already taken — the door cannot have opened, \
             so a server that reports itself started is reporting something that did not happen"
        ),
        Err(e) => format!("{e:#}"),
    };
    assert!(
        err.contains(&taken.to_string()),
        "the address must be named: {err}"
    );
    assert!(
        err.contains("REFUSING TO START"),
        "the verdict must be unambiguous: {err}"
    );
    drop(squatter);
}

/// **Not vacuous**: a free address starts. Without this the test above would
/// pass just as well against a holger that refused every address.
#[test]
fn a_free_address_starts() {
    let tmp = tempfile::tempdir().unwrap();
    let ron = format!(
        r#"(
    repositories: [
        ( ron_name: "bundles", ron_repo_type: "raw", ron_upstreams: [], ron_in: None,
          ron_out: Some(( ron_storage_endpoint: "store", ron_exposed_endpoint: "doors" )),
          ron_data_dir: Some("{store}/bundles") ),
    ],
    exposed_endpoints: [ ( ron_name: "doors", ron_url: "127.0.0.1:0" ) ],
    storage_endpoints: [ ( ron_name: "store", ron_storage_type: "file", ron_path: "{store}" ) ],
    audit: (dir: "{store}/audit", enabled: true),
)"#,
        store = tmp.path().display(),
    );
    let path = tmp.path().join("h.ron");
    std::fs::write(&path, &ron).unwrap();
    let mut holger = read_ron_config(&path).expect("the RON parses");
    holger
        .instantiate_backends()
        .expect("the raw backend stands up");
    wire_holger(&mut holger).expect("the repository is wired to the door");
    let rt = tokio::runtime::Builder::new_current_thread()
        .enable_all()
        .build()
        .unwrap();
    rt.block_on(async { holger.start() })
        .expect("a free address opens");
}

/// The residual path — the address taken between the proof and the real bind,
/// or a serve loop that dies later — ends the process, so its words are read
/// here instead. A frozen code, the address, and why ending is the answer.
#[test]
fn the_dead_door_refusal_names_the_code_the_address_and_the_consequence() {
    let addr: std::net::SocketAddr = "10.1.2.3:50051".parse().unwrap();
    let msg = holger_server_lib::grpc_door_died(&addr.to_string(), &"transport error");
    assert!(msg.contains("holger-door-1"), "frozen code: {msg}");
    assert!(msg.contains("10.1.2.3:50051"), "the address: {msg}");
    assert!(msg.contains("transport error"), "the cause: {msg}");
    assert!(msg.contains("ENDING"), "the consequence: {msg}");
}