use super::*;
#[test]
fn test_normalize_detector_name() {
assert_eq!(normalize_detector_name("GodClassDetector"), "god-class");
assert_eq!(normalize_detector_name("god_class"), "god-class");
assert_eq!(normalize_detector_name("god-class"), "god-class");
assert_eq!(
normalize_detector_name("SQLInjectionDetector"),
"sql-injection"
);
assert_eq!(normalize_detector_name("NPlusOneDetector"), "n-plus-one");
}
#[test]
fn test_glob_match() {
assert!(glob_match("**/vendor/**", "src/vendor/lib/foo.py"));
assert!(glob_match("generated/", "generated/model.py"));
assert!(glob_match("*.test.ts", "foo.test.ts"));
assert!(glob_match("vendor/", "vendor/lib/foo.py"));
assert!(!glob_match("vendor/", "src/vendor/foo.py"));
}
#[test]
fn test_pillar_weights_validation() {
let valid = PillarWeights {
structure: 0.4,
quality: 0.3,
architecture: 0.3,
};
assert!(valid.is_valid());
let invalid = PillarWeights {
structure: 0.5,
quality: 0.5,
architecture: 0.5,
};
assert!(!invalid.is_valid());
}
#[test]
fn test_pillar_weights_normalize() {
let mut weights = PillarWeights {
structure: 2.0,
quality: 1.0,
architecture: 1.0,
};
weights.normalize();
assert!((weights.structure - 0.5).abs() < 0.001);
assert!((weights.quality - 0.25).abs() < 0.001);
assert!((weights.architecture - 0.25).abs() < 0.001);
}
#[test]
fn test_threshold_value() {
let int_val = ThresholdValue::Integer(42);
assert_eq!(int_val.as_i64(), Some(42));
assert_eq!(int_val.as_f64(), Some(42.0));
assert_eq!(int_val.as_bool(), None);
let float_val = ThresholdValue::Float(2.5);
assert_eq!(float_val.as_i64(), Some(2));
assert_eq!(float_val.as_f64(), Some(2.5));
let bool_val = ThresholdValue::Boolean(true);
assert_eq!(bool_val.as_bool(), Some(true));
assert_eq!(bool_val.as_i64(), None);
}
#[test]
fn test_default_config() {
let config = ProjectConfig::default();
assert!(config.is_detector_enabled("god-class"));
assert!(config.is_detector_enabled("unknown-detector"));
assert!(config.severity_override("god-class").is_none());
assert!((config.scoring.security_multiplier - 3.0).abs() < 0.001);
assert!(config.scoring.pillar_weights.is_valid());
}
#[test]
fn test_parse_toml_config() {
let toml_content = r#"
[detectors.god-class]
enabled = true
thresholds = { method_count = 30, loc = 600 }
[detectors.sql-injection]
severity = "high"
enabled = false
[scoring]
security_multiplier = 5.0
[scoring.pillar_weights]
structure = 0.3
quality = 0.4
architecture = 0.3
[exclude]
paths = ["generated/", "vendor/"]
[defaults]
format = "json"
workers = 4
skip_detectors = ["debug-code"]
"#;
let config: ProjectConfig = basic_toml::from_str(toml_content).expect("parse project config");
assert!(config.is_detector_enabled("god-class"));
assert!(!config.is_detector_enabled("sql-injection"));
assert_eq!(
config.severity_override("sql-injection"),
Some(crate::models::Severity::High)
);
assert_eq!(config.threshold_i64("god-class", "method_count"), Some(30));
assert_eq!(config.threshold_i64("god-class", "loc"), Some(600));
assert!((config.scoring.security_multiplier - 5.0).abs() < 0.001);
assert!((config.scoring.pillar_weights.structure - 0.3).abs() < 0.001);
assert_eq!(config.exclude.paths.len(), 2);
assert!(config.should_exclude(Path::new("generated/foo.py")));
assert_eq!(
config.defaults.format,
Some(crate::reporters::OutputFormat::Json)
);
assert_eq!(config.defaults.workers, Some(4));
assert!(config
.defaults
.skip_detectors
.contains(&"debug-code".to_string()));
}
#[test]
fn test_parse_detector_tier_cap() {
let toml_content = r#"
[detectors.insecure-tls]
tier = "advisory"
[detectors.command-injection]
tier = "deep"
[detectors.sql-injection]
severity = "high"
"#;
let config: ProjectConfig = basic_toml::from_str(toml_content).expect("parse project config");
assert_eq!(
config.detector_tier_cap("insecure-tls"),
Some(crate::models::Tier::Advisory)
);
assert_eq!(
config.detector_tier_cap("InsecureTlsDetector"),
Some(crate::models::Tier::Advisory)
);
assert_eq!(
config.detector_tier_cap("command-injection"),
Some(crate::models::Tier::Deep)
);
assert_eq!(config.detector_tier_cap("sql-injection"), None);
assert_eq!(config.detector_tier_cap("god-class"), None);
}
#[test]
fn test_default_exclude_patterns_applied() {
let config = ExcludeConfig::default();
let patterns = config.effective_patterns();
assert!(patterns.contains(&"**/vendor/**".to_string()));
assert!(patterns.contains(&"**/node_modules/**".to_string()));
assert!(patterns.contains(&"**/*.min.js".to_string()));
assert_eq!(patterns.len(), DEFAULT_EXCLUDE_PATTERNS.len());
}
#[test]
fn test_skip_defaults_disables_builtin_patterns() {
let config = ExcludeConfig {
paths: vec!["custom/".to_string()],
skip_defaults: true,
};
let patterns = config.effective_patterns();
assert_eq!(patterns, vec!["custom/"]);
assert!(!patterns.contains(&"**/vendor/**".to_string()));
}
#[test]
fn test_user_patterns_merged_with_defaults() {
let config = ExcludeConfig {
paths: vec!["generated/".to_string()],
skip_defaults: false,
};
let patterns = config.effective_patterns();
assert!(patterns.contains(&"**/vendor/**".to_string()));
assert!(patterns.contains(&"generated/".to_string()));
assert_eq!(patterns.len(), DEFAULT_EXCLUDE_PATTERNS.len() + 1);
}
#[test]
fn test_effective_patterns_deduplication() {
let config = ExcludeConfig {
paths: vec!["**/vendor/**".to_string()],
skip_defaults: false,
};
let patterns = config.effective_patterns();
let vendor_count = patterns.iter().filter(|p| *p == "**/vendor/**").count();
assert_eq!(vendor_count, 1);
}
#[test]
fn test_should_exclude_vendor_by_default() {
let config = ProjectConfig::default();
assert!(config.should_exclude(std::path::Path::new("src/vendor/jquery.js")));
assert!(config.should_exclude(std::path::Path::new("node_modules/react/index.js")));
assert!(config.should_exclude(std::path::Path::new("deep/path/dist/bundle.js")));
assert!(config.should_exclude(std::path::Path::new("assets/lib.min.js")));
assert!(config.should_exclude(std::path::Path::new("css/styles.min.css")));
assert!(config.should_exclude(std::path::Path::new("js/app.bundle.js")));
assert!(!config.should_exclude(std::path::Path::new("src/main.py")));
assert!(config.should_exclude(std::path::Path::new(
"/tmp/django/django/contrib/admin/static/admin/js/vendor/jquery/jquery.js"
)));
assert!(config.should_exclude(std::path::Path::new(
"/tmp/project/node_modules/react/index.js"
)));
assert!(config.should_exclude(std::path::Path::new("/home/user/project/assets/app.min.js")));
}
#[test]
fn test_default_project_type() {
let pt = ProjectType::default();
assert_eq!(pt, ProjectType::Web);
}
#[test]
fn test_default_exclude_patterns_populated() {
assert!(!DEFAULT_EXCLUDE_PATTERNS.is_empty());
assert!(DEFAULT_EXCLUDE_PATTERNS.contains(&"**/node_modules/**"));
assert!(DEFAULT_EXCLUDE_PATTERNS.contains(&"**/vendor/**"));
assert!(DEFAULT_EXCLUDE_PATTERNS.contains(&"**/dist/**"));
assert!(DEFAULT_EXCLUDE_PATTERNS.contains(&"**/*.min.js"));
}
#[test]
fn test_project_config_toml_with_project_type() {
let toml_str = r#"
project_type = "library"
[scoring]
security_multiplier = 3.0
[exclude]
paths = ["generated/"]
"#;
let config: ProjectConfig = basic_toml::from_str(toml_str).expect("parse scoring config");
assert_eq!(config.project_type, Some(ProjectType::Library));
assert!((config.scoring.security_multiplier - 3.0).abs() < 0.001);
assert_eq!(config.exclude.paths, vec!["generated/"]);
}
#[test]
fn test_project_config_all_project_types_parse() {
for (type_str, expected) in [
("web", ProjectType::Web),
("interpreter", ProjectType::Interpreter),
("compiler", ProjectType::Compiler),
("library", ProjectType::Library),
("framework", ProjectType::Framework),
("cli", ProjectType::Cli),
("kernel", ProjectType::Kernel),
("game", ProjectType::Game),
("datascience", ProjectType::DataScience),
("mobile", ProjectType::Mobile),
] {
let toml_str = format!("project_type = \"{}\"", type_str);
let config: ProjectConfig =
basic_toml::from_str(&toml_str).expect("parse project type config");
assert_eq!(
config.project_type,
Some(expected),
"Failed for project_type = \"{}\"",
type_str
);
}
}
#[test]
fn test_unknown_project_type_is_error() {
let toml_str = r#"project_type = "unknown_type""#;
let result = basic_toml::from_str::<ProjectConfig>(toml_str);
assert!(result.is_err());
}
#[test]
fn test_coupling_multiplier_varies_by_type() {
assert!((ProjectType::Web.coupling_multiplier() - 1.0).abs() < 0.001);
assert!(ProjectType::Compiler.coupling_multiplier() > 2.0);
assert!(ProjectType::Kernel.coupling_multiplier() > 2.0);
}
#[test]
fn test_lenient_dead_code() {
assert!(ProjectType::Interpreter.lenient_dead_code());
assert!(ProjectType::Kernel.lenient_dead_code());
assert!(ProjectType::Game.lenient_dead_code());
assert!(ProjectType::Framework.lenient_dead_code());
assert!(ProjectType::DataScience.lenient_dead_code());
assert!(!ProjectType::Web.lenient_dead_code());
assert!(!ProjectType::Library.lenient_dead_code());
assert!(!ProjectType::Cli.lenient_dead_code());
assert!(!ProjectType::Compiler.lenient_dead_code());
assert!(!ProjectType::Mobile.lenient_dead_code());
}
#[test]
fn test_disabled_detectors() {
let toml_str = r#"
[detectors.god-class]
enabled = false
[detectors.sql-injection]
enabled = true
[defaults]
skip_detectors = ["debug-code"]
"#;
let config: ProjectConfig =
basic_toml::from_str(toml_str).expect("parse disabled detectors config");
let disabled = config.disabled_detectors();
assert!(disabled.contains(&"god-class".to_string()));
assert!(disabled.contains(&"debug-code".to_string()));
assert!(!disabled.contains(&"sql-injection".to_string()));
}
#[test]
fn test_cli_defaults_parsing() {
let toml_str = r#"
[defaults]
format = "sarif"
severity = "high"
workers = 16
per_page = 50
thorough = true
no_git = false
no_emoji = true
fail_on = "medium"
skip_detectors = ["dead-code", "unused-import"]
"#;
let config: ProjectConfig = basic_toml::from_str(toml_str).expect("parse CLI defaults config");
assert_eq!(
config.defaults.format,
Some(crate::reporters::OutputFormat::Sarif)
);
assert_eq!(
config.defaults.severity,
Some(crate::models::Severity::High)
);
assert_eq!(config.defaults.workers, Some(16));
assert_eq!(config.defaults.per_page, Some(50));
assert_eq!(config.defaults.thorough, Some(true));
assert_eq!(config.defaults.no_git, Some(false));
assert_eq!(config.defaults.no_emoji, Some(true));
assert_eq!(
config.defaults.fail_on,
Some(crate::models::Severity::Medium)
);
assert_eq!(config.defaults.skip_detectors.len(), 2);
}
#[test]
fn test_detector_config_confidence_threshold() {
let toml_str = r#"
[detectors.hidden-n-plus-one]
confidence_threshold = "high"
enabled = true
severity = "medium"
"#;
let config: ProjectConfig = basic_toml::from_str(toml_str).unwrap();
let det = config.detectors.get("hidden-n-plus-one").unwrap();
assert_eq!(
det.confidence_threshold,
Some(crate::models::Confidence::High)
);
assert_eq!(det.enabled, Some(true));
assert_eq!(det.severity, Some(crate::models::Severity::Medium));
}
#[test]
fn test_per_file_ignores_default_empty() {
let config = ProjectConfig::default();
assert!(config.per_file_ignores.is_empty());
assert!(!config.is_per_file_ignored(Path::new("src/git/raw/sha1.rs"), "InsecureCryptoDetector"));
}
#[test]
fn test_per_file_ignores_toml_parsing() {
let toml_str = r#"
[per_file_ignores]
"src/git/raw/**" = ["insecure-crypto"]
"benchmarks/*.py" = ["debug-code", "single-char-names"]
"tests/**" = ["*"]
"#;
let config: ProjectConfig = basic_toml::from_str(toml_str).unwrap();
assert_eq!(config.per_file_ignores.len(), 3);
assert_eq!(
config.per_file_ignores.get("src/git/raw/**"),
Some(&vec!["insecure-crypto".to_string()])
);
assert_eq!(
config.per_file_ignores.get("benchmarks/*.py"),
Some(&vec![
"debug-code".to_string(),
"single-char-names".to_string()
])
);
}
#[test]
fn test_per_file_ignores_glob_matches_detector() {
let toml_str = r#"
[per_file_ignores]
"src/git/raw/**" = ["insecure-crypto"]
"#;
let config: ProjectConfig = basic_toml::from_str(toml_str).unwrap();
assert!(config.is_per_file_ignored(Path::new("src/git/raw/sha1.rs"), "insecure-crypto"));
assert!(config.is_per_file_ignored(Path::new("src/git/raw/sha1.rs"), "InsecureCryptoDetector"));
assert!(config.is_per_file_ignored(Path::new("src/git/raw/sha1.rs"), "insecure_crypto"));
}
#[test]
fn test_per_file_ignores_path_must_match() {
let toml_str = r#"
[per_file_ignores]
"src/git/raw/**" = ["insecure-crypto"]
"#;
let config: ProjectConfig = basic_toml::from_str(toml_str).unwrap();
assert!(!config.is_per_file_ignored(Path::new("src/auth/password.rs"), "insecure-crypto"));
}
#[test]
fn test_per_file_ignores_detector_must_match() {
let toml_str = r#"
[per_file_ignores]
"src/git/raw/**" = ["insecure-crypto"]
"#;
let config: ProjectConfig = basic_toml::from_str(toml_str).unwrap();
assert!(!config.is_per_file_ignored(Path::new("src/git/raw/sha1.rs"), "sql-injection"));
}
#[test]
fn test_per_file_ignores_wildcard_detector() {
let toml_str = r#"
[per_file_ignores]
"vendor/legacy/**" = ["*"]
"#;
let config: ProjectConfig = basic_toml::from_str(toml_str).unwrap();
assert!(config.is_per_file_ignored(Path::new("vendor/legacy/old.py"), "InsecureCryptoDetector"));
assert!(config.is_per_file_ignored(Path::new("vendor/legacy/old.py"), "GodClassDetector"));
assert!(!config.is_per_file_ignored(Path::new("src/main.rs"), "InsecureCryptoDetector"));
}
#[test]
fn test_per_file_ignores_multiple_patterns() {
let toml_str = r#"
[per_file_ignores]
"src/git/raw/**" = ["insecure-crypto"]
"benchmarks/*.py" = ["debug-code"]
"#;
let config: ProjectConfig = basic_toml::from_str(toml_str).unwrap();
assert!(config.is_per_file_ignored(Path::new("src/git/raw/sha1.rs"), "insecure-crypto"));
assert!(config.is_per_file_ignored(Path::new("benchmarks/bench.py"), "debug-code"));
assert!(!config.is_per_file_ignored(Path::new("src/git/raw/sha1.rs"), "debug-code"));
assert!(!config.is_per_file_ignored(Path::new("benchmarks/bench.py"), "insecure-crypto"));
}