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/open_build_speaks_no_money.rs

//! **The open console speaks no money** — the twin of gunnar's own
//! `gunnar-server/tests/open_build_speaks_no_money.rs`.
//!
//! # The owner's ruling, 2026-09-15
//!
//! > The OPEN-SOURCE gunnar-ui (built WITHOUT `metered`) shall NOT mention
//! > money anywhere a user or operator can see it — UI text, `--help`,
//! > error/refusal messages, served JSON field names and values — stays
//! > UNLIMITED (no quotas, no buy/request-more flow), but SHOULD show how much
//! > storage the box has.
//!
//! Three halves, each saying what it did:
//!
//! * **the ROUTES**, driven — a default build's router is asked for every money
//!   path and must answer 404. `cfg(not(feature = "metered"))`.
//!
//! * **the WORDS the console can render**, scanned — every string literal in
//!   `gunnar-ui-web/src` (bar the one metered page) must be free of the money
//!   vocabulary. **It never skips**, in either build: the wasm bundle is ONE
//!   artifact, so a money word typed into a shared panel ships to everybody.
//!
//! * **the WORDS the open BFF can say** — every string literal an open
//!   `gunnar-ui-server` compiles: its `--help`, its refusal and error bodies,
//!   its JSON values, its served pages. Comments are not scanned, nor is code
//!   the open build does not contain (`#[cfg(feature = "metered")]` items and
//!   modules, `#[cfg(test)]`/`#[test]` items). `cfg(not(feature = "metered"))`.
//!
//! The served JSON's field NAMES are Rust identifiers, not literals; the one
//! JSON a new panel adds (`/api/dataset`) is asserted word by word in
//! `api::usage`'s own test, and a live guest's JSON is judged by gunnar's
//! capture half (`GUNNAR_OPEN_GUARD_CAPTURE`).
//!
//! # The word list and the judge are gunnar's, verbatim
//!
//! Copied from gunnar's file rather than re-argued — including the contextual
//! rule: `plan`, `checkout`, `renew` and `subscribe` are money only when
//! nothing in the same literal makes them technical ([`TECHNICAL_CONTEXT`]),
//! and `stripe` only when it spells the vendor (`Stripe`, `stripe.com`). Two
//! spellings of one rule across two repositories is how one of them goes
//! stale, so when one changes the other is changed in the same breath.

use std::path::{Path, PathBuf};

const MONEY: &[&str] = &[
    "price", "prices", "priced", "pricing",
    "pay", "pays", "paying", "paid", "payable", "payment", "payments", "paywall",
    "buy", "buys", "buying", "bought", "buyer", "buyers",
    "purchase", "purchases", "purchased", "purchasing",
    "sell", "sells", "selling", "sold", "seller", "sellers",
    "bill", "bills", "billed", "billing", "billable",
    "invoice", "invoices", "invoicing",
    "checkout", "checkouts",
    "money", "currency", "currencies", "sek", "eur", "usd",
    "stripe",
    "plan", "plans",
    "quota", "quotas",
    "subscription", "subscriptions", "subscribe",
    "renew", "renews", "renewal", "renewals",
    "refund", "refunds", "revenue",
];

const CONTEXTUAL: &[&str] = &[
    "plan", "plans", "checkout", "checkouts", "renew", "renews", "renewal", "renewals",
    "subscription", "subscriptions", "subscribe",
];

const TECHNICAL_CONTEXT: &[&str] = &[
    "interface", "interfaces", "lease", "leases", "dhcp", "dhcprequest", "address", "addresses",
    "route", "routes", "network", "net",
    "event", "events", "bus", "watch", "watchrequest", "replication", "webhook", "runner",
    "subscriber", "stream",
    "git", "tree", "worktree", "commit", "branch", "clone", "entry", "build",
    "grow", "growth", "sectors", "blocks", "resize", "layout", "install", "reset", "passkeys",
];

fn words(text: &str) -> Vec<String> {
    text.split(|c: char| !c.is_ascii_alphanumeric())
        .filter(|w| !w.is_empty())
        .map(|w| w.to_ascii_lowercase())
        .collect()
}

