use secfinding::{Finding, Severity};
#[test]
fn finding_new_rejects_empty_scanner() {
let r = Finding::new("", "t", Severity::High, "title", "detail");
assert!(r.is_err());
}
#[test]
fn finding_new_rejects_empty_target() {
let r = Finding::new("s", "", Severity::High, "title", "detail");
assert!(r.is_err());
}
#[test]
fn finding_new_rejects_empty_title() {
let r = Finding::new("s", "t", Severity::High, "", "detail");
assert!(r.is_err());
}
#[test]
fn finding_with_long_detail() {
let long = "x".repeat(100_000);
let f = Finding::builder("s", "t", Severity::High)
.title("test")
.detail(&long)
.build()
.unwrap();
assert_eq!(f.detail().len(), 100_000);
}
#[test]
fn duplicate_tags_are_deduped() {
let f = Finding::builder("s", "t", Severity::High)
.title("test")
.tag("sqli")
.tag("sqli")
.tag("sqli")
.build()
.unwrap();
assert_eq!(f.tags().len(), 1);
}
#[test]
fn finding_deserialization_allows_unknown_fields() {
let json = r#"{
"scanner":"scan",
"target":"target",
"severity":"high",
"title":"title",
"unexpected":true
}"#;
let finding = serde_json::from_str::<Finding>(json).unwrap();
assert_eq!(finding.scanner(), "scan");
}
#[test]
fn finding_deserialization_rejects_invalid_cve() {
let json = r#"{
"scanner":"scan",
"target":"target",
"severity":"high",
"title":"title",
"cve_ids":["not-a-cve"]
}"#;
let error = serde_json::from_str::<Finding>(json).unwrap_err();
assert!(error.to_string().contains("invalid CVE format"));
}
#[test]
fn finding_deserialization_deduplicates_tags_and_rejects_out_of_range_scores() {
let bad = r#"{
"scanner":"scan",
"target":"target",
"severity":"high",
"title":"title",
"confidence":1.5
}"#;
assert!(
serde_json::from_str::<Finding>(bad).is_err(),
"out-of-range confidence must be rejected at deserialization"
);
let bad_cvss = r#"{
"scanner":"scan",
"target":"target",
"severity":"high",
"title":"title",
"cvss_score":99.9
}"#;
assert!(
serde_json::from_str::<Finding>(bad_cvss).is_err(),
"out-of-range cvss must be rejected at deserialization"
);
let good = r#"{
"scanner":"scan",
"target":"target",
"severity":"high",
"title":"title",
"tags":["dup","dup","alpha"],
"confidence":0.95,
"cvss_score":9.8
}"#;
let finding = serde_json::from_str::<Finding>(good).unwrap();
assert_eq!(
finding.tags(),
vec![std::sync::Arc::from("alpha"), std::sync::Arc::from("dup")]
);
assert_eq!(finding.confidence(), Some(0.95));
assert_eq!(finding.cvss_score(), Some(9.8));
}
#[test]
fn many_findings_unique_ids() {
let findings: Vec<Finding> = (0..1_000)
.map(|i| {
Finding::builder("s", format!("target-{i}"), Severity::Low)
.title(format!("finding-{i}"))
.build()
.unwrap()
})
.collect();
let ids: std::collections::HashSet<_> = findings.iter().map(|f| f.id()).collect();
assert_eq!(ids.len(), 1_000);
}
#[test]
fn adversarial_empty_input_boundary() {
let r = Finding::new("\0", "\0", Severity::High, "\0", "\0");
assert!(
r.is_err(),
"Null bytes should be rejected in security findings"
);
}
#[test]
fn adversarial_huge_input() {
let huge = "A".repeat(1024 * 1024 * 10);
let result = Finding::builder(&huge, "target", Severity::High)
.title("title")
.build();
assert!(
result.is_err(),
"Engine must gracefully reject scanner strings exceeding the max length"
);
}
#[test]
fn adversarial_invalid_utf8_simulated() {
let invalid_utf8_simulated = String::from_utf8_lossy(b"hello \xFF world").to_string();
let result = Finding::builder("scan", "target", Severity::High)
.title(&invalid_utf8_simulated)
.build();
assert!(
result.is_err(),
"Unicode replacement character should be rejected in security findings"
);
}
#[test]
fn adversarial_u32_max_bounds() {
let size = 1024 * 1024 * 5; let large = "A".repeat(size);
let result = Finding::builder("scan", "target", Severity::High)
.title("title")
.detail(&large)
.build();
assert!(
result.is_err(),
"Engine must gracefully reject large detail strings (>1MB) rather than accept."
);
}
#[test]
fn adversarial_empty_tags_vector() {
let f = Finding::builder("s", "t", Severity::High)
.title("t")
.build()
.unwrap();
assert!(f.tags().is_empty(), "Empty tags vector should not panic");
}
#[test]
fn integer_limits_for_cvss_score() {
let r = Finding::builder("s", "t", Severity::High)
.title("t")
.cvss_score(std::f64::INFINITY)
.build();
assert!(r.is_err());
let r_neg = Finding::builder("s", "t", Severity::High)
.title("t")
.cvss_score(std::f64::NEG_INFINITY)
.build();
assert!(r_neg.is_err());
}
#[test]
fn integer_limits_for_confidence() {
let r = Finding::builder("s", "t", Severity::High)
.title("t")
.confidence(std::f64::INFINITY)
.build();
assert!(r.is_err());
let r_neg = Finding::builder("s", "t", Severity::High)
.title("t")
.confidence(std::f64::NEG_INFINITY)
.build();
assert!(r_neg.is_err());
}