crates/gunnar-grpc/tests/wire.rs
//! **The wiring in [`router`], asserted over a real socket.**
//!
//! `tests/auth.rs` is thorough about the two things it tests — the gate's
//! logic, and every handler's own deny-by-default — and it tests both by
//! calling the service traits DIRECTLY. Its own header says so: the 56-RPC
//! sweep runs "with no interceptor in the way". Nothing in it, and nothing
//! anywhere else in `crates/gunnar-grpc/tests/`, ever constructs a tonic
//! server. The single production assembly function
//! [`gunnar_grpc::router_with_trust`] has exactly one non-test caller
//! (`gunnar-server/src/serve.rs`) and, until this file, was executed by no test
//! at all.
//!
//! So the mapping from `gate` to `add_service(…, gate.clone())` — nine lines in
//! `lib.rs` — was asserted by prose and by nothing else. `tests/auth.rs:1013`
//! says out loud that it is *asserting in a comment* that `MirrorServer` is
//! mounted with the interceptor.
//!
//! ## Why this is not the same test twice
//!
//! Because the two layers are indistinguishable from the client's chair, and
//! deliberately so. `ServiceContext::caller` returns
//! `AuthFailure::Missing.status()` when there is no `Caller` extension, which is
//! byte-for-byte the status the gate returns when there is no credential. A
//! wire test that only asserted `Code::Unauthenticated` would therefore stay
//! green with every `with_interceptor` deleted — a hollow guard, and exactly
//! the shape a red-first test exists to catch. Measured: see
//! `an_unauthenticated_call_is_refused_BY_THE_GATE`, whose two assertions were
//! run against a deliberately unmounted `AdminServer`; the status assertion
//! stayed green and the audit assertion went red.
//!
//! The distinguishing signal is the audit record, and `auth.rs` says it is
//! there for this purpose: the gate's refusal emits `grpc.auth.refused` and
//! "names no principal because there is none, which is the difference a test
//! can assert on between this and a handler-level denial". A handler-level
//! refusal emits no such record. So:
//!
//! * `Code::Unauthenticated` says *somebody* refused;
//! * `grpc.auth.refused` says **the interceptor** refused, i.e. the service is
//! genuinely mounted behind the gate.
//!
//! ## The red
//!
//! Every claim here is asserted in both directions. `AuthMode::Required`
//! refuses; the SAME calls over the SAME wire against a listener built with
//! `insecure_allow_all_for_local_development()` are served. Without that half,
//! a router that refused everything — a dead port, a broken client, a codec
//! mismatch — would pass this file perfectly.
//!
//! `Bootstrap` is the control: it is mounted OUTSIDE the gate on purpose, and
//! it must answer an uncredentialled caller in both modes. That is what
//! separates "the gate is on" from "the port is dead".
//!
//! ## What this file does NOT prove
//!
//! It drives a plaintext loopback listener, so it says nothing about the mutual
//! TLS path in `router_with_trust(.., Some(trust))`. `lib.rs`'s own
//! `trust_tests` module explains why that one needs three live handshakes.
use std::net::{SocketAddr, TcpListener};
use std::sync::{Arc, Mutex, MutexGuard, OnceLock};
use std::time::{Duration, Instant};
use gunnar_grpc::{
router, ControlPlane, EventBus, MemoryControlPlane, ServerConfig, ServiceContext, AUDIT_TARGET,
};
use gunnar_proto::*;
use tonic::transport::Channel;
use tonic::{Code, Request};
use tracing_subscriber::layer::SubscriberExt;
const NS: &str = "nordisk";
const REPO: &str = "gunnar";
// ───────────────────────────────────────────────────────────────────────────
// The audit trail, captured globally
// ───────────────────────────────────────────────────────────────────────────
//
// `tests/auth.rs` uses `with_default`, which is THREAD-LOCAL. A tonic server
// answers on the runtime's worker threads, so a thread-local subscriber sees
// none of its events — a capture that quietly recorded nothing would make every
// audit assertion here vacuously... false, and then vacuously green the moment
// someone "fixed" it by asserting emptiness. It is a global subscriber for that
// reason, and the tests take a lock so one test's records are not another's.
static AUDIT: OnceLock<Arc<Mutex<Vec<String>>>> = OnceLock::new();
static SERIALIZE: Mutex<()> = Mutex::new(());
fn audit() -> &'static Arc<Mutex<Vec<String>>> {
AUDIT.get_or_init(|| {
let log: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(Vec::new()));
let subscriber = tracing_subscriber::registry().with(AuditLayer(Arc::clone(&log)));
tracing::subscriber::set_global_default(subscriber)
.expect("this test binary installs the audit subscriber exactly once");
log
})
}
struct AuditLayer(Arc<Mutex<Vec<String>>>);
impl<S: tracing::Subscriber> tracing_subscriber::Layer<S> for AuditLayer {
fn on_event(
&self,
event: &tracing::Event<'_>,
_ctx: tracing_subscriber::layer::Context<'_, S>,
) {
if event.metadata().target() != AUDIT_TARGET {
return;
}
struct Visit(String);
impl tracing::field::Visit for Visit {
fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn std::fmt::Debug) {
self.0.push_str(&format!("{}={:?} ", field.name(), value));
}
}
let mut visit = Visit(String::new());
event.record(&mut visit);
self.0.lock().unwrap().push(visit.0);
}
}
/// Take the serialization lock and start this test with an empty audit log.
fn fresh_audit() -> MutexGuard<'static, ()> {
let guard = SERIALIZE.lock().unwrap_or_else(|e| e.into_inner());
audit().lock().unwrap().clear();
guard
}
/// The audit records emitted so far.
///
/// The gate warns from the worker thread that answered the call, and the client
/// future can resolve before that `tracing::warn!` has been appended, so this
/// waits briefly for a record rather than sampling once. It returns whatever it
/// has when the deadline passes — an empty answer is a legitimate outcome here
/// (it is exactly what a handler-level refusal produces) and must not be
/// smoothed into a hang or a panic.
fn audit_lines() -> Vec<String> {
let deadline = Instant::now() + Duration::from_secs(2);
loop {
let lines = audit().lock().unwrap().clone();
if !lines.is_empty() || Instant::now() >= deadline {
return lines;
}
std::thread::sleep(Duration::from_millis(10));
}
}
fn refusals(lines: &[String]) -> Vec<&String> {
lines
.iter()
.filter(|line| line.contains(r#"event="grpc.auth.refused""#))
.collect()
}
// ───────────────────────────────────────────────────────────────────────────
// A real listener, serving the real router
// ───────────────────────────────────────────────────────────────────────────
/// The production assembly, on a real TCP port.
///
/// [`router`] and nothing else: a fixture that hand-assembled a tonic server
/// here would be a SECOND wiring, and the drift between it and `lib.rs` is the
/// entire thing this file exists to detect.
struct Wire {
addr: SocketAddr,
runtime: tokio::runtime::Runtime,
}
impl Wire {
fn serve(config: ServerConfig) -> Wire {
// Bind to :0 to learn a free port, then release it. tonic's `serve`
// binds the address itself, so the port has to be named before it is
// handed over.
let addr = TcpListener::bind("127.0.0.1:0")
.expect("a loopback port")
.local_addr()
.expect("its address");
let plane = Arc::new(MemoryControlPlane::with_repo(NS, REPO));
let control: Arc<dyn ControlPlane> = plane;
let ctx = ServiceContext::new(control, Arc::new(EventBus::new(64)), config);
let runtime = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.expect("runtime");
runtime.spawn(async move {
let _ = router(ctx, || 0).serve(addr).await;
});
let wire = Wire { addr, runtime };
wire.await_listening();
wire
}
/// Block until the listener answers, and fail loudly if it never does.
///
/// A test that raced the bind would report `Unauthenticated`-shaped
/// failures that are really connection failures, which is the same green
/// as a working gate read backwards.
fn await_listening(&self) {
let deadline = Instant::now() + Duration::from_secs(10);
while Instant::now() < deadline {
if std::net::TcpStream::connect_timeout(&self.addr, Duration::from_millis(200)).is_ok()
{
return;
}
std::thread::sleep(Duration::from_millis(20));
}
panic!("the router never bound {}", self.addr);
}
fn channel(&self) -> Channel {
let url = format!("http://{}", self.addr);
self.runtime.block_on(async move {
Channel::from_shared(url)
.expect("a valid endpoint")
.connect()
.await
.expect("connect to the router")
})
}
}
/// The gate ON — the shipped default, and the fixture asserts that rather than
/// trusting it.
fn required() -> Wire {
let config = ServerConfig::default();
assert_eq!(
config.auth,
gunnar_grpc::AuthMode::Required,
"the fixture must serve the SHIPPED default; a wire test that quietly ran the escape \
hatch would prove nothing"
);
Wire::serve(config)
}
/// The gate OFF — the escape hatch, used here as the red half and nowhere else.
fn allow_all() -> Wire {
Wire::serve(ServerConfig::default().insecure_allow_all_for_local_development())
}
/// Drive one uncredentialled RPC on each of the nine gated services, over the
/// wire, and hand back `(service.method, outcome)`.
///
/// One list, used by both the refusal test and the allow-all test, so the two
/// halves cannot drift into asking different questions (one implementation).
fn sweep(wire: &Wire) -> Vec<(&'static str, Result<(), Code>)> {
let channel = wire.channel();
let mut out = Vec::new();
wire.runtime.block_on(async {
macro_rules! call {
($name:expr, $client:path, $method:ident, $request:expr) => {{
let mut client = <$client>::new(channel.clone());
let outcome = client
.$method(Request::new($request))
.await
.map(|_| ())
.map_err(|status| status.code());
out.push(($name, outcome));
}};
}
call!(
"Repos.List",
repos_client::ReposClient<Channel>,
list,
ListReposRequest::default()
);
call!(
"Refs.List",
refs_client::RefsClient<Channel>,
list,
ListRefsRequest::default()
);
call!(
"Access.ListPrincipals",
access_client::AccessClient<Channel>,
list_principals,
ListPrincipalsRequest::default()
);
call!(
"Policy.ListGates",
policy_client::PolicyClient<Channel>,
list_gates,
ListGatesRequest::default()
);
call!(
"Events.Watch",
events_client::EventsClient<Channel>,
watch,
WatchRequest::default()
);
call!(
"Admin.Health",
admin_client::AdminClient<Channel>,
health,
HealthRequest::default()
);
call!(
"Mirror.Status",
mirror_client::MirrorClient<Channel>,
status,
MirrorStatusRequest::default()
);
call!(
"PullRequests.List",
pull_requests_client::PullRequestsClient<Channel>,
list,
ListPullRequestsRequest::default()
);
call!(
"Forge.ListReviews",
forge_client::ForgeClient<Channel>,
list_reviews,
ListReviewsRequest::default()
);
});
out
}
// ───────────────────────────────────────────────────────────────────────────
// The claims
// ───────────────────────────────────────────────────────────────────────────
/// **Every gated service refuses a caller who sent no credential — and it is
/// the GATE that refuses.**
///
/// The second half is the load-bearing one. See the module header: the status
/// alone cannot tell a mounted interceptor from a handler catching the same
/// request behind an unmounted one.
#[test]
fn an_unauthenticated_call_is_refused_by_the_gate() {
let _guard = fresh_audit();
let wire = required();
let outcomes = sweep(&wire);
assert_eq!(outcomes.len(), 9, "one call per gated service");
for (name, outcome) in &outcomes {
assert_eq!(
*outcome,
Err(Code::Unauthenticated),
"{name} answered an uncredentialled caller over the wire"
);
}
// And the interceptor is what answered. A handler-level denial emits no
// `grpc.auth.refused` record at all.
let lines = audit_lines();
let refused = refusals(&lines);
assert_eq!(
refused.len(),
outcomes.len(),
"expected one gate refusal per call; the gate is not mounted in front of every service. \
records: {lines:#?}"
);
for line in &refused {
assert!(
line.contains(r#"reason="missing""#),
"a call with no credential must be refused as `missing`, not {line}"
);
}
}
/// **`Bootstrap` answers without a credential — in the gated mode too.**
///
/// The control. It is mounted outside the gate deliberately (`lib.rs`: a gated
/// `Bootstrap` on a server with an empty key table could be reached by nobody),
/// and it is what proves the nine refusals above are a gate and not a dead
/// port, a bad codec or a client that cannot connect.
#[test]
fn bootstrap_is_served_without_a_credential() {
let _guard = fresh_audit();
let wire = required();
let channel = wire.channel();
let answered = wire.runtime.block_on(async {
bootstrap_client::BootstrapClient::new(channel)
.status(Request::new(BootstrapStatusRequest::default()))
.await
});
assert!(
answered.is_ok(),
"Bootstrap.Status is the ONE unauthenticated service and must answer: {answered:?}"
);
assert!(
refusals(&audit_lines()).is_empty(),
"the gate refused a service it is not mounted in front of"
);
}
/// **THE RED: the same nine calls, on the same wire, are SERVED when the gate
/// is off.**
///
/// Without this, a router that refused every request for any reason would pass
/// the test above and prove nothing. `Repos.List` is asserted as an outright
/// success; the others are only asserted to be no longer refused *for want of a
/// credential*, because several of them name a repository this fixture does not
/// have and are entitled to fail on their own merits.
#[test]
fn the_gate_is_what_refuses_and_not_the_wire() {
let _guard = fresh_audit();
let wire = allow_all();
let outcomes = sweep(&wire);
let repos = outcomes
.iter()
.find(|(name, _)| *name == "Repos.List")
.expect("Repos.List is in the sweep");
assert_eq!(
repos.1,
Ok(()),
"with the gate off, an uncredentialled Repos.List must be served — if this is \
UNAUTHENTICATED the refusal above was never the gate's"
);
for (name, outcome) in &outcomes {
assert_ne!(
*outcome,
Err(Code::Unauthenticated),
"{name} was refused for want of a credential on a listener that requires none"
);
}
// **The predicate `gunnar control status` reports on.** That verb calls
// `Admin.Health` with no credential and prints `authentication REQUIRED`
// when it is refused; its other branch — the ⚠ one that says the control
// plane is serving uncredentialled admin calls — cannot be reached through
// any shipped path, because nothing in production calls
// `insecure_allow_all_for_local_development`. This is the assertion that
// keeps that warning from being fiction: on a listener with the gate off,
// an uncredentialled `Admin.Health` really is SERVED, so the verb's two
// branches describe two states that both exist.
let health = outcomes
.iter()
.find(|(name, _)| *name == "Admin.Health")
.expect("Admin.Health is in the sweep");
assert_eq!(
health.1,
Ok(()),
"with the gate off an uncredentialled Admin.Health must be served — this is the exact \
call `gunnar control status` uses to decide which of its two verdicts to print"
);
assert!(
refusals(&audit_lines()).is_empty(),
"the gate refused nothing here, so it must have recorded nothing"
);
}