/// **The one judge**, gunnar's.
fn leaks_in(text: &str) -> Vec<String> {
    let ws = words(text);
    let technical = ws.iter().any(|w| TECHNICAL_CONTEXT.contains(&w.as_str()));
    let mut out = Vec::new();
    for w in &ws {
        if !MONEY.contains(&w.as_str()) {
            continue;
        }
        if CONTEXTUAL.contains(&w.as_str()) && technical {
            continue;
        }
        if w == "stripe" && !(text.contains("Stripe") || text.contains("STRIPE") || text.contains("stripe.com")) {
            continue;
        }
        out.push(w.clone());
    }
    out.sort();
    out.dedup();
    out
}

#[test]
fn the_judge_reads_the_sentence() {
    assert_eq!(leaks_in("Buy 100:- 10 GB one month"), vec!["buy"]);
    assert!(leaks_in("payload").is_empty());
    assert_eq!(leaks_in("request a bigger quota"), vec!["quota"]);
    assert_eq!(leaks_in("your plan"), vec!["plan"]);
    assert!(leaks_in("off-stripe").is_empty());
    assert!(leaks_in("a reset plan for the passkeys").is_empty());
}

/// **The one file the web scan steps over**, and the step-over is asserted.
/// **The files a community build does not contain**, and therefore the only
/// ones allowed to say what it must never say.
///
/// A list rather than a single name since 2026-09-17, when the customer's own
/// account pane joined the "more room" panel behind `metered`. Every entry
/// widens the area this scanner is NOT watching, so each one is a decision:
/// the file must be genuinely absent from an open build, and
/// `only_one_module_is_behind_the_feature` in the same suite holds
/// `pages/mod.rs` to exactly this set so the two cannot drift apart.
// `purchase.rs` joined 2026-09-28: the customer's order words (price with
// VAT, "Pay with Stripe", the steps), host-side so they are tested, and gated
// `#[cfg(feature = "metered")]` in lib.rs exactly as the two pages are in
// pages/mod.rs.
const METERED_ONLY: &[&str] = &["pages/increase.rs", "pages/subscription.rs", "purchase.rs"];

fn web_src() -> PathBuf {
    Path::new(env!("CARGO_MANIFEST_DIR")).join("../gunnar-ui-web/src")
}

fn sources(dir: &Path) -> Vec<PathBuf> {
    let mut found = Vec::new();
    let mut stack = vec![dir.to_path_buf()];
    while let Some(here) = stack.pop() {
        let entries = std::fs::read_dir(&here)
            .unwrap_or_else(|e| panic!("cannot read {}: {e}", here.display()));
        for entry in entries.filter_map(Result::ok) {
            let path = entry.path();
            if path.is_dir() {
                stack.push(path);
            } else if path.extension().is_some_and(|e| e == "rs") {
                found.push(path);
            }
        }
    }
    found.sort();
    found
}

// ── the lexer, gunnar's ─────────────────────────────────────────────────────

#[derive(Debug, Clone, PartialEq)]
enum Tok {
    Str(String),
    Ident(String),
    Punct(char),
}

#[derive(Default)]
struct Scanned {
    literals: Vec<(usize, String)>,
    skipped_mods: Vec<String>,
}

