use crate::config::project_config::normalize_detector_name;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct DualBranchConfig {
#[serde(default)]
pub enabled: bool,
#[serde(default)]
pub detectors: HashMap<String, bool>,
}
impl DualBranchConfig {
pub fn is_enabled_for(&self, detector_id: &str) -> bool {
if !self.enabled {
return false;
}
let normalized = normalize_detector_name(detector_id);
self.detectors
.iter()
.filter(|(k, _)| normalize_detector_name(k) == normalized)
.any(|(_, &v)| v)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_config(enabled: bool, pairs: &[(&str, bool)]) -> DualBranchConfig {
DualBranchConfig {
enabled,
detectors: pairs.iter().map(|(k, v)| (k.to_string(), *v)).collect(),
}
}
#[test]
fn default_config_disables_everything() {
let config = DualBranchConfig::default();
assert!(!config.is_enabled_for("insecure-crypto"));
assert!(!config.is_enabled_for("any-other-detector"));
assert!(!config.is_enabled_for(""));
}
#[test]
fn master_switch_off_disables_even_opted_in_detectors() {
let config = make_config(false, &[("insecure-crypto", true)]);
assert!(
!config.is_enabled_for("insecure-crypto"),
"master switch off must override per-detector opt-in",
);
}
#[test]
fn detector_off_with_master_on_is_disabled() {
let config = make_config(true, &[("path-traversal", false)]);
assert!(!config.is_enabled_for("path-traversal"));
assert!(!config.is_enabled_for("not-in-map"));
}
#[test]
fn both_master_and_detector_on_enables() {
let config = make_config(true, &[("insecure-crypto", true)]);
assert!(config.is_enabled_for("insecure-crypto"));
}
#[test]
fn lookup_normalizes_detector_id() {
let config = make_config(true, &[("insecure-crypto", true)]);
for form in [
"insecure-crypto",
"insecure_crypto",
"InsecureCrypto",
"InsecureCryptoDetector",
] {
assert!(
config.is_enabled_for(form),
"detector ID form {:?} should match normalized key 'insecure-crypto'",
form,
);
}
}
#[test]
fn lookup_normalizes_toml_keys_too() {
let config = make_config(true, &[("InsecureCryptoDetector", true)]);
assert!(config.is_enabled_for("insecure-crypto"));
assert!(config.is_enabled_for("InsecureCrypto"));
}
#[test]
fn toml_deserialization_populates_config() {
let toml_text = r#"
enabled = true
[detectors]
insecure-crypto = true
path-traversal = false
"#;
let parsed: DualBranchConfig =
basic_toml::from_str(toml_text).expect("deserialize dual_branch config");
assert!(parsed.enabled);
assert!(parsed.is_enabled_for("insecure-crypto"));
assert!(!parsed.is_enabled_for("path-traversal"));
assert!(!parsed.is_enabled_for("not-mentioned"));
}
#[test]
fn missing_dual_branch_section_in_toml_is_disabled() {
let toml_text = "";
let project: crate::config::project_config::ProjectConfig =
basic_toml::from_str(toml_text).expect("deserialize");
assert!(!project.dual_branch.enabled);
assert!(!project.dual_branch.is_enabled_for("anything"));
}
#[test]
fn explicit_false_in_map_is_treated_as_disabled() {
let config = make_config(true, &[("insecure-crypto", false)]);
assert!(!config.is_enabled_for("insecure-crypto"));
}
#[test]
fn conflicting_keys_resolve_to_enabled_for_opt_in_semantics() {
let config1 = make_config(
true,
&[("insecure-crypto", false), ("InsecureCryptoDetector", true)],
);
assert!(
config1.is_enabled_for("insecure-crypto"),
"opt-in must win even when normalized-form key is false",
);
let config2 = make_config(
true,
&[("InsecureCrypto", false), ("InsecureCryptoDetector", true)],
);
assert!(
config2.is_enabled_for("insecure-crypto"),
"opt-in must win across unnormalized-key conflicts \
regardless of HashMap iteration order",
);
let config3 = make_config(
true,
&[
("insecure-crypto", false),
("InsecureCryptoDetector", false),
],
);
assert!(
!config3.is_enabled_for("insecure-crypto"),
"all-false must disable (no opt-in to honor)",
);
}
}