use std::path::PathBuf;
use std::process::Command;
fn bin() -> &'static str {
env!("CARGO_BIN_EXE_candor-scan")
}
fn make_crate(name: &str, src: &str) -> PathBuf {
let d = std::env::temp_dir().join(format!("candor-scan-cli-{name}-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&d);
std::fs::create_dir_all(d.join("src")).unwrap();
std::fs::write(d.join("Cargo.toml"), format!("[package]\nname = \"{name}\"\n")).unwrap();
std::fs::write(d.join("src/lib.rs"), src).unwrap();
d
}
#[test]
fn json_plus_policy_keeps_stdout_pure_json_and_routes_violations_to_stderr() {
let d = make_crate("jsonpol", "pub fn go() { std::process::Command::new(\"sh\").status().unwrap(); }");
let pp = d.join("candor.policy");
std::fs::write(&pp, "deny Exec\n").unwrap();
let out = Command::new(bin())
.arg(d.to_string_lossy().as_ref())
.arg("--json")
.arg("--policy")
.arg(pp.to_string_lossy().as_ref())
.output()
.expect("run candor-scan");
let _ = std::fs::remove_dir_all(&d);
assert_eq!(out.status.code(), Some(1), "a deny-Exec violation must exit 1");
let stdout = String::from_utf8(out.stdout).expect("utf8 stdout");
let parsed: Result<serde_json::Value, _> = serde_json::from_str(stdout.trim());
assert!(parsed.is_ok(), "stdout under --json --policy must parse as JSON, got:\n{stdout}");
let stderr = String::from_utf8(out.stderr).expect("utf8 stderr");
assert!(stderr.contains("AS-EFF") || stderr.contains("violation"),
"the policy violation must be reported on stderr, got stderr:\n{stderr}");
assert!(!stdout.contains("AS-EFF"),
"no policy/violation text may appear on the JSON stdout stream:\n{stdout}");
}
#[test]
fn valueless_trailing_policy_flag_errors_exit_2() {
let d = make_crate("nopolval", "pub fn go() {}");
let out = Command::new(bin())
.arg(d.to_string_lossy().as_ref())
.arg("--policy") .output()
.expect("run candor-scan");
let _ = std::fs::remove_dir_all(&d);
assert_eq!(out.status.code(), Some(2), "a valueless --policy must exit 2, not silently skip the gate");
}
#[test]
fn unreadable_policy_exits_2() {
let d = make_crate("unreadpol", "pub fn go() {}");
let missing = d.join("does-not-exist.policy");
let out = Command::new(bin())
.arg(d.to_string_lossy().as_ref())
.arg("--policy")
.arg(missing.to_string_lossy().as_ref())
.output()
.expect("run candor-scan");
let _ = std::fs::remove_dir_all(&d);
assert_eq!(out.status.code(), Some(2), "an unreadable policy must exit 2");
}
#[test]
fn json_plus_policy_over_unparseable_source_exits_2() {
let d = make_crate("brokenbin", "pub fn ok() {}\nthis is not valid rust @@@\n");
let pp = d.join("candor.policy");
std::fs::write(&pp, "deny Exec\n").unwrap();
let out = Command::new(bin())
.arg(d.to_string_lossy().as_ref())
.arg("--json")
.arg("--policy")
.arg(pp.to_string_lossy().as_ref())
.output()
.expect("run candor-scan");
let _ = std::fs::remove_dir_all(&d);
assert_eq!(out.status.code(), Some(2),
"a gate over an unparseable source must exit 2, never green");
let stdout = String::from_utf8(out.stdout).expect("utf8 stdout");
if !stdout.trim().is_empty() {
assert!(serde_json::from_str::<serde_json::Value>(stdout.trim()).is_ok(),
"any stdout under --json must remain valid JSON:\n{stdout}");
}
}