xtask/tests/cve_report.rs
//! The CVE gate's verdict as data (`xtask/src/cve.rs`): it is clean only when every
//! sub-check exited 0, the report names each failure and the advisory database's age,
//! and the age is read from the newest fetch of any database under `$CARGO_HOME`.
//!
//! Its own test binary: one test points `$CARGO_HOME` at a scratch directory.
use std::path::PathBuf;
use std::time::Duration;
use xtask::cve::{advisory_db_age, advisory_db_dir, Check, CveReport};
fn check(name: &'static str, exit: Option<i32>, output: &str) -> Check {
Check {
name,
exit,
output: output.to_string(),
}
}
fn report(adv: Option<i32>, bans: Option<i32>, sources: Option<i32>, output: &str) -> CveReport {
CveReport {
advisories: check("advisories", adv, output),
bans: check("bans", bans, ""),
sources: check("sources", sources, ""),
db_age: Some(Duration::from_secs(3 * 24 * 60 * 60 + 5)),
offline: true,
}
}
const FIRED: &str = "error[vulnerability]: a flaw\n ┌─ Cargo.lock:1:1\n │\n ├ ID: RUSTSEC-2024-0421\n ├ Advisory: https://rustsec.org/advisories/RUSTSEC-2024-0421\n";
#[test]
fn only_three_clean_exits_are_a_clean_gate() {
assert!(report(Some(0), Some(0), Some(0), "").is_clean());
assert!(!report(Some(1), Some(0), Some(0), FIRED).is_clean());
assert!(!report(Some(0), Some(2), Some(0), "").is_clean());
assert!(
!report(Some(0), Some(0), None, "").is_clean(),
"a killed check is not a pass"
);
assert!(!check("x", None, "").passed());
}
#[test]
fn the_report_names_each_failure_the_firing_ids_and_the_database() {
let page = report(Some(1), Some(0), None, FIRED).render();
assert!(page.contains(" advisories: FAILED (exit 1)\n"), "{page}");
assert!(page.contains(" bans: ok\n"), "{page}");
assert!(
page.contains(" sources: FAILED (killed, no exit code)\n"),
"{page}"
);
assert!(
page.contains(" advisories firing: RUSTSEC-2024-0421\n"),
"{page}"
);
assert!(
page.contains(" advisory db: 3 day(s) old, read offline\n"),
"{page}"
);
let mut clean = report(Some(0), Some(0), Some(0), "");
clean.db_age = None;
clean.offline = false;
let page = clean.render();
assert!(!page.contains("firing"), "{page}");
assert!(
page.contains("advisory db: NOT PRESENT, fetched for this run"),
"{page}"
);
}
struct Scratch(PathBuf);
impl Drop for Scratch {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.0);
}
}
/// The age is the newest `FETCH_HEAD` (else `HEAD`) of any database; no database is
/// `None`, never a fresh age.
#[test]
fn the_database_age_is_the_newest_fetch_under_cargo_home() {
let home = std::env::temp_dir().join(format!("gunnar-cve-home-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&home);
let _s = Scratch(home.clone());
std::env::set_var("CARGO_HOME", &home);
assert_eq!(advisory_db_dir(), home.join("advisory-dbs"));
assert_eq!(advisory_db_age(), None, "no database directory at all");
std::fs::create_dir_all(home.join("advisory-dbs/empty-0000")).unwrap();
assert_eq!(advisory_db_age(), None, "a database with no git markers");
let old = home.join("advisory-dbs/old-1111/.git");
std::fs::create_dir_all(&old).unwrap();
std::fs::write(old.join("HEAD"), "ref: refs/heads/main\n").unwrap();
let past = std::time::SystemTime::now() - Duration::from_secs(30 * 24 * 60 * 60);
std::fs::File::options()
.write(true)
.open(old.join("HEAD"))
.unwrap()
.set_modified(past)
.unwrap();
let age = advisory_db_age().expect("the cloned database");
assert!(age >= Duration::from_secs(29 * 24 * 60 * 60), "{age:?}");
let fresh = home.join("advisory-dbs/fresh-2222/.git");
std::fs::create_dir_all(&fresh).unwrap();
std::fs::write(fresh.join("FETCH_HEAD"), "x\n").unwrap();
let age = advisory_db_age().expect("the fetched database");
assert!(
age < Duration::from_secs(60 * 60),
"the newest database wins: {age:?}"
);
std::env::remove_var("CARGO_HOME");
}