use keyhog::testing::{CliTestApi as _, API};
use keyhog_core::{MatchLocation, RawMatch, Severity};
use std::sync::Arc;
#[test]
fn startup_summary_includes_detector_count() {
assert!(!API.format_gpu_summary().is_empty());
}
#[test]
fn startup_banner_is_the_wordmark_without_icon_art() {
let plain = API.write_banner(false, 922).expect("plain banner");
let expected = format!(
" K E Y H O G\n ───────────\n v{} · secret scanner · 922 detectors\n by santh\n\n",
env!("CARGO_PKG_VERSION")
);
assert_eq!(plain, expected.as_bytes());
let colored = API.write_banner(true, 922).expect("colored banner");
let colored = String::from_utf8(colored).expect("banner is UTF-8");
assert!(colored.contains("K E Y H O G"));
assert!(colored.contains("922 detectors"));
assert!(
colored.contains("\u{1b}["),
"colored banner must emit ANSI styling"
);
}
#[test]
fn render_credential_honors_show_secrets_both_directions() {
let short = keyhog_core::SensitiveString::from("secret12");
assert_eq!(API.render_credential(&short, true), "secret12");
assert_eq!(API.render_credential(&short, false), "****");
let long = keyhog_core::SensitiveString::from(concat!("AK", "IAQYLPMN5HFIQR7XYA"));
assert_eq!(
API.render_credential(&long, true),
concat!("AK", "IAQYLPMN5HFIQR7XYA")
);
let redacted = API.render_credential(&long, false);
assert!(
redacted.contains("...") && redacted != "AKIAQYLPMN5HFIQR7XYA",
"redacted form must mask the interior: {redacted}"
);
}
fn test_hash() -> [u8; 32] {
[7u8; 32]
}
#[test]
fn find_config_file_returns_none_for_empty_temp_dir() {
let dir = tempfile::tempdir().unwrap();
assert!(API.find_config_file(Some(dir.path())).is_none());
}
#[test]
fn filter_inline_suppressions_keeps_non_filesystem_matches() {
let m = RawMatch {
detector_id: Arc::from("demo"),
detector_name: Arc::from("Demo"),
service: Arc::from("demo"),
severity: Severity::Low,
credential: keyhog_core::SensitiveString::from("abc"),
credential_hash: test_hash().into(),
companions: Default::default(),
location: MatchLocation {
source: Arc::from("stdin"),
file_path: None,
line: None,
offset: 0,
commit: None,
author: None,
date: None,
},
entropy: None,
confidence: None,
};
let kept = API.filter_inline_suppressions(vec![m]);
assert_eq!(kept.len(), 1);
}
#[test]
fn filter_inline_suppressions_drops_directive_marked_line() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("with_ignore.rs");
std::fs::write(
&path,
"let x = 1; // keyhog:ignore\nlet token = \"secret\";\n",
)
.unwrap();
let m = RawMatch {
detector_id: Arc::from("demo"),
detector_name: Arc::from("Demo"),
service: Arc::from("demo"),
severity: Severity::Low,
credential: keyhog_core::SensitiveString::from("secret"),
credential_hash: test_hash().into(),
companions: Default::default(),
location: MatchLocation {
source: Arc::from("filesystem"),
file_path: Some(Arc::from(path.to_string_lossy().as_ref())),
line: Some(2),
offset: 0,
commit: None,
author: None,
date: None,
},
entropy: None,
confidence: None,
};
let kept = API.filter_inline_suppressions(vec![m]);
assert!(kept.is_empty());
}
#[test]
fn filter_inline_suppressions_keeps_findings_after_read_error() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("invalid_utf8_after_ignore.rs");
std::fs::write(
&path,
b"// keyhog:ignore\n\xff\xfe\nlet token = \"secret\";\n",
)
.unwrap();
let m = RawMatch {
detector_id: Arc::from("demo"),
detector_name: Arc::from("Demo"),
service: Arc::from("demo"),
severity: Severity::Low,
credential: keyhog_core::SensitiveString::from("secret"),
credential_hash: test_hash().into(),
companions: Default::default(),
location: MatchLocation {
source: Arc::from("filesystem"),
file_path: Some(Arc::from(path.to_string_lossy().as_ref())),
line: Some(3),
offset: 0,
commit: None,
author: None,
date: None,
},
entropy: None,
confidence: None,
};
let kept = API.filter_inline_suppressions(vec![m]);
assert_eq!(
kept.len(),
1,
"a read error after an inline directive must not reuse stale suppression context for later findings"
);
}
#[test]
fn filter_inline_suppressions_supports_migrated_directives() {
for directive in &["keyhog:allow", "gitleaks:allow", "betterleaks:allow"] {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("with_directive.rs");
std::fs::write(
&path,
format!("let x = 1; // {}\nlet token = \"secret\";\n", directive),
)
.unwrap();
let m = RawMatch {
detector_id: Arc::from("demo"),
detector_name: Arc::from("Demo"),
service: Arc::from("demo"),
severity: Severity::Low,
credential: keyhog_core::SensitiveString::from("secret"),
credential_hash: test_hash().into(),
companions: Default::default(),
location: MatchLocation {
source: Arc::from("filesystem"),
file_path: Some(Arc::from(path.to_string_lossy().as_ref())),
line: Some(2),
offset: 0,
commit: None,
author: None,
date: None,
},
entropy: None,
confidence: None,
};
let kept = API.filter_inline_suppressions(vec![m]);
assert!(
kept.is_empty(),
"directive '{}' did not suppress finding",
directive
);
}
}
#[test]
fn filter_inline_suppressions_with_detector_suffix() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("with_suffix.rs");
std::fs::write(
&path,
"let x = 1; // keyhog:ignore detector=aws-access-key\nlet token = \"secret\";\n",
)
.unwrap();
let m_match = RawMatch {
detector_id: Arc::from("aws-access-key"),
detector_name: Arc::from("AWS Access Key"),
service: Arc::from("aws"),
severity: Severity::Low,
credential: keyhog_core::SensitiveString::from("secret"),
credential_hash: test_hash().into(),
companions: Default::default(),
location: MatchLocation {
source: Arc::from("filesystem"),
file_path: Some(Arc::from(path.to_string_lossy().as_ref())),
line: Some(2),
offset: 0,
commit: None,
author: None,
date: None,
},
entropy: None,
confidence: None,
};
let kept_match = API.filter_inline_suppressions(vec![m_match]);
assert!(
kept_match.is_empty(),
"matching detector should be suppressed"
);
let m_nonmatch = RawMatch {
detector_id: Arc::from("stripe-secret-key"),
detector_name: Arc::from("Stripe Secret Key"),
service: Arc::from("stripe"),
severity: Severity::Low,
credential: keyhog_core::SensitiveString::from("secret"),
credential_hash: test_hash().into(),
companions: Default::default(),
location: MatchLocation {
source: Arc::from("filesystem"),
file_path: Some(Arc::from(path.to_string_lossy().as_ref())),
line: Some(2),
offset: 0,
commit: None,
author: None,
date: None,
},
entropy: None,
confidence: None,
};
let kept_nonmatch = API.filter_inline_suppressions(vec![m_nonmatch]);
assert_eq!(
kept_nonmatch.len(),
1,
"non-matching detector should not be suppressed"
);
}
#[test]
fn filter_inline_suppressions_detector_suffix_is_case_insensitive() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("mixed_case.rs");
std::fs::write(
&path,
"let x = 1; // keyhog:ignore detector=aws-access-key\nlet token = \"secret\";\n",
)
.unwrap();
let make = |id: &str| RawMatch {
detector_id: Arc::from(id),
detector_name: Arc::from("AWS Access Key"),
service: Arc::from("aws"),
severity: Severity::Low,
credential: keyhog_core::SensitiveString::from("secret"),
credential_hash: test_hash().into(),
companions: Default::default(),
location: MatchLocation {
source: Arc::from("filesystem"),
file_path: Some(Arc::from(path.to_string_lossy().as_ref())),
line: Some(2),
offset: 0,
commit: None,
author: None,
date: None,
},
entropy: None,
confidence: None,
};
let kept_match = API.filter_inline_suppressions(vec![make("AWS-Access-KEY")]);
assert!(
kept_match.is_empty(),
"mixed-case detector id matching the directive target must be suppressed"
);
let kept_nonmatch = API.filter_inline_suppressions(vec![make("GCP-Service-Key")]);
assert_eq!(
kept_nonmatch.len(),
1,
"a detector id that does not match the directive target must survive"
);
}