fn lex(src: &str) -> Vec<(usize, Tok)> {
    let b: Vec<char> = src.chars().collect();
    let mut out = Vec::new();
    let (mut i, mut line) = (0usize, 1usize);
    while i < b.len() {
        let c = b[i];
        if c == '\n' {
            line += 1;
            i += 1;
            continue;
        }
        if c.is_whitespace() {
            i += 1;
            continue;
        }
        if c == '/' && b.get(i + 1) == Some(&'/') {
            while i < b.len() && b[i] != '\n' {
                i += 1;
            }
            continue;
        }
        if c == '/' && b.get(i + 1) == Some(&'*') {
            let mut depth = 0;
            while i < b.len() {
                if b[i] == '/' && b.get(i + 1) == Some(&'*') {
                    depth += 1;
                    i += 2;
                } else if b[i] == '*' && b.get(i + 1) == Some(&'/') {
                    depth -= 1;
                    i += 2;
                    if depth == 0 {
                        break;
                    }
                } else {
                    if b[i] == '\n' {
                        line += 1;
                    }
                    i += 1;
                }
            }
            continue;
        }
        let raw_start = if c == 'r' && matches!(b.get(i + 1), Some('"') | Some('#')) {
            Some(i + 1)
        } else if c == 'b' && b.get(i + 1) == Some(&'r') && matches!(b.get(i + 2), Some('"') | Some('#')) {
            Some(i + 2)
        } else {
            None
        };
        if let Some(mut j) = raw_start {
            let mut hashes = 0;
            while b.get(j) == Some(&'#') {
                hashes += 1;
                j += 1;
            }
            if b.get(j) == Some(&'"') {
                let start_line = line;
                j += 1;
                let mut text = String::new();
                while j < b.len() {
                    if b[j] == '"' && (0..hashes).all(|k| b.get(j + 1 + k) == Some(&'#')) {
                        j += 1 + hashes;
                        break;
                    }
                    if b[j] == '\n' {
                        line += 1;
                    }
                    text.push(b[j]);
                    j += 1;
                }
                out.push((start_line, Tok::Str(text)));
                i = j;
                continue;
            }
        }
        if c == '"' || (c == 'b' && b.get(i + 1) == Some(&'"')) {
            let start_line = line;
            let mut j = if c == 'b' { i + 2 } else { i + 1 };
            let mut text = String::new();
            while j < b.len() {
                match b[j] {
                    '\\' => {
                        if b.get(j + 1) == Some(&'\n') {
                            line += 1;
                            j += 2;
                            while j < b.len() && b[j].is_whitespace() {
                                if b[j] == '\n' {
                                    line += 1;
                                }
                                j += 1;
                            }
                            text.push(' ');
                            continue;
                        }
                        if let Some(n) = b.get(j + 1) {
                            text.push(match n {
                                'n' => '\n',
                                't' => ' ',
                                other => *other,
                            });
                        }
                        j += 2;
                    }
                    '"' => {
                        j += 1;
                        break;
                    }
                    ch => {
                        if ch == '\n' {
                            line += 1;
                        }
                        text.push(ch);
                        j += 1;
                    }
                }
            }
            out.push((start_line, Tok::Str(text)));
            i = j;
            continue;
        }
        if c == '\'' {
            if b.get(i + 1) == Some(&'\\') {
                let mut j = i + 2;
                while j < b.len() && b[j] != '\'' {
                    j += 1;
                }
                i = j + 1;
                continue;
            }
            if b.get(i + 2) == Some(&'\'') {
                i += 3;
                continue;
            }
            i += 1;
            continue;
        }
        if c.is_alphanumeric() || c == '_' {
            let mut j = i;
            let mut ident = String::new();
            while j < b.len() && (b[j].is_alphanumeric() || b[j] == '_') {
                ident.push(b[j]);
                j += 1;
            }
            out.push((line, Tok::Ident(ident)));
            i = j;
            continue;
        }
        out.push((line, Tok::Punct(c)));
        i += 1;
    }
    out
}

fn removes_from_open_build(attr: &[Tok]) -> bool {
    let t: String = attr
        .iter()
        .map(|t| match t {
            Tok::Str(s) => format!("\"{s}\""),
            Tok::Ident(s) => s.clone(),
            Tok::Punct(c) => c.to_string(),
        })
        .collect();
    if t == "test" || t == "tokio::test" || t.starts_with("tokio::test(") {
        return true;
    }
    if !t.starts_with("cfg(") || t.contains("not(") {
        return false;
    }
    t == "cfg(test)" || t.starts_with("cfg(all(test") || t.contains("feature=\"metered\"") || (t.starts_with("cfg(all(") && t.contains("test,"))
}

