tests/cli_zone.rs
//! **ZONE routing tests** — the shared constellation-wide environment axis (the ONE
//! `nornir_zone::Zone` leaf, consumed not twinned, L5) threaded through modgunn's
//! CLI `--zone demo|test|prod` into its warehouse **data namespace**.
//!
//! The contract these pin (RED-when-broken):
//! * modgunn's OWN data lands in the selected zone's isolated namespace
//! `Zone::namespace("modgunn")` = `modgunn_<zone>` (so a `test` populate never
//! touches `demo`/`prod`);
//! * `--zone` defaults to `demo`, so every pre-zone invocation is unchanged (additive);
//! * the pre-zone [`Warehouse::open`] still addresses the legacy `nornir` namespace
//! (existing library consumers unaffected);
//! * an unknown `--zone` value is a clear hard error, never a silent wrong environment.
//!
//! Gated on `warehouse` (the binary's `required-features`): without it the CLI is not
//! built and this crate compiles to nothing.
#![cfg(feature = "warehouse")]
use std::path::PathBuf;
use std::process::Command;
use std::time::{SystemTime, UNIX_EPOCH};
use modgunn::core::Severity;
use modgunn::scan::Advisory;
use modgunn::warehouse::{Warehouse, Zone};
/// A unique scratch catalog root (skade is single-writer per catalog).
fn temp_root(tag: &str) -> PathBuf {
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_nanos();
let dir = std::env::temp_dir().join(format!("modgunn-zone-{tag}-{nanos}"));
std::fs::create_dir_all(&dir).unwrap();
dir
}
/// Run the compiled `modgunn` binary with `args`; return `(stdout, stderr, code)`.
fn run(args: &[&str]) -> (String, String, i32) {
let out = Command::new(env!("CARGO_BIN_EXE_modgunn"))
.args(args)
.output()
.expect("spawn modgunn binary");
(
String::from_utf8_lossy(&out.stdout).into_owned(),
String::from_utf8_lossy(&out.stderr).into_owned(),
out.status.code().unwrap_or(-1),
)
}
/// **RED-when-broken: the zone threads through to the data namespace.**
/// `open_in_zone(Test)` MUST route modgunn's own tables to `modgunn_test`, `Demo` to
/// `modgunn_demo` (the default), `Prod` to `modgunn_prod` — and each is distinct, so
/// two zones never collide. The legacy `open` MUST stay on the pre-zone `nornir`
/// namespace (existing consumers unchanged). If the zone failed to thread, these
/// namespaces would coincide (isolation lost) or drift from `modgunn_<zone>`.
#[test]
fn zone_routes_modgunn_namespace() {
let root = temp_root("ns");
let demo = Warehouse::open_in_zone(&root, Zone::Demo).unwrap();
assert_eq!(demo.namespace(), "modgunn_demo", "Demo → modgunn_demo");
drop(demo);
let test = Warehouse::open_in_zone(&root, Zone::Test).unwrap();
assert_eq!(test.namespace(), "modgunn_test", "Test → modgunn_test");
drop(test);
let prod = Warehouse::open_in_zone(&root, Zone::Prod).unwrap();
assert_eq!(prod.namespace(), "modgunn_prod", "Prod → modgunn_prod");
drop(prod);
// The pre-zone entry stays on the legacy shared namespace (byte-for-byte additive).
let legacy = Warehouse::open(&root).unwrap();
assert_eq!(
legacy.namespace(),
"nornir",
"legacy open keeps the nornir namespace"
);
drop(legacy);
// The leaf's own default is Demo, so an absent `--zone` lands in modgunn_demo.
assert_eq!(Zone::default(), Zone::Demo);
assert_eq!(Zone::default().namespace("modgunn"), "modgunn_demo");
std::fs::remove_dir_all(&root).ok();
}
/// **RED-when-broken: the CLI `--zone` ISOLATES modgunn's data end-to-end.**
/// Seed advisories into the *test* zone in-process, then drive the real binary:
/// `warehouse stats --zone test` MUST see them (the CLI threaded `--zone test` into
/// the `modgunn_test` namespace), while the DEFAULT (no `--zone`, i.e. demo) MUST see
/// zero — the two zones are isolated. If the CLI ignored `--zone`, the test-zone read
/// would come back empty (RED) or the demo read would leak the test data (RED).
#[test]
fn cli_zone_isolates_advisory_data() {
let root = temp_root("iso");
let root_s = root.to_str().unwrap().to_string();
// Populate ONLY the test zone (modgunn_test namespace).
{
let wh = Warehouse::open_in_zone(&root, Zone::Test).unwrap();
wh.ensure_modgunn_tables().unwrap();
wh.append_advisories(
"rev-test",
&[Advisory::basic(
"CVE-2023-9999",
"cargo",
"openssl",
vec!["0.10.55".into()],
Severity::High,
"test-zone advisory",
)],
)
.unwrap();
} // drop releases the writer lock before the subprocess opens the same root.
// The CLI, told `--zone test`, reads the SAME modgunn_test namespace → sees it.
let (out_test, err_test, code_test) = run(&[
"warehouse",
"stats",
"--warehouse",
&root_s,
"--zone",
"test",
]);
assert_eq!(
code_test, 0,
"stats --zone test exits 0 (stderr: {err_test})"
);
assert!(
out_test.contains("1 advisories"),
"the test zone sees its own advisory: {out_test}"
);
// The DEFAULT (demo) zone is a DIFFERENT namespace (modgunn_demo) → sees nothing.
let (out_demo, _e, code_demo) = run(&["warehouse", "stats", "--warehouse", &root_s]);
assert_eq!(code_demo, 0, "default (demo) stats exits 0");
assert!(
out_demo.contains("0 advisories"),
"the demo zone is isolated from the test zone's data: {out_demo}"
);
// Explicit `--zone demo` matches the default (also isolated / empty).
let (out_demo2, _e, _c) = run(&[
"warehouse",
"stats",
"--warehouse",
&root_s,
"--zone",
"demo",
]);
assert!(
out_demo2.contains("0 advisories"),
"explicit demo == default: {out_demo2}"
);
std::fs::remove_dir_all(&root).ok();
}
/// **RED-when-broken: an unknown `--zone` value is a clear hard error.** A typo'd zone
/// must NOT silently fall through to demo (which could hide data in the wrong
/// environment): the CLI exits non-zero and names the valid zones.
#[test]
fn cli_rejects_unknown_zone() {
let root = temp_root("badzone");
let root_s = root.to_str().unwrap().to_string();
let (_out, err, code) = run(&[
"warehouse",
"stats",
"--warehouse",
&root_s,
"--zone",
"staging-typo",
]);
assert_ne!(
code, 0,
"an unknown --zone fails (never a silent demo fall-through)"
);
assert!(
err.contains("unknown --zone"),
"error names the bad flag: {err}"
);
assert!(
err.contains("demo") && err.contains("test") && err.contains("prod"),
"error lists the valid zones: {err}"
);
std::fs::remove_dir_all(&root).ok();
}