use std::path::PathBuf;
use std::process::{Command, Output};
use tempfile::TempDir;
fn binary() -> PathBuf {
PathBuf::from(env!("CARGO_BIN_EXE_keyhog"))
}
fn scan_cpu(path: &std::path::Path) -> Output {
Command::new(binary())
.args(["scan", "--backend", "cpu", "--daemon=off"])
.arg(path)
.env("NO_COLOR", "1")
.output()
.expect("spawn keyhog scan")
}
fn scan_default(path: &std::path::Path) -> Output {
Command::new(binary())
.args(["scan", "--daemon=off"])
.arg(path)
.env("NO_COLOR", "1")
.output()
.expect("spawn keyhog scan")
}
fn combined(output: &Output) -> String {
format!(
"{}\n{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
)
}
fn assert_exited_by_code(output: &Output, context: &str) {
#[cfg(unix)]
{
use std::os::unix::process::ExitStatusExt;
assert!(
output.status.code().is_some(),
"{context}: process died by signal {:?} (a fail-closed path must \
exit with a code, not crash); output:\n{}",
output.status.signal(),
combined(output)
);
}
#[cfg(not(unix))]
assert!(
output.status.code().is_some(),
"{context}: no exit code; output:\n{}",
combined(output)
);
}
fn dir_with_fixture() -> TempDir {
let dir = TempDir::new().expect("tempdir");
std::fs::write(
dir.path().join("secret.env"),
concat!("AWS_ACCESS_KEY_ID = \"AKIA", "QYLPMN5HFIQR7XYA\"\n"),
)
.expect("write fixture");
dir
}
#[test]
fn expired_allowlist_exits_by_code_not_signal() {
let dir = dir_with_fixture();
std::fs::write(
dir.path().join(".keyhogignore"),
"detector:aws-access-key ; expires=1970-01-01 ; reason=\"old waiver\"\n",
)
.expect("write expired allowlist");
let output = scan_cpu(dir.path());
assert_exited_by_code(&output, "expired allowlist");
}
#[test]
fn expired_allowlist_is_exit_2() {
let dir = dir_with_fixture();
std::fs::write(
dir.path().join(".keyhogignore"),
"detector:aws-access-key ; expires=1970-01-01 ; reason=\"old waiver\"\n",
)
.expect("write expired allowlist");
let output = scan_cpu(dir.path());
assert_eq!(
output.status.code(),
Some(2),
"expired allowlist is a user-error exit; output:\n{}",
combined(&output)
);
}
#[test]
fn nonexistent_path_exits_by_code_not_signal() {
let missing = PathBuf::from("/keyhog-nonexistent-scan-target-xyz");
let output = scan_cpu(&missing);
assert_exited_by_code(&output, "nonexistent scan path");
}
#[test]
fn nonexistent_path_is_exit_2() {
let missing = PathBuf::from("/keyhog-nonexistent-scan-target-xyz");
let output = scan_cpu(&missing);
assert_eq!(
output.status.code(),
Some(2),
"a missing scan path is a user error; output:\n{}",
combined(&output)
);
}
#[test]
fn nonexistent_path_diagnostic_is_operator_visible() {
let missing = PathBuf::from("/keyhog-nonexistent-scan-target-xyz");
let output = scan_cpu(&missing);
let text = combined(&output);
assert!(
text.contains("does not exist"),
"the missing-path error must explain the fix; got:\n{text}"
);
}
#[test]
fn expired_allowlist_diagnostic_is_operator_visible() {
let dir = dir_with_fixture();
std::fs::write(
dir.path().join(".keyhogignore"),
"detector:aws-access-key ; expires=1970-01-01 ; reason=\"old waiver\"\n",
)
.expect("write expired allowlist");
let output = scan_cpu(dir.path());
let text = combined(&output);
assert!(
text.contains("expired allowlist policy")
&& text.contains("refusing to scan with stale suppressions"),
"the expired-allowlist error must stay operator-visible even though the \
scan now exits cleanly; got:\n{text}"
);
}
#[test]
fn lockdown_required_without_flag_exits_by_code_not_signal() {
let dir = dir_with_fixture();
std::fs::write(
dir.path().join(".keyhog.toml"),
"[lockdown]\nrequire = true\n",
)
.expect("write lockdown config");
let output = scan_cpu(dir.path());
assert_exited_by_code(&output, "lockdown required but --lockdown absent");
}
#[test]
fn empty_dir_scan_is_incomplete_coverage_exit_13() {
let dir = TempDir::new().expect("tempdir");
let output = scan_cpu(dir.path());
let combined_output = combined(&output);
assert_eq!(
output.status.code(),
Some(13),
"an empty directory scan exits 13 (incomplete coverage), not clean; output:\n{combined_output}"
);
assert!(
combined_output.contains("ZERO bytes") || combined_output.contains("covered nothing"),
"empty-directory incomplete coverage must be operator-visible; output:\n{combined_output}"
);
}
#[test]
fn content_scan_completes_cleanly_baseline() {
let dir = dir_with_fixture();
let output = scan_cpu(dir.path());
assert!(
matches!(output.status.code(), Some(0) | Some(1)),
"a clean content scan exits 0 or 1, not a crash or setup error; got \
{:?}\n{}",
output.status.code(),
combined(&output)
);
}
#[test]
fn repeated_error_scans_never_signal() {
let missing = PathBuf::from("/keyhog-nonexistent-scan-target-xyz");
for i in 0..5 {
let output = scan_cpu(&missing);
assert_exited_by_code(&output, &format!("nonexistent scan path (iteration {i})"));
}
}
#[test]
fn nonexistent_path_default_backend_exits_by_code_not_signal() {
let missing = PathBuf::from("/keyhog-nonexistent-scan-target-xyz");
let output = scan_default(&missing);
assert_exited_by_code(&output, "nonexistent scan path (default backend)");
}
#[test]
fn nonexistent_path_default_backend_is_exit_2() {
let missing = PathBuf::from("/keyhog-nonexistent-scan-target-xyz");
let output = scan_default(&missing);
assert_eq!(
output.status.code(),
Some(2),
"a missing scan path is a user error on any backend; output:\n{}",
combined(&output)
);
}
#[test]
fn nonexistent_extra_path_exits_by_code_not_signal() {
let good = dir_with_fixture();
let output = Command::new(binary())
.args(["scan", "--backend", "cpu", "--daemon=off"])
.arg(good.path())
.arg("/keyhog-nonexistent-extra-root-xyz")
.env("NO_COLOR", "1")
.output()
.expect("spawn keyhog scan");
assert_exited_by_code(&output, "typo'd extra scan root");
assert_eq!(
output.status.code(),
Some(2),
"a missing extra root is a user error; output:\n{}",
combined(&output)
);
}
#[test]
fn default_backend_valid_dir_exits_by_code_not_signal() {
let dir = dir_with_fixture();
let output = scan_default(dir.path());
assert_exited_by_code(&output, "autoroute scan of a valid dir");
}