fn skip_item(toks: &[(usize, Tok)], mut i: usize, mods: &mut Vec<String>) -> usize {
    while matches!(toks.get(i), Some((_, Tok::Punct('#')))) && matches!(toks.get(i + 1), Some((_, Tok::Punct('[')))) {
        let mut depth = 0;
        let mut j = i + 1;
        while j < toks.len() {
            match toks[j].1 {
                Tok::Punct('[') => depth += 1,
                Tok::Punct(']') => {
                    depth -= 1;
                    if depth == 0 {
                        break;
                    }
                }
                _ => {}
            }
            j += 1;
        }
        i = j + 1;
    }
    let mut depth = 0i32;
    // `<…>` in the item's own header (`-> Result<A, B>`, `HashMap<K, V>`):
    // its comma does not end the item. Counted only outside brackets, where
    // a `<` cannot be a comparison.
    let mut angle = 0i32;
    let mut prev_mod = false;
    while i < toks.len() {
        match &toks[i].1 {
            Tok::Punct('<') if depth == 0 => angle += 1,
            Tok::Punct('>') if depth == 0 && angle > 0 && !matches!(toks.get(i.wrapping_sub(1)), Some((_, Tok::Punct('-' | '=')))) => angle -= 1,
            Tok::Ident(s) if s == "mod" => prev_mod = true,
            Tok::Ident(s) if prev_mod => {
                mods.push(s.clone());
                prev_mod = false;
            }
            Tok::Punct('(') | Tok::Punct('[') | Tok::Punct('{') => depth += 1,
            Tok::Punct(')') | Tok::Punct(']') => depth -= 1,
            Tok::Punct('}') => {
                depth -= 1;
                if depth == 0 {
                    return i + 1;
                }
                if depth < 0 {
                    return i;
                }
            }
            Tok::Punct(';') if depth == 0 => return i + 1,
            Tok::Punct(',') if depth == 0 && angle == 0 => return i + 1,
            _ => {}
        }
        i += 1;
    }
    i
}

fn scan_source(src: &str) -> Scanned {
    let toks = lex(src);
    let mut out = Scanned::default();
    let mut i = 0usize;
    while i < toks.len() {
        match &toks[i].1 {
            Tok::Punct('#') => {
                let inner = matches!(toks.get(i + 1), Some((_, Tok::Punct('!'))));
                let open = if inner { i + 2 } else { i + 1 };
                if !matches!(toks.get(open), Some((_, Tok::Punct('[')))) {
                    i += 1;
                    continue;
                }
                let mut depth = 0i32;
                let mut j = open;
                loop {
                    match toks.get(j).map(|t| &t.1) {
                        Some(Tok::Punct('[')) => depth += 1,
                        Some(Tok::Punct(']')) => {
                            depth -= 1;
                            if depth == 0 {
                                break;
                            }
                        }
                        None => break,
                        _ => {}
                    }
                    j += 1;
                }
                let end = j.min(toks.len());
                let attr: Vec<Tok> = toks[open + 1..end].iter().map(|t| t.1.clone()).collect();
                if removes_from_open_build(&attr) {
                    if inner {
                        return Scanned::default();
                    }
                    i = skip_item(&toks, j + 1, &mut out.skipped_mods);
                    continue;
                }
                let quiet = matches!(attr.first(), Some(Tok::Ident(s)) if s == "doc" || s == "cfg" || s == "cfg_attr");
                if !quiet {
                    for (line, t) in &toks[open + 1..end] {
                        if let Tok::Str(s) = t {
                            out.literals.push((*line, s.clone()));
                        }
                    }
                }
                i = j + 1;
            }
            Tok::Str(s) => {
                out.literals.push((toks[i].0, s.clone()));
                i += 1;
            }
            _ => i += 1,
        }
    }
    out
}

#[test]
fn the_scanner_skips_what_the_open_build_does_not_contain() {
    let s = scan_source(
        r####"
        /// a doc about billing
        fn open() { let _ = "free space"; }
        #[cfg(feature = "metered")]
        fn metered() { let _ = "paid"; }
        #[cfg(feature = "metered")]
        fn generic(raw: String) -> Result<String, Vec<u8>> { let _ = "checkout"; Ok(raw) }
        #[cfg(test)]
        mod tests { fn t() { let _ = "invoice"; } }
        #[cfg(feature = "metered")]
        pub mod increase;
        const C: char = '"';
        fn tail() { let _ = "after"; }
    "####,
    );
    let texts: Vec<&str> = s.literals.iter().map(|l| l.1.as_str()).collect();
    assert_eq!(texts, vec!["free space", "after"]);
    assert_eq!(s.skipped_mods, vec!["tests", "increase"]);
}

