use keyhog_scanner::testing;
#[test]
fn tfstate_flat_output_extracts_exact_pair() {
let text = r#"{"outputs":{"db_password":{"value":"tok-ABC-123"}}}"#;
let pairs = testing::parse_tfstate_tuples(text);
assert_eq!(
pairs,
vec![(
"tfstate-output.db_password".to_string(),
"tok-ABC-123".to_string(),
1,
)],
"a flat tfstate output must surface exactly one (tfstate-output.<key>, value, line) pair"
);
}
#[test]
fn tfstate_multiple_outputs_extract_in_btreemap_sorted_order() {
let text =
r#"{"outputs":{"db_password":{"value":"tok-ABC-123"},"api_key":{"value":"key-XYZ-789"}}}"#;
let pairs = testing::parse_tfstate_tuples(text);
assert_eq!(
pairs,
vec![
(
"tfstate-output.api_key".to_string(),
"key-XYZ-789".to_string(),
1,
),
(
"tfstate-output.db_password".to_string(),
"tok-ABC-123".to_string(),
1,
),
],
"two outputs must surface both pairs in BTreeMap-sorted key order"
);
}
#[test]
fn tfstate_resource_instance_attribute_extracts_exact_context() {
let text = r#"{"resources":[{"type":"aws_secret","name":"db","instances":[{"attributes":{"password":"res-secret-111"}}]}]}"#;
let pairs = testing::parse_tfstate_tuples(text);
assert_eq!(
pairs,
vec![(
"aws_secret.db.password".to_string(),
"res-secret-111".to_string(),
1,
)],
"a resource instance attribute must render as <type>.<name>.<attribute>"
);
}
#[test]
fn tfstate_complex_object_output_value_surfaces_nested_secret() {
let text = r#"{"outputs":{"credentials":{"value":{"api_key":"AKIA-nested-1234","note":"prod"},"sensitive":true}}}"#;
let pairs = testing::parse_tfstate_tuples(&text);
assert!(
pairs.contains(&(
"tfstate-output.credentials.api_key".to_string(),
"AKIA-nested-1234".to_string(),
1,
)),
"a secret nested inside a complex (object) tfstate output value must surface \
with a `tfstate-output.<name>.<attr>` context; got {pairs:?}"
);
assert!(
pairs.contains(&(
"tfstate-output.credentials.note".to_string(),
"prod".to_string(),
1,
)),
"sibling scalars of a complex output value must all surface; got {pairs:?}"
);
}
#[test]
fn tfstate_array_output_value_surfaces_each_nested_secret() {
let text = r#"{"outputs":{"keys":{"value":["list-secret-aaa","list-secret-bbb"]}}}"#;
let pairs = testing::parse_tfstate_tuples(&text);
assert!(
pairs.contains(&(
"tfstate-output.keys.[0]".to_string(),
"list-secret-aaa".to_string(),
1,
)) && pairs.contains(&(
"tfstate-output.keys.[1]".to_string(),
"list-secret-bbb".to_string(),
1,
)),
"each element of a list-valued tfstate output must surface with an indexed \
context; got {pairs:?}"
);
}
#[test]
fn tfstate_deeply_nested_object_output_value_surfaces_secret() {
let text = r#"{"outputs":{"cfg":{"value":{"db":{"conn":{"password":"deep-obj-secret-77"}}}}}}"#;
let pairs = testing::parse_tfstate_tuples(&text);
assert!(
pairs.contains(&(
"tfstate-output.cfg.db.conn.password".to_string(),
"deep-obj-secret-77".to_string(),
1,
)),
"a secret nested several object levels inside a complex output value must \
surface with the full dotted context; got {pairs:?}"
);
}
#[test]
fn tfstate_deep_but_within_limit_still_surfaces_buried_secret() {
let depth = 40;
let mut text = String::new();
for _ in 0..depth {
text.push_str(r#"{"values":"#);
}
text.push_str(r#"{"outputs":{"secret_out":{"value":"deep-nested-secret-42"}}}"#);
for _ in 0..depth {
text.push('}');
}
let pairs = testing::parse_tfstate_tuples(&text);
assert_eq!(
pairs,
vec![(
"tfstate-output.secret_out".to_string(),
"deep-nested-secret-42".to_string(),
1,
)],
"a secret nested 40 `values` levels deep must still surface exactly once"
);
}
#[test]
fn tfstate_nested_beyond_recursion_cap_surfaces_no_deeper_pair() {
let depth = 400;
let mut text = String::new();
for _ in 0..depth {
text.push_str(r#"{"values":"#);
}
text.push_str(r#"{"outputs":{"secret_out":{"value":"way-too-deep-secret"}}}"#);
for _ in 0..depth {
text.push('}');
}
let pairs = testing::parse_tfstate_tuples(&text);
assert_eq!(
pairs.len(),
0,
"a doc nested beyond the recursion/parse cap must surface zero pairs"
);
assert!(
!pairs.iter().any(|(_, v, _)| v == "way-too-deep-secret"),
"the over-deep secret must never be surfaced"
);
}
#[test]
fn tfstate_array_nesting_bomb_terminates_without_panic() {
let depth = 100_000;
let mut text = String::with_capacity(depth * 2 + 32);
for _ in 0..depth {
text.push('[');
}
text.push_str(r#""deep-array-secret""#);
for _ in 0..depth {
text.push(']');
}
let pairs = testing::parse_tfstate_tuples(&text);
assert_eq!(
pairs.len(),
0,
"a 100k-deep array bomb must parse-fail closed to zero pairs, not overflow"
);
}
#[test]
fn tfstate_malformed_json_yields_empty() {
let text = r#"{"outputs": {"x": {"value": "unterminated"#;
let pairs = testing::parse_tfstate_tuples(text);
assert_eq!(
pairs.len(),
0,
"malformed tfstate JSON must yield exactly zero extracted pairs"
);
}
#[test]
fn tfstate_non_object_root_yields_empty() {
let pairs = testing::parse_tfstate_tuples(r#""just-a-string""#);
assert_eq!(
pairs.len(),
0,
"a scalar-root tfstate has no extractable pairs"
);
}
#[test]
fn k8s_secret_flat_data_block_base64_decodes_exact_pair() {
let text = "apiVersion: v1\nkind: Secret\nmetadata:\n name: db-creds\ndata:\n password: cGFzc3dvcmQxMjM=\n";
let pairs = testing::parse_k8s_secret_tuples(text);
assert_eq!(
pairs,
vec![("password".to_string(), "password123".to_string(), 6)],
"a k8s Secret data: value must base64-decode to its plaintext, anchored to line 6"
);
}
#[test]
fn k8s_secret_string_data_surfaces_raw_value() {
let text = "kind: Secret\nstringData:\n db_pass: raw-secret-value\n";
let pairs = testing::parse_k8s_secret_tuples(text);
assert_eq!(
pairs,
vec![("db_pass".to_string(), "raw-secret-value".to_string(), 3)],
"a k8s Secret stringData: value must surface raw at line 3"
);
}
#[test]
fn k8s_secret_non_secret_kind_yields_empty() {
let text = "kind: ConfigMap\ndata:\n password: cGFzc3dvcmQxMjM=\n";
let pairs = testing::parse_k8s_secret_tuples(text);
assert_eq!(pairs.len(), 0, "a non-Secret kind must extract zero pairs");
}
#[test]
fn k8s_secret_deeply_nested_yaml_terminates_empty() {
let depth = 300;
let mut text = String::from("kind: Secret\ndata:\n blob: ");
for _ in 0..depth {
text.push('[');
}
for _ in 0..depth {
text.push(']');
}
text.push('\n');
let pairs = testing::parse_k8s_secret_tuples(&text);
assert_eq!(
pairs.len(),
0,
"a 300-deep YAML flow sequence must parse-fail closed to zero pairs"
);
}
#[test]
fn k8s_secret_malformed_yaml_yields_empty() {
let text = "kind: Secret\ndata:\n\tpassword: cGFzc3dvcmQxMjM=\n";
let pairs = testing::parse_k8s_secret_tuples(text);
assert_eq!(
pairs.len(),
0,
"tab-indented (illegal) YAML must fail closed to zero pairs"
);
}
#[test]
fn compose_environment_mapping_extracts_exact_pair() {
let text = "services:\n web:\n environment:\n API_KEY: secret-value-123\n";
let pairs = testing::parse_docker_compose_tuples(text);
assert_eq!(
pairs,
vec![("API_KEY".to_string(), "secret-value-123".to_string(), 4)],
"a compose environment mapping entry must surface (key, value, line 4)"
);
}
#[test]
fn compose_environment_sequence_splits_key_equals_value() {
let text = "services:\n api:\n environment:\n - DB_TOKEN=tok-987-xyz\n";
let pairs = testing::parse_docker_compose_tuples(text);
assert_eq!(
pairs,
vec![("DB_TOKEN".to_string(), "tok-987-xyz".to_string(), 4)],
"a compose `- KEY=VALUE` entry must split into (KEY, VALUE, line 4)"
);
}
#[test]
fn compose_deeply_nested_yaml_terminates_empty() {
let depth = 300;
let mut text = String::new();
for _ in 0..depth {
text.push_str("a: {");
}
text.push_str("environment: {LEAK: buried-compose-secret}");
for _ in 0..depth {
text.push('}');
}
text.push('\n');
let pairs = testing::parse_docker_compose_tuples(&text);
assert_eq!(
pairs.len(),
0,
"a 300-deep compose mapping must parse-fail closed, surfacing no buried env secret"
);
}
#[test]
fn hcl_variable_default_extracts_exact_pair() {
let text = "variable \"db_password\" {\n default = \"hunter2-secret-value\"\n}\n";
let pairs = testing::parse_hcl_tuples(text);
assert_eq!(
pairs,
vec![(
"db_password".to_string(),
"hunter2-secret-value".to_string(),
2,
)],
"a variable block default must attribute to the variable name at line 2"
);
}
#[test]
fn hcl_flat_assignment_extracts_exact_pair() {
let text = "api_key = \"flat-hcl-secret-42\"\n";
let pairs = testing::parse_hcl_tuples(text);
assert_eq!(
pairs,
vec![("api_key".to_string(), "flat-hcl-secret-42".to_string(), 1)],
"a flat tfvars assignment must surface (name, value, line 1)"
);
}
#[test]
fn hcl_closed_heredoc_joins_body_lines() {
let text = "config = <<EOF\nsecret-heredoc-line-1\nsecret-heredoc-line-2\nEOF\n";
let pairs = testing::parse_hcl_tuples(text);
assert_eq!(
pairs,
vec![(
"config".to_string(),
"secret-heredoc-line-1\nsecret-heredoc-line-2".to_string(),
2,
)],
"a closed heredoc must join its body with '\\n' and anchor to the first content line"
);
}
#[test]
fn hcl_unterminated_heredoc_is_bounded_and_surfaces_nothing() {
let mut text = String::from("config = <<EOF\n");
for i in 0..20_000 {
text.push_str(&format!("filler content line number {i}\n"));
}
let pairs = testing::parse_hcl_tuples(&text);
assert_eq!(
pairs.len(),
0,
"an unterminated 20k-line heredoc must surface zero pairs (bounded lookahead)"
);
}
#[test]
fn oversize_skip_counted_for_decode_through_k8s_secret() {
let text = "kind: Secret\ndata:\n x: eA==\n";
assert!(
testing::structured_oversize_skip_is_counted(text, Some("secret.yaml"), false),
"an oversize k8s Secret skip must count as a decode-through coverage gap"
);
}
#[test]
fn oversize_skip_not_counted_for_context_only_env() {
let text = "API_KEY=some-value\n";
assert!(
!testing::structured_oversize_skip_is_counted(text, Some(".env"), false),
"an oversize .env skip is lossless context-only and must NOT count"
);
}
#[test]
fn oversize_skip_not_counted_for_decode_derived_buffer() {
let text = "kind: Secret\ndata:\n x: eA==\n";
assert!(
!testing::structured_oversize_skip_is_counted(text, Some("secret.yaml"), true),
"a decode-derived buffer skip must never be counted (no false-loud telemetry)"
);
}