Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/gunnar-ui

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}");
}