// ── the web half: never skips ───────────────────────────────────────────────

/// **The console's rendered words, in every build.**
#[test]
fn the_consoles_own_words_are_free_of_money() {
    let root = web_src();
    let mut leaks = Vec::new();
    let mut skipped = Vec::new();
    for path in sources(&root) {
        let relative = path
            .strip_prefix(&root)
            .expect("under the root")
            .to_string_lossy()
            .replace('\\', "/");
        if METERED_ONLY.contains(&relative.as_str()) {
            skipped.push(relative);
            continue;
        }
        let text = std::fs::read_to_string(&path)
            .unwrap_or_else(|e| panic!("cannot read {}: {e}", path.display()));
        for (line, literal) in scan_source(&text).literals {
            for word in leaks_in(&literal) {
                leaks.push(format!("{relative}:{line}: `{word}` in {literal:?}"));
            }
        }
    }

    // The bundle's non-Rust half: the page shell, the styles, the vector art.
    let crate_root = root.parent().expect("the web crate").to_path_buf();
    let mut shells = vec![crate_root.join("index.html")];
    for dir in ["styles", "assets"] {
        if let Ok(entries) = std::fs::read_dir(crate_root.join(dir)) {
            for e in entries.filter_map(Result::ok) {
                let p = e.path();
                if p.extension().is_some_and(|x| x == "css" || x == "svg" || x == "html" || x == "js") {
                    shells.push(p);
                }
            }
        }
    }
    for p in shells {
        let text = std::fs::read_to_string(&p).unwrap_or_else(|e| panic!("{}: {e}", p.display()));
        for (n, para) in text.split("\n\n").enumerate() {
            for word in leaks_in(para) {
                leaks.push(format!("{} ¶{}: `{word}` in {:?}", p.display(), n + 1, para.trim()));
            }
        }
    }

    skipped.sort();
    let mut expected: Vec<String> = METERED_ONLY.iter().map(|s| (*s).to_owned()).collect();
    expected.sort();
    assert_eq!(
        skipped,
        expected,
        "the skip list must match what is on disk exactly; a file that vanished, or a second \
         one that was quietly added, both land here"
    );
    assert!(
        leaks.is_empty(),
        "the gunnar console can render money vocabulary. gunnar takes no money and this console \
         is the same code for a self-hoster as for a paying tenant; every string below belongs \
         to monetize, which renders its own words through the seam.\n  {}",
        leaks.join("\n  ")
    );
}

/// **`pages/increase.rs` is the ONLY module behind the feature**, so the skip
/// above cannot silently grow into a hole.
#[test]
fn only_one_module_is_behind_the_feature() {
    let mod_rs = web_src().join("pages/mod.rs");
    let text = std::fs::read_to_string(&mod_rs).expect("the pages module");
    let gated: Vec<String> = text
        .lines()
        .collect::<Vec<_>>()
        .windows(2)
        .filter(|pair| pair[0].trim() == "#[cfg(feature = \"metered\")]")
        .map(|pair| pair[1].trim().to_owned())
        .collect();

    assert_eq!(
        gated,
        vec!["pub mod increase;".to_owned(), "pub mod subscription;".to_owned()],
        "the modules hidden behind `metered` must be exactly the ones \
         `the_consoles_own_words_are_free_of_money` skips (`METERED_ONLY`). Anything gated here \
         and not on that list is a file the money scanner silently stops watching"
    );
}

// ── the BFF half: what an OPEN gunnar-ui-server compiles ────────────────────

/// **Deliberate exceptions**, `(file suffix, fragment, reason)`. Each must
/// still match something.
#[cfg(not(feature = "metered"))]
const SERVER_EXCEPTIONS: &[(&str, &str, &str)] = &[
    (
        "src/security.rs",
        "payment=()",
        "the W3C Permissions-Policy feature token that DENIES the browser's Payment Request API \
         to every frame; a response header no person reads, and removing it would widen the policy",
    ),
    (
        "src/envfile.rs",
        "GUNNAR_UI_BUY_URL",
        "an env-file KEY the open build must still RECOGNISE in order to refuse an install that \
         sets it (`lifecycle::Refusal::ConfiguredToSellButCannot`) — the twin of gunnar's \
         `--monetize-pubkey`, kept so a community binary is never deployed behind a signup page \
         it cannot answer",
    ),
];

