tests/cli_provenance.rs
//! CLI glue tests for `warehouse verify-provenance` that do NOT need a real sealed
//! blob — the flag-parsing + guard/bail path in `src/main.rs`
//! (`read_der_roots` empty-roots bail, the `--sha256` + `--blob-dir` guard, and the
//! "no blobs found" bail). The positive verify + policy-score path is driven
//! end-to-end at the library level (`provenance::verify_tests`), where a fixture can
//! be sealed with the upstream crates; here we only prove the CLI rejects malformed
//! invocations with a clear, non-zero exit.
//!
//! Gated on `provenance-verify` (the feature that compiles the verify verb).
#![cfg(all(feature = "provenance-verify", feature = "warehouse"))]
use std::path::PathBuf;
use std::process::Command;
use std::time::{SystemTime, UNIX_EPOCH};
fn temp_dir(tag: &str) -> PathBuf {
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_nanos();
let dir = std::env::temp_dir().join(format!("modgunn-provcli-{tag}-{nanos}"));
std::fs::create_dir_all(&dir).unwrap();
dir
}
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),
)
}
/// An empty `--roots` directory yields no certs → the verifier bails non-zero rather
/// than silently accepting an unverifiable blob.
#[test]
fn empty_roots_dir_bails() {
let base = temp_dir("emptyroots");
let roots = base.join("roots"); // empty dir
std::fs::create_dir_all(&roots).unwrap();
let blob = base.join("x.build-provenance.znippy");
std::fs::write(&blob, b"not a real seal").unwrap();
let wh = base.join("wh");
let (_out, err, code) = run(&[
"warehouse",
"verify-provenance",
"--warehouse",
wh.to_str().unwrap(),
"--roots",
roots.to_str().unwrap(),
"--blob",
blob.to_str().unwrap(),
]);
assert_ne!(code, 0, "empty roots must fail");
assert!(
err.contains("no certificate files"),
"clear bail reason: {err}"
);
std::fs::remove_dir_all(&base).ok();
}
/// `--sha256` pins ONE artifact, so combining it with a `--blob-dir` that holds more
/// than one blob is a usage error — the guard fires before any verification.
#[test]
fn sha256_with_multi_blob_dir_bails() {
let base = temp_dir("sha-guard");
// A dummy roots file (read as bytes; the CertStore is only built during verify,
// which the guard short-circuits before reaching).
let roots = base.join("ca.der");
std::fs::write(&roots, b"dummy-der-bytes").unwrap();
// A blob dir with TWO discovered blobs.
let blobdir = base.join("blobs");
std::fs::create_dir_all(&blobdir).unwrap();
std::fs::write(blobdir.join("a.build-provenance.znippy"), b"a").unwrap();
std::fs::write(blobdir.join("b.build-provenance.znippy"), b"b").unwrap();
let wh = base.join("wh");
let (_out, err, code) = run(&[
"warehouse",
"verify-provenance",
"--warehouse",
wh.to_str().unwrap(),
"--roots",
roots.to_str().unwrap(),
"--blob-dir",
blobdir.to_str().unwrap(),
"--sha256",
&"a".repeat(64),
]);
assert_ne!(code, 0, "--sha256 with a multi-blob dir must fail");
assert!(err.contains("single --blob"), "clear guard reason: {err}");
std::fs::remove_dir_all(&base).ok();
}
/// A `--blob-dir` with no `*.build-provenance.znippy` files bails "no blobs found".
#[test]
fn empty_blob_dir_bails() {
let base = temp_dir("noblobs");
let roots = base.join("ca.der");
std::fs::write(&roots, b"dummy").unwrap();
let blobdir = base.join("blobs");
std::fs::create_dir_all(&blobdir).unwrap();
std::fs::write(blobdir.join("unrelated.txt"), b"x").unwrap();
let wh = base.join("wh");
let (_out, err, code) = run(&[
"warehouse",
"verify-provenance",
"--warehouse",
wh.to_str().unwrap(),
"--roots",
roots.to_str().unwrap(),
"--blob-dir",
blobdir.to_str().unwrap(),
]);
assert_ne!(code, 0, "no blobs found must fail");
assert!(
err.contains("no ") && err.contains("blobs found"),
"clear bail reason: {err}"
);
std::fs::remove_dir_all(&base).ok();
}
// ── Positive-verify + policy-gate paths (need a REAL sealed blob) ──────────────
// Gated on `test-support`, which exposes `provenance::seal_provenance_fixture` —
// the same seal the in-crate `verify_tests` mint, now reusable out-of-crate. Run:
// `cargo test --features "warehouse test-support" --test cli_provenance`.
/// The VERIFIED branch end-to-end: seal a real fixture, drive `verify-provenance`,
/// assert it verifies + records the verdict, then read it back with `provenance-log`
/// — proving the verb's warehouse write actually landed.
#[cfg(feature = "test-support")]
#[test]
fn verify_provenance_records_verdict_and_reads_back() {
let base = temp_dir("verifyok");
let (blob, ca_der, art_sha) =
modgunn::provenance::seal_provenance_fixture(&base, "nordisk-ci").unwrap();
let roots = base.join("ca.der");
std::fs::write(&roots, &ca_der).unwrap();
let wh = base.join("wh");
let wh_s = wh.to_str().unwrap().to_string();
let (out, err, code) = run(&[
"warehouse",
"verify-provenance",
"--warehouse",
&wh_s,
"--roots",
roots.to_str().unwrap(),
"--blob",
blob.to_str().unwrap(),
"--sha256",
&art_sha,
]);
assert_eq!(code, 0, "a genuine seal verifies (stderr: {err})");
assert!(
out.contains("VERIFIED"),
"the blob is reported VERIFIED: {out}"
);
assert!(
out.contains("1 verdict(s) recorded"),
"the verdict is recorded: {out}"
);
// Read it back from the ledger (single-writer — the verify subprocess dropped
// its lock on exit).
let (log, _e, lcode) = run(&["warehouse", "provenance-log", "--warehouse", &wh_s]);
assert_eq!(lcode, 0, "provenance-log exits 0");
assert!(
log.contains("VERIFIED"),
"the verified row is read back: {log}"
);
assert!(
log.contains("1 verified"),
"the ledger holds one verified verdict: {log}"
);
std::fs::remove_dir_all(&base).ok();
}
/// The policy-gate exit-3 path: a genuinely-sealed L2 fixture fails a `--min-slsa 3`
/// requirement, so the verb emits a policy finding and exits 3 (distinct from the
/// exit-2 usage errors above) — the signal CI/promotion blocks on.
#[cfg(feature = "test-support")]
#[test]
fn verify_provenance_policy_gate_exits_3() {
let base = temp_dir("verifygate");
let (blob, ca_der, _art_sha) =
modgunn::provenance::seal_provenance_fixture(&base, "nordisk-ci").unwrap();
let roots = base.join("ca.der");
std::fs::write(&roots, &ca_der).unwrap();
let wh = base.join("wh");
let (out, _err, code) = run(&[
"warehouse",
"verify-provenance",
"--warehouse",
wh.to_str().unwrap(),
"--roots",
roots.to_str().unwrap(),
"--blob",
blob.to_str().unwrap(),
// The seal attests SLSA L2; requiring L3 must trip the gate.
"--min-slsa",
"3",
]);
assert_eq!(code, 3, "a provenance-policy violation exits 3: {out}");
assert!(
out.contains("policy"),
"a policy finding line is printed: {out}"
);
assert!(
out.contains("policy finding(s)"),
"the policy summary is printed: {out}"
);
// The seal itself is valid — the FAILURE is the policy, not the crypto.
assert!(out.contains("VERIFIED"), "the crypto still verifies: {out}");
std::fs::remove_dir_all(&base).ok();
}