use keyhog_scanner::testing::fragment_cache::{
shard_index_drift_probe, FragmentCache, SecretFragment,
};
use std::sync::Arc;
use zeroize::Zeroizing;
fn frag(prefix: &str, var: &str, value: &str, line: usize, path: &str) -> SecretFragment {
SecretFragment {
prefix: prefix.to_string(),
var_name: var.to_string(),
value: Zeroizing::new(value.to_string()),
line,
path: Some(Arc::from(path)),
}
}
#[test]
fn same_file_fragments_within_window_reassemble() {
let cache = FragmentCache::new(64);
let dir = "/repo/.env.d";
let first = cache.record_and_reassemble(frag(
"awskey",
"AWS_ACCESS_KEY_PART1",
"AKIA0000000000000000", 10,
&format!("{dir}/keys.env"),
));
assert!(
first.is_empty(),
"single-fragment scope must not yield candidates, got {} candidates",
first.len()
);
let joined = cache.record_and_reassemble(frag(
"awskey",
"AWS_ACCESS_KEY_PART2",
"BBBBBBBBBBBBBBBBBBBB",
20,
&format!("{dir}/keys.env"),
));
let glued: Vec<String> = joined.iter().map(|z| z.to_string()).collect();
assert!(
glued
.iter()
.any(|g| g == "AKIA0000000000000000BBBBBBBBBBBBBBBBBBBB"), "expected forward AKIA||BBBB reassembly in {:?}",
glued
);
assert!(
glued
.iter()
.any(|g| g == "BBBBBBBBBBBBBBBBBBBBAKIA0000000000000000"), "expected reverse BBBB||AKIA reassembly in {:?}",
glued
);
assert_eq!(
glued.len(),
2,
"exactly two ordered pairs expected, got {}: {:?}",
glued.len(),
glued
);
}
#[test]
fn cross_file_fragments_do_not_reassemble() {
let cache = FragmentCache::new(64);
let dir = "/repo/.env.d";
let _ = cache.record_and_reassemble(frag(
"awskey",
"AWS_ACCESS_KEY",
"AKIA0000000000000000", 6,
&format!("{dir}/file_a.yaml"),
));
let cross = cache.record_and_reassemble(frag(
"awskey",
"AWS_ACCESS_KEY",
"BBBBBBBBBBBBBBBBBBBB",
6,
&format!("{dir}/file_b.sh"),
));
assert!(
cross.is_empty(),
"cross-file reassembly must be suppressed, got {} candidates: {:?}",
cross.len(),
cross.iter().map(|z| z.to_string()).collect::<Vec<_>>()
);
}
#[test]
fn same_file_fragments_outside_window_do_not_reassemble() {
let cache = FragmentCache::new(64);
let path = "/repo/huge.env";
let _ = cache.record_and_reassemble(frag(
"awskey",
"AWS_ACCESS_KEY_A",
"AKIA0000000000000000", 1,
path,
));
let far = cache.record_and_reassemble(frag(
"awskey",
"AWS_ACCESS_KEY_B",
"BBBBBBBBBBBBBBBBBBBB",
500,
path,
));
assert!(
far.is_empty(),
"out-of-window same-file reassembly must be suppressed, got {:?}",
far.iter().map(|z| z.to_string()).collect::<Vec<_>>()
);
}
#[test]
fn shard_index_of_matches_joined_key_hash() {
let cases = [
("", ""),
("awskey", ""),
("", "/repo/.env"),
("awskey", "/repo/.env.d/keys.env"),
("gh\0pat", "/a/b\0c/d"),
("prefix-with-emoji-\u{1f511}", "/path/\u{e9}t\u{e9}/clef"),
("a", "b"),
];
for (prefix, scope) in cases {
let (slice_pair, joined_key) = shard_index_drift_probe(prefix, scope);
assert_eq!(
slice_pair, joined_key,
"shard hash drift for prefix={prefix:?} scope={scope:?}"
);
}
}
#[test]
fn capacity_tracks_live_workload_and_resets_after_clear() {
let cache = FragmentCache::new(512);
assert_eq!(cache.storage_for_test(), (0, 64, 512));
let prefix = "workload";
let target_shard = shard_index_drift_probe(prefix, "/repo/seed.env").0;
let mut paths = Vec::new();
for index in 0..10_000 {
let path = format!("/repo/candidate-{index}.env");
if shard_index_drift_probe(prefix, &path).0 == target_shard {
paths.push(path);
if paths.len() == 4 {
break;
}
}
}
assert_eq!(paths.len(), 4, "test must find four colliding shard keys");
for (line, path) in paths.iter().enumerate() {
let candidates =
cache.record_and_reassemble(frag(prefix, "WORKLOAD_PART", "abcd", line, path));
assert!(candidates.is_empty());
}
assert_eq!(
cache.storage_for_test(),
(4, 67, 512),
"only the touched shard should grow from one to four rows"
);
cache.clear();
assert_eq!(
cache.storage_for_test(),
(0, 64, 512),
"completed workload state and shard capacity must be released"
);
}