//! `wai audit report|check|verify` and `wai verify` on an audit bundle, against this crate's own program, //! on the same input: the stream-audit corpus (`live-conformance/audit/`) stored by the collector, //! a channel's report from that store, the report checked against its bundles, and each bundle //! verified. Each folded command prints what `wai-audit` prints and exits as it exits, byte for //! byte; `--owner` is `WAI_AUDIT_OWNER` to the program, and a fold that dropped it would be told //! apart here (the program refuses to check without an owner). //! //! `wai` is built from `wai/wai-rs` with the feature `quickstart`, into a target directory of this //! test's own. use ed25519_dalek::SigningKey; use serde_json::Value; use sha2::{Digest, Sha256}; use std::path::{Path, PathBuf}; use std::process::{Command, Output, Stdio}; use wai_audit::collector::{Collector, Config}; fn cargo() -> String { std::env::var("CARGO").unwrap_or_else(|_| "cargo".into()) } fn exe(name: &str) -> String { format!("{name}{}", std::env::consts::EXE_SUFFIX) } /// The `wai` binary, built with the feature quickstart. fn wai_bin() -> PathBuf { let manifest = Path::new(env!("CARGO_MANIFEST_DIR")).join("../wai-rs/Cargo.toml"); let target = Path::new(env!("CARGO_TARGET_TMPDIR")).join("wai-quickstart"); let status = Command::new(cargo()) .args(["build", "--locked", "--no-default-features", "--features", "quickstart", "--bin", "wai", "--manifest-path"]) .arg(&manifest) .env("CARGO_TARGET_DIR", &target) .status() .expect("cargo runs"); assert!(status.success(), "the wai binary builds with the feature quickstart"); target.join("debug").join(exe("wai")) } fn corpus() -> Value { let p = Path::new(env!("CARGO_MANIFEST_DIR")).join("../live-conformance/audit/vectors.json"); serde_json::from_slice(&std::fs::read(&p).unwrap()).unwrap() } /// The corpus's owner key: SHA-256 of its label and index 0 (its README). fn owner_seed() -> [u8; 32] { let mut h = Sha256::new(); h.update(b"wai:stream-audit/corpus/key\x00"); h.update([0u8]); h.finalize().into() } fn bundle_bytes(v: &Value) -> Vec { match &v["bundle_text"] { Value::String(s) => s.clone().into_bytes(), _ => serde_json::to_vec(&v["bundle"]).unwrap(), } } fn run(bin: &Path, args: &[&str], envs: &[(&str, &str)]) -> Output { let mut c = Command::new(bin); c.args(args).env_remove("WAI_AUDIT_OWNER").env_remove("WAI_AUDIT_KEY").stdin(Stdio::null()); for (k, v) in envs { c.env(k, v); } c.output().expect("it runs") } fn text(o: &Output) -> String { format!("{}{}", String::from_utf8_lossy(&o.stdout), String::from_utf8_lossy(&o.stderr)) } fn same(a: &Output, b: &Output, what: &str) { assert_eq!(a.status.code(), b.status.code(), "{what}: the exit status\n{}\n---\n{}", text(a), text(b)); assert_eq!(String::from_utf8_lossy(&a.stdout), String::from_utf8_lossy(&b.stdout), "{what}: standard output"); assert_eq!(String::from_utf8_lossy(&a.stderr), String::from_utf8_lossy(&b.stderr), "{what}: standard error"); } #[test] fn the_folds_print_what_wai_audit_prints() { let wai = wai_bin(); let me = Path::new(env!("CARGO_BIN_EXE_wai-audit")); let m = me.to_str().unwrap(); let doc = corpus(); let dir = Path::new(env!("CARGO_TARGET_TMPDIR")).join("fold"); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); // The collector's store, every session of the corpus posted to it, and its key's file. let channels: Vec = doc["channels"].as_array().unwrap().iter().map(|c| c.as_str().unwrap().to_string()).collect(); let data = dir.join("data"); let config = Config { channels: channels.clone(), data: data.clone(), allow_origins: vec![], max_bundle: 4 << 20, max_batch: 64 << 10, per_minute: u32::MAX }; let collector = Collector::new(config, SigningKey::from_bytes(&owner_seed())); for s in doc["sessions"].as_array().unwrap() { collector.submit(&bundle_bytes(s)); } let key = dir.join("owner.key"); std::fs::write(&key, owner_seed()).unwrap(); let k = key.to_str().unwrap(); let owner = doc["owner"].as_str().unwrap(); for r in doc["reports"].as_array().unwrap().iter().filter(|r| r["outcome"]["outcome"] == "accept") { let name = r["name"].as_str().unwrap(); let chan = r["channel"].as_str().unwrap(); let rep = &r["served"]["report"]; let (from, to) = (rep["from"].to_string(), rep["to"].to_string()); // The report: wai-audit's, byte for byte, and the corpus's. let theirs = run(me, &["report", data.to_str().unwrap(), chan, &from, &to], &[("WAI_AUDIT_KEY", k)]); let ours = run(&wai, &["audit", "report", data.to_str().unwrap(), chan, &from, &to, "--audit", m], &[("WAI_AUDIT_KEY", k)]); same(&ours, &theirs, &format!("{name}: report")); if !theirs.status.success() { continue; } let served: Value = serde_json::from_slice(&theirs.stdout).unwrap(); assert_eq!(served, r["served"], "{name}: the report is the corpus's"); let served_file = dir.join(format!("{name}.served.json")); std::fs::write(&served_file, &theirs.stdout).unwrap(); // With --json, the envelope carries its JSON. let j = run(&wai, &["audit", "report", data.to_str().unwrap(), chan, &from, &to, "--audit", m, "--json"], &[("WAI_AUDIT_KEY", k)]); let e: Value = serde_json::from_slice(&j.stdout).unwrap_or_else(|e| panic!("{e}: {}", text(&j))); assert_eq!(e["data"], served, "{name}: --json"); // The report checked against the bundles it reads. let bundles: Vec = r["sessions"] .as_array() .unwrap() .iter() .map(|n| { let s = doc["sessions"].as_array().unwrap().iter().find(|s| s["name"] == *n).unwrap(); let f = dir.join(format!("{}.bundle.json", n.as_str().unwrap())); std::fs::write(&f, bundle_bytes(s)).unwrap(); f }) .collect(); let mut check = vec!["check", served_file.to_str().unwrap(), chan]; check.extend(bundles.iter().map(|b| b.to_str().unwrap())); let theirs = run(me, &check, &[("WAI_AUDIT_OWNER", owner)]); let mut fold = vec!["audit"]; fold.extend(&check); fold.extend(["--owner", owner, "--audit", m]); let ours = run(&wai, &fold, &[]); same(&ours, &theirs, &format!("{name}: check")); assert!(theirs.status.success(), "{name}: {}", text(&theirs)); // Without the owner the program refuses: a fold that dropped --owner is told apart. let ownerless = run(me, &check, &[]); assert_ne!(ownerless.status.code(), ours.status.code(), "{name}: the owner matters to the check"); // A bundle left out: refused by both alike. if bundles.len() > 1 { let short: Vec<&str> = check[..check.len() - 1].to_vec(); let theirs = run(me, &short, &[("WAI_AUDIT_OWNER", owner)]); let mut f = vec!["audit"]; f.extend(&short); f.extend(["--owner", owner, "--audit", m]); let ours = run(&wai, &f, &[]); assert_eq!(theirs.status.code(), Some(1), "{name}: {}", text(&theirs)); same(&ours, &theirs, &format!("{name}: a bundle left out")); } } // Each bundle verified: `wai audit verify` is wai-audit verify, byte for byte, and wai verify's // answer for the bundle is the same JSON. let mut flags = Vec::new(); for c in &channels { flags.extend(["--channel", c.as_str()]); } let mut n = 0; for s in doc["sessions"].as_array().unwrap().iter().filter(|s| s["bundle"].is_object()) { let name = s["name"].as_str().unwrap(); let f = dir.join(format!("verify-{name}.json")); std::fs::write(&f, bundle_bytes(s)).unwrap(); let file = f.to_str().unwrap(); let mut a = vec!["verify", file]; a.extend(channels.iter().map(String::as_str)); let theirs = run(me, &a, &[("WAI_AUDIT_OWNER", owner)]); let mut folded = vec!["audit"]; folded.extend(&a); folded.extend(["--owner", owner, "--audit", m]); same(&run(&wai, &folded, &[]), &theirs, &format!("{name}: audit verify")); let mut v = vec!["verify", file, "--owner", owner]; v.extend(&flags); let ours = run(&wai, &v, &[]); assert_eq!(ours.status.code(), theirs.status.code(), "{name}: {}\n{}", text(&ours), text(&theirs)); let body: String = String::from_utf8_lossy(&ours.stdout).lines().skip(1).take_while(|l| !l.starts_with("wai verify:")).map(|l| l.strip_prefix(" ").unwrap_or(l)).collect::>().join("\n"); assert_eq!(format!("{body}\n"), String::from_utf8_lossy(&theirs.stdout), "{name}: wai-audit verify's JSON"); n += 1; } assert!(n > 80, "{n} bundles"); }