#[cfg(not(feature = "metered"))]
#[test]
fn the_open_bff_says_no_money() {
    let src = Path::new(env!("CARGO_MANIFEST_DIR")).join("src");
    let files: Vec<(PathBuf, Scanned)> = sources(&src)
        .into_iter()
        .filter(|p| {
            let n = p.file_name().unwrap().to_string_lossy();
            !(n == "tests.rs" || n.ends_with("_tests.rs"))
        })
        .map(|p| {
            let text = std::fs::read_to_string(&p).unwrap_or_default();
            let s = scan_source(&text);
            (p, s)
        })
        .collect();
    let mut dead: Vec<PathBuf> = Vec::new();
    for (p, s) in &files {
        let stem = p.file_stem().unwrap().to_string_lossy();
        let base = if matches!(stem.as_ref(), "lib" | "main" | "mod") {
            p.parent().unwrap().to_path_buf()
        } else {
            p.parent().unwrap().join(stem.as_ref())
        };
        for m in &s.skipped_mods {
            dead.push(base.join(format!("{m}.rs")));
            dead.push(base.join(m));
        }
    }
    let mut leaks = Vec::new();
    let mut used = vec![false; SERVER_EXCEPTIONS.len()];
    let mut scanned = 0usize;
    for (p, s) in &files {
        if dead.iter().any(|d| p == d || p.starts_with(d)) {
            continue;
        }
        scanned += 1;
        let rel = p.strip_prefix(Path::new(env!("CARGO_MANIFEST_DIR"))).unwrap_or(p).to_string_lossy().replace('\\', "/");
        for (line, lit) in &s.literals {
            let hits = leaks_in(lit);
            if hits.is_empty() {
                continue;
            }
            if let Some(k) = SERVER_EXCEPTIONS.iter().position(|(f, frag, _)| rel.ends_with(f) && lit.contains(frag)) {
                used[k] = true;
                continue;
            }
            leaks.push(format!("{rel}:{line}: {hits:?} in {lit:?}"));
        }
    }
    // ── the served ASSETS a live source `include_str!`s ────────────────────
    //
    // A page the BFF serves is words to a visitor, comments and markup
    // included (view-source is one keystroke). Any literal naming an
    // `.html`/`.css`/`.js`/`.svg` beside a LIVE source is that file; an asset
    // only a `metered` item includes is not in this list, by the same cfg
    // walk that stepped over the item.
    let mut assets = 0usize;
    for (p, s) in &files {
        if dead.iter().any(|d| p == d || p.starts_with(d)) {
            continue;
        }
        for (_, lit) in &s.literals {
            if !["html", "css", "js", "svg"].iter().any(|x| lit.ends_with(&format!(".{x}"))) || lit.contains('/') {
                continue;
            }
            let asset = p.parent().unwrap().join(lit);
            let Ok(text) = std::fs::read_to_string(&asset) else { continue };
            assets += 1;
            for (n, para) in text.split("\n\n").enumerate() {
                let hits = leaks_in(para);
                if !hits.is_empty() {
                    leaks.push(format!("{} ¶{}: {hits:?} in {:?}", asset.display(), n + 1, para.trim()));
                }
            }
        }
    }
    assert!(assets >= 1, "no served asset found; login.html is expected");
    assert!(scanned > 30, "only {scanned} files scanned; the layout moved");
    // The metered modules really were stepped over, by the attribute and not by luck.
    for metered in ["src/api/increase.rs", "src/monetize.rs"] {
        let path = Path::new(env!("CARGO_MANIFEST_DIR")).join(metered);
        assert!(
            dead.iter().any(|d| &path == d),
            "{metered} is expected to be a `#[cfg(feature = \"metered\")]` module"
        );
    }
    let stale: Vec<_> = SERVER_EXCEPTIONS.iter().zip(&used).filter(|(_, u)| !**u).map(|(e, _)| e.0).collect();
    assert!(stale.is_empty(), "exceptions that no longer match anything: {stale:?}");
    assert!(
        leaks.is_empty(),
        "an OPEN gunnar-ui-server compiles money vocabulary into what it says ({} literals). \
         Reword neutrally, or gate the code under `metered`.\n  {}",
        leaks.len(),
        leaks.join("\n  ")
    );
}

