use std::path::PathBuf;
use std::process::Command;
use tempfile::TempDir;
fn binary() -> PathBuf {
PathBuf::from(env!("CARGO_BIN_EXE_keyhog"))
}
fn scan_text_file(content: &str, extra_args: &[&str]) -> (String, String, Option<i32>) {
let dir = TempDir::new().expect("tempdir");
let path = dir.path().join("config.txt");
std::fs::write(&path, content).expect("write fixture");
let output = Command::new(binary())
.arg("scan")
.args(extra_args)
.arg("--format")
.arg("json")
.arg("--no-daemon")
.arg(&path)
.output()
.expect("spawn keyhog scan");
(
String::from_utf8_lossy(&output.stdout).into_owned(),
String::from_utf8_lossy(&output.stderr).into_owned(),
output.status.code(),
)
}
#[test]
fn precision_mode_conflicts_with_deep() {
let (_o, err, code) = scan_text_file("ordinary content\n", &["--precision", "--deep"]);
assert_eq!(
code,
Some(2),
"clap usage error (exit 2) expected for conflicting --precision --deep; got {code:?}"
);
assert!(
err.contains("cannot be used with")
|| err.to_lowercase().contains("conflict")
|| err.to_lowercase().contains("precision"),
"the usage error must name the conflict; stderr={err}"
);
}
#[test]
fn precision_mode_conflicts_with_no_entropy() {
let (_o, err, code) = scan_text_file("ordinary content\n", &["--precision", "--no-entropy"]);
assert_eq!(
code,
Some(2),
"clap usage error (exit 2) expected for conflicting --precision --no-entropy; got {code:?}"
);
assert!(
err.contains("cannot be used with")
|| err.to_lowercase().contains("conflict")
|| err.to_lowercase().contains("precision"),
"the usage error must name the conflict; stderr={err}"
);
}
#[test]
fn precision_mode_conflicts_with_no_decode() {
let (_o, err, code) = scan_text_file("ordinary content\n", &["--precision", "--no-decode"]);
assert_eq!(
code,
Some(2),
"clap usage error (exit 2) expected for conflicting --precision --no-decode; got {code:?}"
);
assert!(
err.contains("cannot be used with")
|| err.to_lowercase().contains("conflict")
|| err.to_lowercase().contains("precision"),
"the usage error must name the conflict; stderr={err}"
);
}
#[test]
fn precision_mode_exits_zero_on_clean_file() {
let fixture = "fn main() { println!(\"hello world\"); }\n";
let (_stdout, _stderr, code) = scan_text_file(fixture, &["--precision"]);
assert_eq!(
code,
Some(0),
"precision mode must exit 0 on a clean file; got {code:?}"
);
}
#[test]
fn precision_mode_exits_one_on_findings() {
let fixture = concat!("aws_secret_access_key = \"kP8xQ2mNvR7tZ4wL9bYsH3jD6fG1cA0eXuViK5oT\"\n");
let (_stdout, _stderr, code) = scan_text_file(fixture, &["--precision"]);
assert_eq!(
code,
Some(1),
"precision mode must exit 1 when findings are detected; got {code:?}"
);
}
#[test]
fn precision_mode_enforces_0_85_floor_on_weak_credentials() {
let fixture = concat!("AWS_ACCESS_KEY_ID = \"AKIA", "QYLPMN5HGT3KZ7WB\"\n");
let (def_out, _e, def_code) = scan_text_file(fixture, &[]);
assert_eq!(def_code, Some(1), "default mode must find the weak AWS key");
let def_findings: serde_json::Value =
serde_json::from_str(&def_out).expect("default stdout is JSON");
let def_arr = def_findings.as_array().expect("array");
assert!(
!def_arr.is_empty(),
"default must surface the AKIA key; got {def_out}"
);
let (prec_out, _e2, prec_code) = scan_text_file(fixture, &["--precision"]);
let prec_findings: serde_json::Value =
serde_json::from_str(&prec_out).expect("precision stdout is JSON");
let prec_arr = prec_findings.as_array().expect("array");
assert!(
prec_arr.is_empty(),
"precision mode must drop the AKIA key (conf < 0.85); got {prec_out}"
);
assert!(
prec_code.is_some_and(|c| c == 0),
"precision mode must exit 0 when all findings are below the floor; got {prec_code:?}"
);
}
#[test]
fn precision_mode_clamps_detector_floor_up_to_0_85() {
let dir = TempDir::new().expect("tempdir");
let aws_secret =
concat!("aws_secret_access_key = \"kP8xQ2mNvR7tZ4wL9bYsH3jD6fG1cA0eXuViK5oT\"\n");
std::fs::write(dir.path().join("planted.txt"), aws_secret).expect("write fixture");
let config = "[detector.aws-secret-access-key]\nmin_confidence = 0.25\n";
std::fs::write(dir.path().join(".keyhog.toml"), config).expect("write config");
let output = Command::new(binary())
.args(["scan", "--precision", "--format", "json", "--no-daemon"])
.arg(dir.path())
.output()
.expect("spawn precision scan with config");
assert_eq!(
output.status.code(),
Some(1),
"precision mode must find the AWS secret (it is above 0.85); stderr={}",
String::from_utf8_lossy(&output.stderr)
);
let findings: serde_json::Value =
serde_json::from_slice(&output.stdout).expect("stdout is JSON");
let arr = findings.as_array().expect("array");
assert!(
arr.iter()
.any(|f| f.get("detector_id").and_then(|v| v.as_str()) == Some("aws-secret-access-key")),
"precision must find the AWS secret; got {arr:?}"
);
}
#[test]
fn precision_mode_respects_min_confidence_override() {
let fixture = concat!("aws_secret_access_key = \"kP8xQ2mNvR7tZ4wL9bYsH3jD6fG1cA0eXuViK5oT\"\n");
let (out, _e, code) = scan_text_file(fixture, &["--precision", "--min-confidence", "0.9"]);
assert_eq!(
code,
Some(1),
"precision 0.9 must find the high-confidence AWS key"
);
let findings: serde_json::Value = serde_json::from_str(&out).expect("JSON");
let arr = findings.as_array().expect("array");
assert!(
!arr.is_empty(),
"precision 0.9 must find the AWS secret; got {out}"
);
}
#[test]
fn precision_mode_json_schema_carries_required_fields() {
let fixture = "GH_TOKEN = \"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\"\n";
let (stdout, _stderr, _code) = scan_text_file(fixture, &["--precision"]);
let findings: serde_json::Value = serde_json::from_str(&stdout).expect("stdout is valid JSON");
let arr = findings.as_array().expect("findings is a JSON array");
assert!(
!arr.is_empty(),
"precision must find the checksum-valid GitHub token (floored at 0.9)"
);
for f in arr {
for required in [
"detector_id",
"detector_name",
"service",
"severity",
"credential_redacted",
"credential_hash",
"location",
"verification",
] {
assert!(
f.get(required).is_some(),
"finding is missing required field `{required}`: {f}",
);
}
let loc = f.get("location").unwrap();
for required in ["source", "file_path", "line", "offset"] {
assert!(
loc.get(required).is_some(),
"location is missing required field `{required}`: {loc}",
);
}
}
}
#[test]
fn precision_mode_is_stricter_than_default_overall_reduction() {
let fixture = concat!(
"aws_secret_access_key = \"kP8xQ2mNvR7tZ4wL9bYsH3jD6fG1cA0eXuViK5oT\"\n",
"DATABASE_PASSWORD = \"admin123\"\n",
);
let (def_out, _, _) = scan_text_file(fixture, &[]);
let (prec_out, _, _) = scan_text_file(fixture, &["--precision"]);
let def_findings: serde_json::Value = serde_json::from_str(&def_out).expect("default JSON");
let prec_findings: serde_json::Value = serde_json::from_str(&prec_out).expect("precision JSON");
let def_count = def_findings.as_array().map(|a| a.len()).unwrap_or(0);
let prec_count = prec_findings.as_array().map(|a| a.len()).unwrap_or(0);
assert!(
prec_count < def_count,
"precision must be stricter; default={def_count}, precision={prec_count}"
);
assert!(
prec_count > 0,
"precision must still find the high-confidence AWS secret; got {prec_count}"
);
}