use keyhog_core::testing::{CoreTestApi, TestApi};
use keyhog_core::{max_decode_depth_limit, ScanConfig};
const F64_EPS: f64 = 1e-12;
const BASE_TOML: &str = r#"
min_confidence = 0.5
max_decode_depth = 4
entropy_enabled = true
entropy_in_source_files = false
entropy_threshold = 4.0
min_secret_len = 20
max_file_size = 1000
dedup = "Credential"
ml_enabled = true
ml_weight = 0.5
unicode_normalization = true
validate_decode = true
max_decode_bytes = 4096
max_matches_per_chunk = 100
known_prefixes = []
secret_keywords = []
test_keywords = []
placeholder_keywords = []
"#;
fn parse(toml_text: &str) -> ScanConfig {
toml::from_str::<ScanConfig>(toml_text)
.unwrap_or_else(|e| panic!("expected TOML to deserialize into ScanConfig, got: {e}"))
}
#[test]
fn default_config_passes_validation() {
let cfg = ScanConfig::default();
assert_eq!(TestApi.scan_config_validate(&cfg), Ok(()));
assert!((cfg.min_confidence - 0.40).abs() < F64_EPS);
assert_eq!(cfg.max_decode_depth, 10);
}
#[test]
fn min_confidence_zero_and_one_boundaries_accepted() {
let mut cfg = ScanConfig::default();
cfg.min_confidence = 0.0;
assert_eq!(TestApi.scan_config_validate(&cfg), Ok(()));
cfg.min_confidence = 1.0;
assert_eq!(TestApi.scan_config_validate(&cfg), Ok(()));
}
#[test]
fn min_confidence_negative_zero_accepted() {
let mut cfg = ScanConfig::default();
cfg.min_confidence = -0.0;
assert_eq!(TestApi.scan_config_validate(&cfg), Ok(()));
}
#[test]
fn min_confidence_above_one_rejected_exact_error() {
let mut cfg = ScanConfig::default();
cfg.min_confidence = 1.5;
assert_eq!(
TestApi.scan_config_validate(&cfg),
Err("min_confidence must be between 0.0 and 1.0, found 1.5".to_string())
);
}
#[test]
fn min_confidence_negative_rejected_exact_error() {
let mut cfg = ScanConfig::default();
cfg.min_confidence = -0.25;
assert_eq!(
TestApi.scan_config_validate(&cfg),
Err("min_confidence must be between 0.0 and 1.0, found -0.25".to_string())
);
}
#[test]
fn min_confidence_nan_rejected_exact_error() {
let mut cfg = ScanConfig::default();
cfg.min_confidence = f64::NAN;
assert_eq!(
TestApi.scan_config_validate(&cfg),
Err("min_confidence must be between 0.0 and 1.0, found NaN".to_string())
);
}
#[test]
fn min_confidence_positive_infinity_rejected_exact_error() {
let mut cfg = ScanConfig::default();
cfg.min_confidence = f64::INFINITY;
assert_eq!(
TestApi.scan_config_validate(&cfg),
Err("min_confidence must be between 0.0 and 1.0, found inf".to_string())
);
}
#[test]
fn min_confidence_negative_infinity_rejected_exact_error() {
let mut cfg = ScanConfig::default();
cfg.min_confidence = f64::NEG_INFINITY;
assert_eq!(
TestApi.scan_config_validate(&cfg),
Err("min_confidence must be between 0.0 and 1.0, found -inf".to_string())
);
}
#[test]
fn max_decode_depth_zero_and_limit_accepted() {
assert_eq!(max_decode_depth_limit(), 10);
let mut cfg = ScanConfig::default();
cfg.max_decode_depth = 0;
assert_eq!(TestApi.scan_config_validate(&cfg), Ok(()));
cfg.max_decode_depth = max_decode_depth_limit();
assert_eq!(TestApi.scan_config_validate(&cfg), Ok(()));
}
#[test]
fn max_decode_depth_above_limit_rejected_exact_error() {
let mut cfg = ScanConfig::default();
cfg.max_decode_depth = 20;
assert_eq!(
TestApi.scan_config_validate(&cfg),
Err("max_decode_depth exceeds limit of 10, found 20".to_string())
);
}
#[test]
fn max_decode_depth_usize_max_rejected_exact_error() {
let mut cfg = ScanConfig::default();
cfg.max_decode_depth = usize::MAX;
assert_eq!(
TestApi.scan_config_validate(&cfg),
Err("max_decode_depth exceeds limit of 10, found 18446744073709551615".to_string())
);
}
#[test]
fn validate_reports_confidence_error_before_depth_error() {
let mut cfg = ScanConfig::default();
cfg.min_confidence = 1.5; cfg.max_decode_depth = 99; assert_eq!(
TestApi.scan_config_validate(&cfg),
Err("min_confidence must be between 0.0 and 1.0, found 1.5".to_string())
);
}
#[test]
fn toml_out_of_range_confidence_parses_then_validate_rejects() {
let doc = BASE_TOML.replace("min_confidence = 0.5", "min_confidence = 1.5");
let cfg = parse(&doc); assert!((cfg.min_confidence - 1.5).abs() < F64_EPS);
assert_eq!(
TestApi.scan_config_validate(&cfg),
Err("min_confidence must be between 0.0 and 1.0, found 1.5".to_string())
);
}
#[test]
fn toml_out_of_range_depth_parses_then_validate_rejects() {
let doc = BASE_TOML.replace("max_decode_depth = 4", "max_decode_depth = 25");
let cfg = parse(&doc);
assert_eq!(cfg.max_decode_depth, 25); assert_eq!(
TestApi.scan_config_validate(&cfg),
Err("max_decode_depth exceeds limit of 10, found 25".to_string())
);
}
#[test]
fn unknown_field_fails_closed_naming_field() {
let doc = format!("{BASE_TOML}\nmax_decode_dept = 3\n"); let err = toml::from_str::<ScanConfig>(&doc)
.expect_err("unknown field must fail closed")
.to_string();
assert!(
err.contains("unknown field `max_decode_dept`"),
"expected 'unknown field `max_decode_dept`' in error, got: {err}"
);
}
#[test]
fn wrong_type_min_confidence_field_rejected() {
let doc = BASE_TOML.replace("min_confidence = 0.5", "min_confidence = \"high\"");
let err = toml::from_str::<ScanConfig>(&doc)
.expect_err("string for a float field must be rejected")
.to_string();
assert!(
err.contains("min_confidence"),
"error must name the offending key, got: {err}"
);
assert!(
err.contains("invalid type"),
"error must state the type mismatch, got: {err}"
);
}