crates/storage-git-gix/parked-gix-2/gunnar-wire-gix/tests/serve_wiring.rs
#![cfg(feature = "server")]
//! The two refusals and the one default that `gunnar serve` is responsible for,
//! plus proof that the gRPC control plane is wired to `gunnar-core` and not to
//! `gunnar-grpc`'s in-memory test double.
//!
//! Each is asserted in **both** directions. "The gRPC bind is loopback" passes
//! against a predicate that returns true for everything, so the SSH bind (which
//! is deliberately public) is asserted not to be. "An empty key table refuses"
//! passes against a server that refuses everything, so the same server with an
//! admin key is asserted to start.
use std::sync::Arc;
use gunnar_cli::server::{ServeConfig, Server, ServerStores};
use gunnar_core::{NewRepo, RepoRef};
fn runtime() -> tokio::runtime::Runtime {
tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.expect("a tokio runtime")
}
// ── D9: the gRPC listener defaults to loopback ──────────────────────────────
/// Goes red if the default moves off loopback.
///
/// The control plane authenticates (`AuthMode::Required` is the default), so
/// this is defence in depth rather than the only lock — but D9's escape hatch
/// `insecure_allow_all_for_local_development` still exists, and a loopback
/// default is what keeps *that* hole local rather than remote. It costs
/// nothing.
///
/// The sentence here used to read "gRPC is unauthenticated in v1 (D9): anyone
/// who can reach the port is an admin of the control plane". That stopped
/// being true and nothing caught it; see
/// `the_started_server_reports_the_auth_mode_it_actually_serves` below, which
/// is the guard for the same staleness on the startup banner.
#[test]
fn the_grpc_default_bind_is_loopback() {
let config = ServeConfig::default();
assert_eq!(
config.grpc_bind,
gunnar_grpc::DEFAULT_BIND,
"gunnar-cli must take gunnar-grpc's default rather than inventing one, or the two \
defaults can drift apart"
);
assert_eq!(config.grpc_bind.to_string(), "127.0.0.1:50051");
assert!(
config.grpc_bind.ip().is_loopback(),
"the unauthenticated control plane must not default to a routable address"
);
// The identity-value guard. `is_loopback()` must be capable of answering
// false on a bind gunnar actually uses, or the assertion above proves
// nothing about the predicate.
assert!(
!config.ssh_bind.ip().is_loopback(),
"SSH authenticates every connection by public key and is meant to bind publicly; if \
this is loopback too then the loopback assertion above is vacuous"
);
}
// ── §5.1, as it now stands: an empty table STARTS, with the door open ───────
/// **What the §5.1 refusal became, and why it had to.**
///
/// Until 2026-08-05 this exact fixture asserted `Server::start` returned an
/// error containing *"refusing to serve"* and *"--admin-key"*. That refusal is
/// gone: it meant a freshly built gunnar could not be reached to be given its
/// first key, so there was no first boot at all, and the only way in was
/// `gunnar key add` with the server DOWN — which needs a shell the appliance
/// does not have.
///
/// The server now starts and announces an OPEN unauthenticated enrolment
/// window. `tests/bootstrap.rs` is where that window's own refusals are
/// asserted; this test's job is the one thing `serve_wiring` owns: that
/// `Server::start` **branches on the readiness value** and says which state it
/// is in, rather than starting silently.
#[test]
fn an_empty_key_table_starts_with_the_enrolment_window_open_and_says_so() {
let dir = tempfile::tempdir().expect("temp dir");
let stores = Arc::new(ServerStores::open(dir.path()).expect("open the stores"));
assert!(
stores.auth().is_empty().expect("read the table"),
"the fixture must start with an empty identity table or it proves nothing"
);
assert_eq!(
stores.auth().prepare_to_serve(None).expect("decide"),
gunnar_core::Readiness::AwaitingBootstrap
);
let rt = runtime();
let server = rt
.block_on(Server::start(
Arc::clone(&stores),
ServeConfig {
no_grpc: true,
..ServeConfig::ephemeral()
},
))
.expect("an empty identity table must START now; refusing to start was the defect");
assert!(
server.bootstrap_window_open(),
"…and it must know, and be able to tell its own banner, that the door is open"
);
rt.block_on(server.shutdown()).expect("stop");
// The sentence an operator reads, in every branch, driven directly so each
// one can be seen on a box that is only ever in one of them.
let bind = "127.0.0.1:50051".parse().expect("an address");
let open = gunnar_cli::server::serve::announce_readiness(
&gunnar_core::Readiness::AwaitingBootstrap,
dir.path(),
bind,
false,
);
assert!(
open.contains("NO IDENTITY HAS EVER BEEN ENROLLED"),
"{open}"
);
assert!(
open.contains(&dir.path().display().to_string()),
"it must name the data path, or an operator cannot tell WHICH server: {open}"
);
// …and with no control plane there is no way to enrol at all, which is a
// different and worse state that must not read the same.
let stranded = gunnar_cli::server::serve::announce_readiness(
&gunnar_core::Readiness::AwaitingBootstrap,
dir.path(),
bind,
true,
);
assert!(stranded.contains("no way to enrol"), "{stranded}");
let ready = gunnar_cli::server::serve::announce_readiness(
&gunnar_core::Readiness::Ready,
dir.path(),
bind,
false,
);
assert!(
!ready.contains("NO IDENTITY"),
"an enrolled server must not shout the open-door line: {ready}"
);
}
/// The green half: the same empty table with an admin key starts, installs the
/// bootstrap admin, and gives it a **server-scoped** grant, without which it
/// could not create the first namespace and the server would be reachable and
/// useless (§5.1).
#[test]
fn an_admin_key_bootstraps_an_empty_table_and_serves() {
let dir = tempfile::tempdir().expect("temp dir");
let stores = Arc::new(ServerStores::open(dir.path()).expect("open the stores"));
let private = gunnar_core::mint_ed25519().expect("mint an admin key");
let public = gunnar_core::ssh_key::PrivateKey::from_openssh(&private)
.expect("parse")
.public_key()
.to_openssh()
.expect("render");
let rt = runtime();
let server = rt
.block_on(Server::start(
Arc::clone(&stores),
ServeConfig {
no_grpc: true,
admin_key: Some(public.clone()),
..ServeConfig::ephemeral()
},
))
.expect("an admin key must let the same server start");
assert!(!stores.auth().is_empty().expect("read the table"));
let grants = stores.auth().grants(None).expect("read the grants");
assert!(
grants
.iter()
.any(|g| g.scope == gunnar_core::Scope::Server && g.level == gunnar_core::Level::Admin),
"the bootstrap admin needs a Scope::Server grant to create the first namespace; got \
{grants:?}"
);
rt.block_on(server.shutdown()).expect("shut down");
}
/// The SSH host key is minted once and reused, so clients do not see a
/// host-key-changed warning after a restart (§5.1).
#[test]
fn the_host_key_survives_a_restart() {
let dir = tempfile::tempdir().expect("temp dir");
let private = gunnar_core::mint_ed25519().expect("mint");
let admin = gunnar_core::ssh_key::PrivateKey::from_openssh(&private)
.expect("parse")
.public_key()
.to_openssh()
.expect("render");
let rt = runtime();
let first = {
let stores = Arc::new(ServerStores::open(dir.path()).expect("open"));
let key = gunnar_core::ensure_host_key(&stores.secrets()).expect("host key");
let server = rt
.block_on(Server::start(
Arc::clone(&stores),
ServeConfig {
no_grpc: true,
admin_key: Some(admin.clone()),
..ServeConfig::ephemeral()
},
))
.expect("start");
rt.block_on(server.shutdown()).expect("shut down");
key.fingerprint().to_string()
};
// A second ServerStores on the same directory: only possible because the
// first was dropped and released its store lock, which is itself worth
// knowing works.
let stores = Arc::new(ServerStores::open(dir.path()).expect("reopen"));
let second = gunnar_core::ensure_host_key(&stores.secrets())
.expect("host key")
.fingerprint()
.to_string();
assert_eq!(
first, second,
"the host key was regenerated across a restart"
);
// The red half: a *fresh* data root must produce a different key, or the
// assertion above would pass on a constant.
let other = tempfile::tempdir().expect("temp dir");
let elsewhere = ServerStores::open(other.path()).expect("open");
let third = gunnar_core::ensure_host_key(&elsewhere.secrets())
.expect("host key")
.fingerprint()
.to_string();
assert_ne!(
first, third,
"two independent servers minted the same host key; the stability assertion above is \
meaningless"
);
}
// ── the gRPC control plane is the real one ──────────────────────────────────
/// `Admin.Health` and `Repos.List` answered from `gunnar-core`, over a real
/// tonic connection.
///
/// The repository is created through the catalog *before* the server starts and
/// is then asked for over gRPC, so a `MemoryControlPlane` (which starts empty)
/// could not produce this answer.
#[test]
fn the_grpc_control_plane_answers_from_gunnar_core() {
let dir = tempfile::tempdir().expect("temp dir");
let stores = Arc::new(ServerStores::open(dir.path()).expect("open"));
stores.catalog().create_namespace("team").expect("ns");
stores
.catalog()
.create(NewRepo::new(RepoRef::new("team", "project").unwrap()))
.expect("repo");
let private = gunnar_core::mint_ed25519().expect("mint");
let admin_key = gunnar_core::ssh_key::PrivateKey::from_openssh(&private).expect("parse");
let admin = admin_key.public_key().to_openssh().expect("render");
// gRPC authenticates against the SAME key table as SSH (D9 closed): the
// bootstrap admin key registered above is the credential below, and there
// is no second secret anywhere in this test.
let rt = runtime();
let server = rt
.block_on(Server::start(
Arc::clone(&stores),
ServeConfig {
admin_key: Some(admin),
..ServeConfig::ephemeral()
},
))
.expect("start");
let addr = server.grpc_addr().expect("the gRPC listener is bound");
assert!(
addr.ip().is_loopback(),
"an ephemeral test bind must still be loopback"
);
let endpoint = format!("http://{addr}");
rt.block_on(async {
// `connect` is not generated on these clients (gunnar-proto builds
// tonic without the channel feature), so the channel is built here.
let channel = tonic::transport::Channel::from_shared(endpoint)
.expect("a valid endpoint")
.connect()
.await
.expect("connect to the gRPC control plane");
let mut admin_client = gunnar_proto::admin_client::AdminClient::new(channel.clone());
// ── D9 is closed: the same call with no credential must be refused,
// over the real socket, through the real router. This is the proof
// that `gunnar_grpc::router` actually MOUNTS the gate: a gate that
// exists and is not wired in is the exact shape of a silent green.
let refused = admin_client
.health(gunnar_proto::HealthRequest {})
.await
.expect_err("an unauthenticated Admin.Health must be refused");
assert_eq!(
refused.code(),
tonic::Code::Unauthenticated,
"the gRPC control plane served an unauthenticated caller: {refused:?}"
);
let health = admin_client
.health(authed_with(&admin_key, gunnar_proto::HealthRequest {}))
.await
.expect("Admin.Health")
.into_inner();
assert!(
health.serving,
"the control plane reports itself not serving"
);
assert!(
health.grpc_authentication_enabled,
"D9 is closed and this listener requires a credential — the refusal above proves \
it — so Admin.Health must SAY SO on the wire. A `false` here would be a lie an \
operator would act on."
);
let stats = admin_client
.stats(authed_with(&admin_key, gunnar_proto::StatsRequest {}))
.await
.expect("Admin.Stats")
.into_inner();
assert_eq!(
stats.repos, 1,
"Admin.Stats is not counting the real catalog"
);
assert_eq!(stats.namespaces, 1);
let mut repos = gunnar_proto::repos_client::ReposClient::new(channel);
let listed = repos
.list(authed_with(
&admin_key,
gunnar_proto::ListReposRequest::default(),
))
.await
.expect("Repos.List")
.into_inner();
let names: Vec<String> = listed
.repos
.iter()
.filter_map(|r| r.path.as_ref())
.map(|p| p.to_string())
.collect();
assert_eq!(
names,
vec!["team/project".to_string()],
"Repos.List did not return the repository the catalog holds — the seam is not \
wired to gunnar-core"
);
});
rt.block_on(server.shutdown()).expect("shut down");
}
/// `gunnar gc` drives `gunnar-store`'s `repack()` (S-006).
///
/// This test used to assert the opposite: that the verb refused, because
/// `repack()` did not exist. It is kept, inverted, rather than deleted: the
/// thing worth guarding is that `gc` reaches the store at all, and the way to
/// prove that is to hand it a repository with **several packs** and assert they
/// became one. `repack()`'s own behaviour (the atomic swap, the crash, the
/// byte-identical objects) is proven in `gunnar-store`'s `tests/repack.rs`
/// against the store directly; what can only be proven here is the wiring.
#[test]
fn gc_repacks_every_repository_it_lists() {
let dir = tempfile::tempdir().expect("temp dir");
let stores = ServerStores::open(dir.path()).expect("open");
stores.catalog().create_namespace("team").expect("ns");
let repo = stores
.catalog()
.create(NewRepo::new(RepoRef::new("team", "project").unwrap()))
.expect("repo");
// Two packs and a loose object, put there the way receive-pack does.
let store = stores.repo_store(&repo).expect("the repository on disk");
let hash = store.hash_kind();
let pack_dir = store.git_dir().join("objects").join("pack");
let mut written = Vec::new();
for round in 0..2 {
let id = gunnar_store::ObjectStore::write(
&store.objects(),
gunnar_store::gix_object::Kind::Blob,
format!("gc round {round}\n").as_bytes(),
)
.expect("write a blob");
let (bytes, _) = gunnar_pack::write_pack_to_vec(
&[id],
&store.objects(),
&gunnar_pack::WriteOptions::new(hash),
)
.expect("write a pack");
gunnar_pack::index_pack(
&mut std::io::Cursor::new(&bytes),
Some(&pack_dir),
&gunnar_pack::IndexOptions::new(hash),
None,
)
.expect("index the pack");
written.push(id);
}
let count_packs = || {
std::fs::read_dir(&pack_dir)
.expect("the pack directory")
.flatten()
.filter(|e| e.path().extension().and_then(|x| x.to_str()) == Some("pack"))
.count()
};
assert_eq!(
count_packs(),
2,
"the fixture did not produce two packs, so folding them proves nothing"
);
gunnar_cli::server::admin::gc(
&stores,
gunnar_cli::cli::GcArgs {
path: None,
..Default::default()
},
)
.expect("gc repacks rather than refusing");
assert_eq!(
count_packs(),
1,
"gc left the packs alone — the verb is not reaching repack()"
);
for id in &written {
assert!(
gunnar_store::ObjectStore::contains(&store.objects(), id),
"gc lost object {id}"
);
}
}
/// **`gunnar gc --prune` reclaims; `gunnar gc` never has.**
///
/// `RepackOptions::prune_unreachable` was implemented, tested against the store
/// in `gunnar-store/tests/repack.rs`, and reachable from nothing: no flag on
/// `gunnar gc`, no caller anywhere in the tree. So nothing gunnar could do ever
/// reclaimed the objects of a rejected push, a deleted branch or a
/// force-pushed-away history. They stayed on disk forever, and an operator who
/// ran `gc` and watched the repository not shrink had no way to find out why.
///
/// **Three** arms on the same fixture, because the claim is a DIFFERENCE and
/// there are two of them. The blobs nothing references survive an ordinary `gc`
/// (which `gc_repacks_every_repository_it_lists` asserts directly, from the
/// other side); they survive `gc --prune` too, because they were written a
/// moment ago and the default grace is two weeks; and they go under
/// `--prune-grace now`.
///
/// Seen red, twice: with `.prune_unreachable(args.prune)` removed from
/// `admin::gc` this fails with *"gc --prune kept <oid>"*, the state the verb
/// shipped in; with `.prune_grace(args.prune_grace)` removed it fails on the
/// middle arm instead, which is the state `gunnar gc --prune` shipped in on
/// 2026-08-05 and which destroys a concurrent push.
#[test]
fn gc_prune_drops_what_no_reference_can_reach_and_gc_alone_keeps_it() {
let dir = tempfile::tempdir().expect("temp dir");
let stores = ServerStores::open(dir.path()).expect("open");
stores.catalog().create_namespace("team").expect("ns");
let repo = stores
.catalog()
.create(NewRepo::new(RepoRef::new("team", "project").unwrap()))
.expect("repo");
let store = stores.repo_store(&repo).expect("the repository on disk");
let hash = store.hash_kind();
let pack_dir = store.git_dir().join("objects").join("pack");
// ONE reachable blob behind a reference, so the store has something to
// keep. Without it every object in the repository is unreachable, and
// `repack()` correctly REFUSES to empty a store — which is a guard, not a
// prune, and a test that hit it would be measuring the wrong thing. This
// test hit it on the first run.
let kept = gunnar_store::ObjectStore::write(
&store.objects(),
gunnar_store::gix_object::Kind::Blob,
b"referenced\n",
)
.expect("write the reachable blob");
gunnar_store::RefStore::transaction(
&store.refs(),
vec![gunnar_store::RefEdit::force(
gunnar_store::FullName::new("refs/heads/kept").expect("a legal ref name"),
gunnar_store::Target::Object(kept),
)],
)
.expect("point a ref at it");
// Two packs of blobs no reference names: what a rejected push leaves
// behind, and what a force-push orphans.
let mut orphans = Vec::new();
for round in 0..2 {
let id = gunnar_store::ObjectStore::write(
&store.objects(),
gunnar_store::gix_object::Kind::Blob,
format!("orphan {round}\n").as_bytes(),
)
.expect("write a blob");
let (bytes, _) = gunnar_pack::write_pack_to_vec(
&[id],
&store.objects(),
&gunnar_pack::WriteOptions::new(hash),
)
.expect("write a pack");
gunnar_pack::index_pack(
&mut std::io::Cursor::new(&bytes),
Some(&pack_dir),
&gunnar_pack::IndexOptions::new(hash),
None,
)
.expect("index the pack");
orphans.push(id);
}
for id in &orphans {
assert!(
gunnar_store::ObjectStore::contains(&store.objects(), id),
"the fixture never wrote {id}, so both arms below would be vacuous"
);
}
// ── an ordinary gc keeps them ──────────────────────────────────────────
gunnar_cli::server::admin::gc(
&stores,
gunnar_cli::cli::GcArgs {
path: None,
..Default::default()
},
)
.expect("gc");
for id in &orphans {
assert!(
gunnar_store::ObjectStore::contains(&store.objects(), id),
"a `gc` with no --prune DROPPED {id}. Reclaiming is a destructive decision and must \
never be the default"
);
}
// ── and so does `--prune` under the DEFAULT grace ──────────────────────
//
// These blobs were written a millisecond ago, and the two-week grace is
// what stops a scheduled `gc --prune` from destroying a push that is still
// in flight (`RepackOptions::prune_grace`). Asserting it here and not only
// in `gunnar-store` is the point: the flag has to reach `admin::gc` with the
// store's default in it, and a `GcArgs` that defaulted the grace to zero
// would leave every other assertion in this test green.
gunnar_cli::server::admin::gc(
&stores,
gunnar_cli::cli::GcArgs {
path: None,
prune: true,
..Default::default()
},
)
.expect("gc --prune under the default grace");
for id in &orphans {
assert!(
gunnar_store::ObjectStore::contains(
&stores.repo_store(&repo).expect("reopen").objects(),
id
),
"`gc --prune` dropped {id}, written a moment ago, under the default two-week \
grace. A live push in its gate window would have lost its payload the same way"
);
}
// ── --prune --prune-grace now reclaims them ────────────────────────────
gunnar_cli::server::admin::gc(
&stores,
gunnar_cli::cli::GcArgs {
path: None,
prune: true,
prune_grace: std::time::Duration::ZERO,
..Default::default()
},
)
.expect("gc --prune --prune-grace now");
// Re-opened, because `contains` may be answered off an index snapshot the
// first store took before the fold replaced it — an assertion about disk
// read through a stale handle is exactly the blind green this suite is for.
let store = stores.repo_store(&repo).expect("reopen the repository");
for id in &orphans {
assert!(
!gunnar_store::ObjectStore::contains(&store.objects(), id),
"gc --prune kept {id}, which no reference can reach. The flag reaches nothing"
);
}
// …and it pruned what was unreachable, not everything. A `--prune` that
// emptied the repository would satisfy every assertion above.
assert!(
gunnar_store::ObjectStore::contains(&store.objects(), &kept),
"gc --prune dropped {kept}, which refs/heads/kept points straight at"
);
}
/// A gRPC request carrying a fresh, single-use sshsig credential signed with
/// `key`, the same key the §5 table holds. Each call mints a new nonce
/// because the server refuses a credential twice.
fn authed_with<T>(key: &gunnar_core::ssh_key::PrivateKey, message: T) -> tonic::Request<T> {
use std::sync::atomic::{AtomicU64, Ordering};
static SEQ: AtomicU64 = AtomicU64::new(0);
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("after 1970")
.as_millis() as i64;
let nonce = format!("serve-wiring-{:016}", SEQ.fetch_add(1, Ordering::Relaxed));
let credential = gunnar_grpc::sign_credential(key, now, &nonce)
.expect("sign a gRPC credential")
.to_header_value();
let mut request = tonic::Request::new(message);
request.metadata_mut().insert(
gunnar_grpc::CREDENTIAL_METADATA_KEY,
credential.parse().expect("an ascii credential"),
);
request
}
/// **The server itself puts no private key on disk in the clear** (`D26`).
///
/// `gunnar-core` proves `SealedSecretStore` seals. That proves nothing about
/// whether the *server* uses it — an unwired sealing layer is exactly the
/// hollow guard shape, and until this test existed the boot path called
/// `control().secrets()` and wrote the host key as plaintext.
///
/// Asserted over the bytes of `secrets.arrow` after a real `Server::start`.
#[test]
fn a_started_server_leaves_no_plaintext_host_key_on_disk() {
let dir = tempfile::tempdir().expect("temp dir");
let private = gunnar_core::mint_ed25519().expect("mint");
let admin = gunnar_core::ssh_key::PrivateKey::from_openssh(&private)
.expect("parse")
.public_key()
.to_openssh()
.expect("render");
let rt = runtime();
// The SERVER is the only writer here. Minting the key from the test first
// would make this pass on the test's own sealing rather than on the boot
// path's, which is the entire question.
let stores = Arc::new(ServerStores::open(dir.path()).expect("open"));
let server = rt
.block_on(Server::start(
Arc::clone(&stores),
ServeConfig {
no_grpc: true,
admin_key: Some(admin),
..ServeConfig::ephemeral()
},
))
.expect("start");
rt.block_on(server.shutdown()).expect("shut down");
// SNAPSHOT THE FILE FIRST, before anything in this test reads a secret.
// This ordering is load-bearing and was got wrong once: reading through
// `stores.secrets()` first MIGRATES a legacy plaintext and compacts it out,
// so the test would find a clean file and pass even with the server writing
// plaintext. The guard was blind until the bytes were captured up front.
//
// `FsStore` keeps its Arrow logs in `<git dir>/gunnar-store/`; the KEK is
// deliberately NOT in there, so a copy of the store directory carries no
// key.
let control = dir.path().join(gunnar_cli::server::stores::CONTROL_DIR);
let log = std::fs::read(control.join("gunnar-store").join("secrets.arrow"))
.expect("the server must have written a secret log");
let key = gunnar_core::ensure_host_key(&stores.secrets()).expect("read back what it minted");
drop(stores);
assert!(
!log.is_empty(),
"the secret log is empty, so this test detects nothing"
);
let contains = |needle: &[u8]| log.windows(needle.len()).any(|w| w == needle);
// The detector proves itself on something that IS there: the sealed-value
// magic. If this fails, the searches below are not searching.
assert!(
contains(gunnar_core::SEAL_MAGIC),
"the server wrote no sealed value at all, so `is not present` below means nothing"
);
assert!(
!contains(b"BEGIN OPENSSH PRIVATE KEY"),
"the server left the OpenSSH private-key armour on disk in the clear"
);
assert!(
!contains(key.private_openssh()),
"the server left the host key's exact bytes on disk in the clear"
);
// And the KEK really is a separate file, outside the log the cold tier
// would ever carry.
let kek = control.join(gunnar_core::KEK_FILE);
assert!(kek.is_file(), "no KEK was written beside the control store");
assert_eq!(std::fs::read(&kek).expect("read").len(), 32);
assert!(
!control
.join("gunnar-store")
.join(gunnar_core::KEK_FILE)
.exists(),
"the KEK is inside the store directory, so anything that copies the logs copies \
the key with them"
);
}
// ── the startup banner may not misstate the control plane's auth ───────────
/// `gunnar serve` prints one line about the gRPC listener on stdout, and until
/// 2026-08-05 that line was the string literal `(UNAUTHENTICATED in v1, D9)`,
/// printed unconditionally.
///
/// It had gone stale. [`gunnar_grpc::AuthMode::Required`] is the default and
/// `warn_authentication_disabled` was already logging *"gRPC authentication is
/// ON"* at `info!` two lines earlier, so one start printed both claims. An
/// operator reading the banner was told the control plane was open to anyone
/// who could reach the port, which is the opposite of what the server does —
/// and that is the direction of error that gets a port opened up "since it is
/// unauthenticated anyway".
///
/// # Both directions
///
/// A banner test that only checks the served default would pass against an
/// accessor hardwired to `true`, so the escape hatch is asserted to report
/// `false` through the same predicate. If somebody makes
/// `authentication_enabled` a constant, one of the two halves goes red.
#[test]
fn the_started_server_reports_the_auth_mode_it_actually_serves() {
let dir = tempfile::tempdir().expect("temp dir");
let stores = Arc::new(ServerStores::open(dir.path()).expect("open the stores"));
let private = gunnar_core::mint_ed25519().expect("mint an admin key");
let public = gunnar_core::ssh_key::PrivateKey::from_openssh(&private)
.expect("parse")
.public_key()
.to_openssh()
.expect("render");
let rt = runtime();
let server = rt
.block_on(Server::start(
Arc::clone(&stores),
ServeConfig {
admin_key: Some(public),
..ServeConfig::ephemeral()
},
))
.expect("the server must start with an admin key");
let (addr, authenticated) = server
.grpc_endpoint()
.expect("the gRPC listener is on in this config, so it must report an endpoint");
assert_eq!(
Some(addr),
server.grpc_addr(),
"the two accessors must name the same listener"
);
assert!(
authenticated,
"the served ServerConfig defaults to AuthMode::Required, so the banner must not say \
UNAUTHENTICATED"
);
assert_eq!(
authenticated,
gunnar_grpc::ServerConfig::default().authentication_enabled(),
"the endpoint's verdict must be the served config's verdict, not a second opinion"
);
// The other direction: the same predicate over the escape hatch. Without
// this, an `authentication_enabled` hardwired to `true` passes above.
assert!(
!gunnar_grpc::ServerConfig::default()
.insecure_allow_all_for_local_development()
.authentication_enabled(),
"the D9 escape hatch must report false through the very predicate the banner reads, or \
the banner cannot distinguish the two modes at all"
);
rt.block_on(server.shutdown()).expect("shut down");
}