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