mod common;
use common::{finding, finding_full, render_json};
use secreport::Format;
use secfinding::{Evidence, Finding, FindingKind, Severity};
#[test]
fn json_empty_is_empty_array() {
let parsed = render_json(&[]);
assert!(parsed.is_empty());
}
#[test]
fn json_single_finding_has_all_expected_fields() {
let f = finding_full("SQL Injection", Severity::Critical);
let parsed = render_json(&[f]);
assert_eq!(parsed.len(), 1);
let item = &parsed[0];
assert_eq!(item["scanner"], "test-scanner");
assert_eq!(item["target"], "https://example.com");
assert_eq!(item["severity"], "critical");
assert_eq!(item["title"], "SQL Injection");
assert_eq!(item["detail"], "Detailed description of the issue.");
assert_eq!(item["rule_id"], "test-scanner/sql-injection");
assert!(item["cwe_ids"].is_array());
assert!(item["cve_ids"].is_array());
assert!(item["tags"].is_array());
assert_eq!(item["confidence"], 0.95);
assert_eq!(item["exploit_hint"], "curl -X GET https://example.com/poc");
assert_eq!(item["kind"], "Vulnerability");
}
#[test]
fn json_multiple_findings_preserve_order() {
let findings: Vec<Finding> = (0..10)
.map(|i| finding(&format!("Finding-{}", i), Severity::Medium))
.collect();
let parsed = render_json(&findings);
assert_eq!(parsed.len(), 10);
for (i, item) in parsed.iter().enumerate() {
assert_eq!(item["title"], format!("Finding-{}", i));
}
}
#[test]
fn json_severity_levels_encoded_correctly() {
let findings = vec![
finding("A", Severity::Info),
finding("B", Severity::Low),
finding("C", Severity::Medium),
finding("D", Severity::High),
finding("E", Severity::Critical),
];
let parsed = render_json(&findings);
let expected = vec!["info", "low", "medium", "high", "critical"];
for (i, exp) in expected.iter().enumerate() {
assert_eq!(parsed[i]["severity"], *exp);
}
}
#[test]
fn json_evidence_array_contains_serialized_evidence() {
let f = Finding::builder("s", "t", Severity::High)
.title("T")
.evidence(Evidence::http_status(404).unwrap())
.evidence(Evidence::DnsRecord {
record_type: "A".into(),
value: "1.2.3.4".into(),
})
.build()
.unwrap();
let parsed = render_json(&[f]);
let ev = parsed[0]["evidence"].as_array().unwrap();
assert_eq!(ev.len(), 2);
assert_eq!(ev[0]["type"], "http_response");
assert_eq!(ev[0]["status"], 404);
assert_eq!(ev[1]["type"], "dns_record");
assert_eq!(ev[1]["record_type"], "A");
}
#[test]
fn json_null_fields_for_missing_optionals() {
let f = finding("Minimal", Severity::Low);
let parsed = render_json(&[f]);
assert_eq!(parsed[0]["confidence"], serde_json::Value::Null);
assert_eq!(parsed[0]["exploit_hint"], serde_json::Value::Null);
}
#[test]
fn json_output_is_pretty_printed() {
let f = finding("T", Severity::High);
let out = secreport::render(&[f], Format::Json, "tool").unwrap();
assert!(out.contains('\n'), "pretty-printed JSON should contain newlines");
assert!(out.contains(" "), "pretty-printed JSON should contain indentation");
}
#[test]
fn json_finding_kind_variants() {
for kind in [
FindingKind::Vulnerability,
FindingKind::Misconfiguration,
FindingKind::Exposure,
FindingKind::TechDetect,
FindingKind::DefaultCredentials,
FindingKind::InfoDisclosure,
FindingKind::FileDiscovery,
FindingKind::SecretLeak,
FindingKind::MaliciousCode,
FindingKind::SupplyChain,
FindingKind::Unclassified,
FindingKind::Other,
] {
let f = Finding::builder("s", "t", Severity::Medium)
.title("T")
.kind(kind)
.build()
.unwrap();
let parsed = render_json(&[f]);
assert_eq!(parsed[0]["kind"], format!("{:?}", kind));
}
}
#[test]
fn json_unicode_preserved() {
let f = Finding::new("スキャナ", "https://例え.jp", Severity::High, "日本語タイトル 🚨", "詳細").unwrap();
let out = secreport::render(&[f], Format::Json, "tool").unwrap();
assert!(out.contains("日本語タイトル 🚨"));
assert!(out.contains("詳細"));
assert!(out.contains("スキャナ"));
}
#[test]
fn json_control_characters_escaped() {
let f = Finding::new("s", "t", Severity::Medium, "a\x00b", "c\x01d").unwrap();
let out = secreport::render(&[f], Format::Json, "tool").unwrap();
assert!(out.contains("\\u0000"));
assert!(out.contains("\\u0001"));
}
#[test]
fn json_large_number_of_findings() {
let findings: Vec<Finding> = (0..5_000)
.map(|i| finding(&format!("Finding-{}", i), Severity::Info))
.collect();
let parsed = render_json(&findings);
assert_eq!(parsed.len(), 5_000);
}
#[test]
fn json_rule_id_present_when_set_via_builder() {
let f = Finding::new("my-scanner", "t", Severity::High, "Test Title", "d").unwrap();
let parsed = render_json(&[f]);
assert_eq!(parsed[0]["rule_id"], "my-scanner/test-title");
}