/// **A default build's router has no money route at all.**
#[cfg(not(feature = "metered"))]
#[tokio::test]
async fn the_open_console_serves_no_money_route() {
    use std::sync::Arc;
    use tower::ServiceExt as _;

    let signer = Arc::new(
        gunnar_ui_server::signer::Signer::new(ssh_key::PrivateKey::from(
            ssh_key::private::Ed25519Keypair::from_seed(&[3u8; 32]),
        ))
        .expect("a fixed seed makes a signer"),
    );
    let store = gunnar_ui_server::state::StateStore::open(
        tempfile::Builder::new()
            .suffix(".redb")
            .tempfile()
            .expect("a temp file")
            .into_temp_path()
            .keep()
            .expect("keep the path"),
    )
    .expect("a state store");
    let gunnar =
        gunnar_ui_server::grpc::Gunnar::connect_lazy("127.0.0.1:1", signer).expect("a lazy handle");
    let router = gunnar_ui_server::serve::router(Arc::new(gunnar_ui_server::app::App {
        webauthn: None,
        admin_token: None,
        events: gunnar_ui_server::sse::EventHub::detached(),
        gunnar,
        twin: None,
        guest: None,
        reviewers: Arc::new(gunnar_ui_server::reviewer::ReviewerKeys::new(
            "127.0.0.1:1".to_owned(),
            store.clone(),
        )),
        tenants: Arc::new(gunnar_ui_server::tenant::TenantKeys::new(
            "127.0.0.1:1".to_owned(),
            store,
        )),
        forges: Default::default(),
        imports: Default::default(),
        forge_apps: Default::default(),
        ui_dir: None,
        caches: gunnar_ui_server::api::Caches::default(),
        objects: Default::default(),
        readmes: Default::default(),
    }));

    for (method, path) in [
        // `/welcome` is where a payment vendor returns somebody who has just
        // bought an account, and `/auth/signup/status` is what that page polls.
        // Neither is in this binary.
        (http::Method::GET, "/welcome?session=cs_test_a1b2c3d4e5"),
        (
            http::Method::GET,
            "/auth/signup/status?session=cs_test_a1b2c3d4e5",
        ),
        (http::Method::GET, "/api/namespaces/alpha/increase"),
        (http::Method::POST, "/api/namespaces/alpha/increase"),
        (
            http::Method::GET,
            "/api/namespaces/alpha/increase/offer?meter=pack_bytes&cap=1",
        ),
        (
            http::Method::GET,
            "/api/namespaces/alpha/increase/status?reference=gunnar/alpha/2026-09-07",
        ),
        // The shop window and the button under it. Added 2026-09-19 with the
        // routes themselves: a money route that was not in this list would be
        // absent from an open build by accident rather than by test.
        (http::Method::GET, "/api/namespaces/alpha/increase/bundles"),
        (http::Method::POST, "/api/namespaces/alpha/increase/bundle"),
        (http::Method::GET, "/api/account/subscription/bundles"),
        (http::Method::POST, "/api/account/subscription/buy-bundle"),
    ] {
        let response = router
            .clone()
            .oneshot(
                http::Request::builder()
                    .method(method.clone())
                    .uri(path)
                    .body(axum::body::Body::empty())
                    .expect("a request"),
            )
            .await
            .expect("the router answers");
        assert_eq!(
            response.status(),
            http::StatusCode::NOT_FOUND,
            "the OPEN console answered {method} {path}; a build without `metered` must not have \
             the route at all"
        );
        let body = axum::body::to_bytes(response.into_body(), 1 << 20)
            .await
            .expect("a body");
        assert!(
            !String::from_utf8_lossy(&body).contains("<html"),
            "{method} {path} was answered with a document; the open console has no page here"
        );
    }
}