crates/gunnar-grpc/tests/census.rs
//! **The census of authorised RPCs is derived from the protos, not counted by
//! hand.**
//!
//! `auth::METHODS` is the list every deny-by-default sweep iterates, and the
//! only thing that had ever kept it complete was somebody remembering to add a
//! line. That works until it does not. On 2026-09-11 the list held 71 names,
//! its own doc comment said "the 37 RPCs", and the `.proto` files declared 89 —
//! so **twelve** RPCs were served and censused nowhere: the four
//! `Access.*Group*` verbs, `Forge.GetIssue`/`ListIssues`,
//! `Mirror.SetCredential`/`ResyncReplication`, `Usage.Activity`, and all three
//! `Webhooks.*`.
//!
//! **None of them was an open door.** Every one was read, and every one calls
//! `require` or `authorize` in its handler. What was missing is the thing that
//! would notice if one stopped — the sweep in `tests/auth.rs` drives
//! `audited_methods()`, so an RPC absent from the list is an RPC the sweep has
//! never once driven. That is the same hole the `PullRequests.*` note in
//! `auth.rs` records being found in on 2026-08-30, five RPCs earlier, and
//! finding it twice is what makes the hand-kept list the defect rather than the
//! omissions.
//!
//! So the expected set is READ OUT OF THE PROTOS here. A new RPC is a failing
//! test in this file until it is declared, and no number anywhere has to be
//! re-counted.
//!
//! # There is a second census in `tests/auth.rs`, and it is not a duplicate
//!
//! `tests/auth.rs::declared_rpcs` reads the same `.proto` files through
//! `include_str!` — the bytes that were COMPILED INTO `gunnar-proto` — and
//! compares them against `audited_methods() + UNAUTHENTICATED_METHODS`, which
//! is the set its deny-by-default sweep actually drives. This file reads the
//! files ON DISK and compares against that set plus `UNIMPLEMENTED_METHODS`,
//! because "declared absent" is a declaration too. The two answer different
//! questions — what was built, and what is in the tree — and either one going
//! red is a different fact about the repository, so both stay.
//!
//! # This is a scanner, not a protobuf parser
//!
//! The same deliberate limit `xtask`'s manifest checks record: a `service X {`
//! line opens a service and an `rpc Y(` line inside one names a method. That is
//! weaker than parsing and can be evaded by writing the same proto a different
//! way. It is still worth having, because the failure it catches is somebody
//! adding an `rpc` line — and `the_scanner_finds_what_is_really_there` below is
//! what keeps a scanner that stopped matching from reading as a clean census.
use std::collections::BTreeSet;
/// Where the protos live, from this crate.
fn proto_dir() -> std::path::PathBuf {
std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.expect("crates/")
.join("gunnar-proto/proto/gunnar/v1")
}
/// Every `Service.Method` the `.proto` files declare.
fn declared_rpcs() -> BTreeSet<String> {
let mut out = BTreeSet::new();
let dir = proto_dir();
let entries =
std::fs::read_dir(&dir).unwrap_or_else(|err| panic!("reading {}: {err}", dir.display()));
for entry in entries.flatten() {
let path = entry.path();
if path.extension().and_then(|e| e.to_str()) != Some("proto") {
continue;
}
let text = std::fs::read_to_string(&path)
.unwrap_or_else(|err| panic!("reading {}: {err}", path.display()));
let mut service: Option<String> = None;
for line in text.lines() {
let line = line.trim();
if let Some(rest) = line.strip_prefix("service ") {
service = rest
.split_whitespace()
.next()
.map(|name| name.trim_end_matches('{').to_string());
continue;
}
if let (Some(service), Some(rest)) = (service.as_ref(), line.strip_prefix("rpc ")) {
if let Some(method) = rest.split('(').next() {
out.insert(format!("{service}.{}", method.trim()));
}
}
}
}
out
}
/// The set the scanner is compared against: everything gunnar declares about
/// an RPC, in any of its four tables.
///
/// `UNIMPLEMENTED_METHODS` is in here because "declared absent" is a
/// declaration too — an RPC that is deliberately not served is censused, it is
/// just censused as not served.
fn censused() -> BTreeSet<String> {
let mut out: BTreeSet<String> = gunnar_grpc::auth::audited_methods()
.into_iter()
.map(str::to_owned)
.collect();
out.extend(
gunnar_grpc::UNAUTHENTICATED_METHODS
.iter()
.map(|m| (*m).to_owned()),
);
out.extend(
gunnar_grpc::UNIMPLEMENTED_METHODS
.iter()
.map(|m| (*m).to_owned()),
);
out
}
/// The two METERED families — `Entitlement.*` and `Billing.*` — are compiled
/// only with the `metered` feature, and their `.proto` files are on disk either
/// way.
///
/// Absent code has no refusal to check, so the community build is held to the
/// community surface and the metered build to its own — rather than to a union
/// that would let one hide inside the other.
///
/// **This is a prefix list and not a wildcard, on purpose.** Every name in it
/// is a family this census stops checking in the community build, so adding one
/// is the act of saying "the community appliance does not serve these" — and
/// `gunnar-cli/tests/feature_split.rs` is what holds that claim to the resolved
/// graph. A rule like "anything the community build happens not to mount" would
/// make an RPC that quietly stopped being mounted disappear from the census
/// instead of failing it.
const METERED_FAMILIES: &[&str] = &["Entitlement.", "Billing.", "Sealed."];
fn expected() -> BTreeSet<String> {
declared_rpcs()
.into_iter()
.filter(|rpc| {
cfg!(feature = "metered")
|| !METERED_FAMILIES
.iter()
.any(|family| rpc.starts_with(family))
})
.collect()
}
/// **Every RPC the protos declare is censused, and nothing else is.**
///
/// Both directions. The first is the finding: an RPC nobody wrote down is an
/// RPC the deny-by-default sweep has never driven. The second is what stops the
/// list rotting the other way, into names of RPCs that no longer exist and
/// whose refusals nothing checks because there is nothing to refuse.
#[test]
fn every_rpc_the_protos_declare_is_censused_and_nothing_else_is() {
let expected = expected();
let censused = censused();
let uncensused: Vec<&String> = expected.difference(&censused).collect();
assert!(
uncensused.is_empty(),
"these RPCs are served and appear in no table, so the deny-by-default sweep has \
never driven them and nothing would notice if one stopped authorising: \
{uncensused:?}"
);
let phantom: Vec<&String> = censused.difference(&expected).collect();
assert!(
phantom.is_empty(),
"these names are censused and no proto declares them, so their entry is checking a \
refusal nothing can make: {phantom:?}"
);
}
/// The identity-value guard for the test above.
///
/// A scanner that matched nothing would make both assertions above pass
/// vacuously in one direction and fail loudly in the other — but a scanner that
/// found the services and missed some METHODS would quietly shrink the expected
/// set until the census "covered" it. So the scan is checked against a handful
/// of RPCs written out here by hand, chosen across four different proto files
/// and including one of the twelve that were missing.
#[test]
fn the_scanner_finds_what_is_really_there() {
let declared = declared_rpcs();
assert!(
declared.len() >= 80,
"the scanner found only {} RPCs across {}; it has stopped matching and every \
census below it is vacuous",
declared.len(),
proto_dir().display()
);
for rpc in [
"Bootstrap.Enroll",
"Repos.Create",
"Refs.ReadLfsObject",
"Mirror.SetCredential",
"Webhooks.AddWebhook",
"Entitlement.Set",
] {
assert!(
declared.contains(rpc),
"the scanner did not find {rpc}, which is in the protos on disk"
);
}
assert!(
!declared.contains("Repos.ThisDoesNotExist"),
"the scanner reports RPCs that are not declared anywhere"
);
}