crates/gunnar-ui-server/tests/airgap_default_build.rs
//! **AIRGAP: gunnar-ui-server's default build links only the approved outbound clients**
//! (owner rule 2026-10-25: "the DEFAULT build is the AIRGAP install").
//!
//! The same guard as gunnar's `crates/gunnar-recipe/tests/airgap_default_builds.rs`, for the
//! console: its cargo-DEFAULT build (`cargo tree -e normal,features`, offline, `--locked`) may
//! contain an outbound client crate, or the outbound half of a dual-use crate, only where
//! [`ALLOW`] names it, with its one-line reason. It fails on a new outbound crate and on an
//! allowance whose crate has gone. The shop relay (monetize-site-buy) is held out of the
//! default build by `default_build_links_no_shop.rs` (G-REDS); this guard does not repeat it.
use std::collections::BTreeSet;
use std::path::{Path, PathBuf};
use std::process::Command;
/// The outbound clients a default build may not link unless [`ALLOW`] names them.
const DENY: &[&str] = &[
"reqwest",
"ureq",
"isahc",
"curl",
"attohttpc",
"surf",
"hyper-rustls",
"hyper-tls",
"lettre",
"async-smtp",
"mail-send",
"instant-acme",
"rustls-acme",
"tokio-rustls-acme",
"hickory-resolver",
"trust-dns-resolver",
"tokio-tungstenite",
"async-nats",
"rumqttc",
"sentry",
"opentelemetry-otlp",
"ldap3",
"gix-transport",
"ssh2",
"git2",
"boringtun",
"tonic/channel",
"hyper/client",
"hyper-util/client-legacy",
];
/// **Never in a default (airgap) build, by name, and no allowance can admit them** (owner rule
/// 7, 2026-10-26: "airgap = the DOWNSTREAM builds"): the public site and its SEO (the crate
/// carries IndexNow's ping and the crawler lists), the MCP connector, and the internet pull
/// clients (the forge import, `gunnar-forge`). They live behind their default-off features
/// (`public-site`, `mcp`, `forges`), and so do those features themselves.
const NEVER: &[&str] = &[
"gunnar-public-site",
"gunnar-mcp",
"gunnar-forge",
"gunnar-ui-server/public-site",
"gunnar-ui-server/mcp",
"gunnar-ui-server/forges",
"gunnar-front/public-site",
"gunnar-front/mcp",
"gunnar-front/forges",
];
/// `(label, manifest relative to gunnar-ui's root, package)`.
const BUILDS: &[(&str, &str, Option<&str>)] = &[("gunnar-ui-server", "Cargo.toml", Some("gunnar-ui-server"))];
const TONIC: &str = "tonic client: own twin or own server (core)";
/// The approved exceptions: `(build, deny entry, reason)`.
const ALLOW: &[(&str, &str, &str)] = &[
("gunnar-ui-server", "tonic/channel", TONIC),
("gunnar-ui-server", "hyper/client", TONIC),
("gunnar-ui-server", "hyper-util/client-legacy", TONIC),
("gunnar-ui-server", "ureq", "edda nornir-docbook's remote-image fetch: linked, never called (every export passes no cache dir); edda has no feature to drop it"),
];
fn root() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR")).join("../..").canonicalize().expect("gunnar-ui's root")
}
/// The crates and `crate/feature` pairs in a `cargo tree -e normal,features --prefix none` listing.
fn present(tree: &str) -> BTreeSet<String> {
let mut out = BTreeSet::new();
for line in tree.lines() {
let mut w = line.split_whitespace();
let Some(name) = w.next() else { continue };
match w.next() {
Some("feature") => {
if let Some(f) = w.next() {
out.insert(format!("{name}/{}", f.trim_matches('"')));
}
out.insert(name.to_owned());
}
Some(v) if v.starts_with('v') => {
out.insert(name.to_owned());
}
_ => {}
}
}
out
}
fn findings(label: &str, present: &BTreeSet<String>) -> Vec<String> {
let allowed: BTreeSet<&str> = ALLOW.iter().filter(|(l, _, _)| *l == label).map(|(_, d, _)| *d).collect();
let mut out = Vec::new();
for n in NEVER {
if present.contains(*n) {
out.push(format!("{label}: the default build links `{n}`, which an airgap build may NEVER contain (owner rule 7)"));
}
}
for d in DENY {
if present.contains(*d) && !allowed.contains(d) {
out.push(format!("{label}: the default build links the outbound client `{d}`, and no approved reason allows it there"));
}
}
for a in allowed {
if !present.contains(a) {
out.push(format!("{label}: `{a}` is allowed but no longer in the default build; drop it from ALLOW"));
}
}
out
}
fn tree(manifest: &str, package: Option<&str>) -> Result<String, String> {
let cargo = std::env::var("CARGO").unwrap_or_else(|_| "cargo".into());
let mut cmd = Command::new(cargo);
cmd.current_dir(root()).args(["tree", "--offline", "--locked", "-e", "normal,features", "--prefix", "none", "--manifest-path", manifest]);
if let Some(p) = package {
cmd.args(["-p", p]);
}
let out = cmd.output().map_err(|e| format!("{manifest}: cargo tree did not run: {e}"))?;
if !out.status.success() {
return Err(format!("{manifest}: cargo tree failed: {}", String::from_utf8_lossy(&out.stderr)));
}
Ok(String::from_utf8_lossy(&out.stdout).into_owned())
}
#[test]
fn every_default_build_links_only_the_approved_outbound_clients() {
let mut bad = Vec::new();
for (label, manifest, package) in BUILDS {
match tree(manifest, *package) {
Ok(t) => bad.extend(findings(label, &present(&t))),
Err(e) => bad.push(e),
}
}
assert!(bad.is_empty(), "a default build is not an airgap build:\n{}", bad.join("\n"));
}
#[test]
fn an_unlisted_client_is_refused_and_a_listed_one_is_not() {
let p = present("reqwest v0.13.5\ntonic feature \"channel\"\nhyper feature \"client\"\nhyper-util feature \"client-legacy\"\nureq v2.12.1\n");
let f = findings("gunnar-ui-server", &p);
assert_eq!(f.len(), 1, "{f:?}");
assert!(f[0].contains("`reqwest`"), "{f:?}");
}
#[test]
fn a_client_allowed_in_one_build_is_refused_in_another() {
assert!(findings("gunnar-ui-server", &present("boringtun v0.7.1\n")).iter().any(|f| f.contains("`boringtun`")));
}
/// ★ Owner rule 7: a fixture that adds one of the NEVER crates or features to a default build
/// is refused BY NAME, whatever the allowances say.
#[test]
fn the_public_site_mcp_and_the_forge_import_are_refused_by_name() {
for (line, name) in [
("gunnar-public-site v2.0.0\n", "gunnar-public-site"),
("gunnar-mcp v2.0.0\n", "gunnar-mcp"),
("gunnar-forge v1.4.0\n", "gunnar-forge"),
("gunnar-ui-server feature \"public-site\"\n", "gunnar-ui-server/public-site"),
("gunnar-ui-server feature \"mcp\"\n", "gunnar-ui-server/mcp"),
("gunnar-ui-server feature \"forges\"\n", "gunnar-ui-server/forges"),
] {
let f = findings("gunnar-ui-server", &present(line));
assert!(f.iter().any(|f| f.contains(&format!("`{name}`")) && f.contains("NEVER")), "{name}: {f:?}");
}
for n in NEVER {
assert!(!ALLOW.iter().any(|(_, d, _)| d == n), "{n} may never be allowed");
}
}
#[test]
fn a_stale_allowance_is_a_finding() {
assert!(findings("gunnar-ui-server", &BTreeSet::new()).iter().any(|f| f.contains("no longer")));
}
#[test]
fn every_allowance_names_a_denied_entry_a_build_and_a_reason() {
for (l, d, r) in ALLOW {
assert!(DENY.contains(d), "{l}: {d} is not denied");
assert!(BUILDS.iter().any(|(b, _, _)| b == l), "{l}: no such build");
assert!(!r.trim().is_empty(), "{l}/{d}: no reason");
}
}