tests/bifrost_cli_contract.rs
//! **`skidbladnir bifrost` must not be quiet about two things.**
//!
//! 1. **A flag the verb cannot honour.** `--bundle` was parsed for every verb and used
//! by only one, so `bifrost rollback --bundle release-0.5.0.znippy` answered
//! "no prior generation retained for `nornir-server` to roll back to" — a reply to a
//! question the operator had not asked. The bundle they named was dropped without a
//! word. Same for `--trust-root`.
//!
//! 2. **Whether the bundle's AUTHORSHIP was checked.** `refresh_product` verifies an
//! ed25519 signature only when a `TrustRoot` is pinned. The CLI hardcoded
//! `trust_root: None` while its own missing-argument message called the file "the
//! signed payload". Integrity (manifest root + per-entry content hashes) was
//! checked; authorship was not, and nothing said so.
//!
//! The authenticity test asserts **on-disk applied output**, not the printed line: the
//! same bundle is deployed twice, once with and once without a pinned trust root, and
//! the generation directory must materialise in exactly one of the two runs. A run that
//! only compared stderr would stay green for a build that printed the warning and then
//! passed `None` anyway — which is precisely the bug.
use std::path::{Path, PathBuf};
use std::process::{Command, Output};
use skidbladnir::archive::{Archive, TarZstd};
const EXE: &str = env!("CARGO_BIN_EXE_skidbladnir");
/// A real, packable product tree carrying the entrypoint
/// `ComponentSpec::nornir_server()` declares (`linux-x86_64/nornir-server`), packed
/// with the same [`TarZstd`] engine the CLI uses — so the bundle is genuinely valid and
/// verification passes for a reason, not because nothing was checked.
fn nornir_server_bundle(dir: &Path) -> PathBuf {
let staging = dir.join("staging");
std::fs::create_dir_all(staging.join("linux-x86_64")).unwrap();
std::fs::write(
staging.join("linux-x86_64/nornir-server"),
b"#!/bin/sh\nexit 0\n",
)
.unwrap();
let bundle = dir.join("nornir-server-test.tar.zst");
TarZstd::new()
.pack(&staging, "test-0.0.1", &bundle)
.expect("pack a valid bundle");
assert!(
skidbladnir::archive::manifest_path(&bundle).exists(),
"the fixture must carry a sidecar manifest, or `verify` would fail for the wrong reason"
);
bundle
}
fn bifrost(args: &[&str]) -> Output {
Command::new(EXE)
.arg("bifrost")
.args(args)
.output()
.expect("run skidbladnir bifrost")
}
/// Is there an unfolded `nornir-server` under this prefix? The generation directory is
/// the APPLIED result of getting past verification — it exists only if `refresh_product`
/// reached step 2.
fn unfolded_entrypoints(prefix: &Path) -> Vec<PathBuf> {
fn walk(d: &Path, out: &mut Vec<PathBuf>) {
let Ok(rd) = std::fs::read_dir(d) else { return };
for e in rd.flatten() {
// NOT `p.is_dir()`: that follows symlinks, and `software/current` is a
// symlink to the version dir, so the walk descended the same generation
// twice and reported two entrypoints for one install (measured: left: 2).
let Ok(ft) = e.file_type() else { continue };
if ft.is_symlink() {
continue;
}
let p = e.path();
if ft.is_dir() {
walk(&p, out);
} else if p.file_name().and_then(|n| n.to_str()) == Some("nornir-server") {
out.push(p);
}
}
}
let mut out = Vec::new();
walk(&prefix.join("software"), &mut out);
out
}
#[test]
fn rollback_refuses_a_bundle_instead_of_silently_dropping_it() {
let tmp = tempfile::tempdir().unwrap();
let out = bifrost(&[
"rollback",
"--bundle",
"/nonexistent/release-0.5.0.znippy",
"--prefix",
tmp.path().join("p").to_str().unwrap(),
"--root",
tmp.path().join("r").to_str().unwrap(),
]);
let stderr = String::from_utf8_lossy(&out.stderr).to_string();
assert!(
stderr.contains("cannot honour `--bundle`"),
"a rollback cannot act on a bundle, so naming one must be refused BY NAME; got:\n{stderr}"
);
assert!(
!stderr.contains("no prior generation retained"),
"the old behaviour: the bundle was dropped and the operator got an answer to a \
different question:\n{stderr}"
);
assert_ne!(out.status.code(), Some(0), "refusing must not exit 0");
}
#[test]
fn rollback_refuses_a_trust_root_it_would_never_use() {
let tmp = tempfile::tempdir().unwrap();
let out = bifrost(&[
"rollback",
"--trust-root",
&"aa".repeat(32),
"--prefix",
tmp.path().join("p").to_str().unwrap(),
]);
let stderr = String::from_utf8_lossy(&out.stderr).to_string();
assert!(
stderr.contains("cannot honour `--trust-root`"),
"a rollback verifies nothing, so accepting a trust root would imply a check that \
never happens; got:\n{stderr}"
);
}
#[test]
fn a_bad_trust_root_is_rejected_before_the_unit_is_touched() {
let tmp = tempfile::tempdir().unwrap();
let bundle = nornir_server_bundle(tmp.path());
let prefix = tmp.path().join("p");
let out = bifrost(&[
"deploy",
"--bundle",
bundle.to_str().unwrap(),
"--trust-root",
"deadbeef", // 4 bytes, not 32
"--prefix",
prefix.to_str().unwrap(),
"--root",
tmp.path().join("r").to_str().unwrap(),
]);
let stderr = String::from_utf8_lossy(&out.stderr).to_string();
assert!(
stderr.contains("32-byte ed25519 public key"),
"a malformed fingerprint must name the expected form; got:\n{stderr}"
);
assert!(
unfolded_entrypoints(&prefix).is_empty(),
"nothing may be unfolded when the trust root itself is unusable"
);
}
/// **The applied proof that `--trust-root` is threaded through, not printed and
/// discarded.** The same valid, UNSIGNED bundle is deployed twice.
///
/// - no `--trust-root` → integrity-only gate passes → the generation IS unfolded
/// (the deploy then fails further on, at the systemd D-Bus restart, which needs root
/// — irrelevant here; getting that far is the point).
/// - `--trust-root <any key>` → authenticity is now MANDATORY, the bundle carries no
/// signature → refused at verify → NOTHING is unfolded.
///
/// With the old hardcoded `trust_root: None` both runs unfold, so the second assertion
/// is the red. Asserting the on-disk difference — rather than the printed notice — is
/// what makes this test unable to pass for a build that merely warns.
#[test]
fn a_pinned_trust_root_actually_refuses_an_unsigned_bundle() {
let tmp = tempfile::tempdir().unwrap();
let bundle = nornir_server_bundle(tmp.path());
let root = tmp.path().join("r");
let unpinned_prefix = tmp.path().join("unpinned");
let out = bifrost(&[
"deploy",
"--bundle",
bundle.to_str().unwrap(),
"--prefix",
unpinned_prefix.to_str().unwrap(),
"--root",
root.to_str().unwrap(),
]);
let unpinned_stderr = String::from_utf8_lossy(&out.stderr).to_string();
assert!(
unpinned_stderr.contains("authenticity NOT verified"),
"an integrity-only deploy must SAY that authorship was not checked; got:\n{unpinned_stderr}"
);
let unfolded = unfolded_entrypoints(&unpinned_prefix);
assert_eq!(
unfolded.len(),
1,
"the control arm must get PAST verification and unfold the generation, or the \
comparison below proves nothing; stderr was:\n{unpinned_stderr}"
);
let pinned_prefix = tmp.path().join("pinned");
let out = bifrost(&[
"deploy",
"--bundle",
bundle.to_str().unwrap(),
"--trust-root",
&"11".repeat(32),
"--prefix",
pinned_prefix.to_str().unwrap(),
"--root",
root.to_str().unwrap(),
]);
let pinned_stderr = String::from_utf8_lossy(&out.stderr).to_string();
assert!(
pinned_stderr.contains("authenticity PINNED"),
"a pinned deploy must name the CA it will accept; got:\n{pinned_stderr}"
);
assert!(
unfolded_entrypoints(&pinned_prefix).is_empty(),
"an UNSIGNED bundle must be refused when a trust root is pinned — nothing may reach \
disk. Anything unfolded here means the CLI printed the pin and then passed \
`trust_root: None`; stderr was:\n{pinned_stderr}"
);
assert_ne!(
out.status.code(),
Some(0),
"a refused deploy must not exit 0"
);
}