crates/gunnar-ui-server/tests/env_declaration.rs
//! **The environment file's declaration, measured against the binary.**
//!
//! `gunnar_ui_server::envfile::VARS` is only worth having if it cannot fall
//! behind. This test scans this crate's own source for every `GUNNAR_UI_*` the
//! server names and fails when one of them is not accounted for — by name, by
//! family (`GUNNAR_UI_FORGE_*`, whose members `forges.rs` builds at run time), or
//! as a build-time stamp that reaches the binary through `cargo:rustc-env` and
//! belongs in no file.
//!
//! It is a source scan and not a runtime one because a variable that is read on
//! one branch of `main` is invisible to a test that does not take that branch —
//! and the whole point is to catch a key somebody adds without declaring it.
use std::collections::BTreeSet;
use std::path::{Path, PathBuf};
use gunnar_ui_server::envfile::{self, Fill, Need};
/// Every `GUNNAR_UI_*` that appears anywhere in the crate's source.
fn keys_in_source() -> BTreeSet<String> {
let src = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("src");
let mut files = Vec::new();
collect_rs(&src, &mut files);
// **The declaration is not its own evidence.** With `envfile.rs` in the
// scan, every key it declares trivially "appears in the source" and
// `the_declaration_names_no_key_the_server_does_not_read` cannot fail — it
// was green against a deliberately injected `GUNNAR_UI_LONG_GONE` before
// this line existed. The forward direction loses nothing either: a key
// named only in the table is declared by definition.
files.retain(|f| f.file_name().is_some_and(|n| n != "envfile.rs"));
assert!(
files.len() > 20,
"only {} source files found under {} — the scan is not looking where it thinks it is",
files.len(),
src.display()
);
let mut keys = BTreeSet::new();
for f in files {
let text = std::fs::read_to_string(&f).unwrap_or_default();
let bytes = text.as_bytes();
let needle = b"GUNNAR_UI_";
let mut i = 0;
while let Some(at) = find(&bytes[i..], needle) {
let start = i + at;
let mut end = start + needle.len();
while end < bytes.len()
&& (bytes[end].is_ascii_uppercase()
|| bytes[end].is_ascii_digit()
|| bytes[end] == b'_')
{
end += 1;
}
// The BARE prefix is prose — `a GUNNAR_UI_* the table does not know
// about`. A real key, including the `GUNNAR_UI_FORGE_` that
// `forges.rs` completes at run time, always has something after it.
if end > start + needle.len() {
keys.insert(text[start..end].to_string());
}
i = end;
}
}
keys
}
fn find(haystack: &[u8], needle: &[u8]) -> Option<usize> {
haystack.windows(needle.len()).position(|w| w == needle)
}
fn collect_rs(dir: &Path, out: &mut Vec<PathBuf>) {
let Ok(entries) = std::fs::read_dir(dir) else {
return;
};
for e in entries.flatten() {
let p = e.path();
if p.is_dir() {
collect_rs(&p, out);
} else if p.extension().is_some_and(|x| x == "rs") {
out.push(p);
}
}
}
#[test]
fn every_gunnar_ui_key_the_server_names_is_declared() {
let undeclared: Vec<String> = keys_in_source()
.into_iter()
.filter(|k| !envfile::declared(k))
.collect();
assert!(
undeclared.is_empty(),
"these GUNNAR_UI_* keys appear in the source and are in no declaration:\n {}\n\nAdd \
each to gunnar_ui_server::envfile::VARS with its Need, its Fill and the reason for \
its value — or to FAMILIES / BUILD_TIME / TEST_ONLY if that is what it is. An undeclared key is \
a key `--env-template` does not write and `--install` does not check, which is how \
/etc/gunnar-ui.env came to be a thing only deploy/README.md knew about.",
undeclared.join("\n ")
);
}
#[test]
fn the_declaration_names_no_key_the_server_does_not_read() {
// The other direction. A declaration that has outlived its variable makes
// `--env-template` write a line that does nothing, and an operator who sets
// it will spend the evening wondering why.
let in_source = keys_in_source();
let orphans: Vec<&str> = envfile::VARS
.iter()
.map(|v| v.key)
.filter(|k| !in_source.contains(*k))
.collect();
assert!(orphans.is_empty(), "declared but read nowhere: {orphans:?}");
}
#[test]
fn the_required_keys_are_exactly_the_ones_this_layout_cannot_start_without() {
// Frozen on purpose. Promoting a key to Required makes every existing host's
// `--install` refuse until somebody edits /etc/gunnar-ui.env, and demoting
// one makes an install stop catching the misconfiguration it was there for.
// Either is a decision; neither is an edit.
let required: Vec<&str> = envfile::VARS
.iter()
.filter(|v| v.need == Need::Required)
.map(|v| v.key)
.collect();
assert_eq!(
required,
vec![
"GUNNAR_UI_BIND",
"GUNNAR_UI_CONTROL",
"GUNNAR_UI_IDENTITY",
"GUNNAR_UI_STATE",
"GUNNAR_UI_DIR",
"GUNNAR_UI_WEBAUTHN_RP_ID",
"GUNNAR_UI_WEBAUTHN_ORIGIN",
"GUNNAR_UI_ADMIN_TOKEN",
]
);
}
#[test]
fn the_template_carries_no_secret_and_says_where_each_one_comes_from() {
let text = envfile::template();
for v in envfile::VARS {
assert!(
text.contains(v.key),
"{} is declared and the template does not mention it",
v.key
);
if let Fill::Placeholder(p) = v.fill {
assert!(
p.contains("REPLACE-ME") || p.starts_with('/'),
"{}'s placeholder {p:?} reads like a value; it must read like a blank",
v.key
);
}
}
// The one thing this repository must never ship.
assert!(text.contains("GUNNAR_UI_ADMIN_TOKEN=REPLACE-ME"), "{text}");
}