mod common;
use common::{finding, finding_full, render_jsonl};
use secreport::Format;
use secfinding::{Evidence, Finding, Severity};
#[test]
fn jsonl_empty_is_empty_string() {
let out = secreport::render(&[] as &[Finding], Format::Jsonl, "tool").unwrap();
assert_eq!(out, "");
}
#[test]
fn jsonl_single_line_valid_json() {
let f = finding("Single", Severity::High);
let parsed = render_jsonl(&[f]);
assert_eq!(parsed.len(), 1);
assert_eq!(parsed[0]["title"], "Single");
assert_eq!(parsed[0]["severity"], "high");
}
#[test]
fn jsonl_multiple_lines_match_order() {
let findings: Vec<Finding> = (0..100)
.map(|i| finding(&format!("Item-{}", i), Severity::Medium))
.collect();
let parsed = render_jsonl(&findings);
assert_eq!(parsed.len(), 100);
for (i, item) in parsed.iter().enumerate() {
assert_eq!(item["title"], format!("Item-{}", i));
}
}
#[test]
fn jsonl_lines_have_no_literal_newlines() {
let f = Finding::new("s", "t", Severity::Low, "Line1\nLine2", "Detail\nDetail2").unwrap();
let out = secreport::render(&[f], Format::Jsonl, "tool").unwrap();
let lines: Vec<&str> = out.lines().collect();
assert_eq!(lines.len(), 1, "JSONL must have exactly one line per finding");
let parsed: serde_json::Value = serde_json::from_str(lines[0]).unwrap();
assert_eq!(parsed["title"], "Line1\nLine2");
assert_eq!(parsed["detail"], "Detail\nDetail2");
}
#[test]
fn jsonl_each_line_independently_parseable() {
let findings = vec![
finding("A", Severity::Critical),
finding("B", Severity::High),
finding("C", Severity::Info),
];
let out = secreport::render(&findings, Format::Jsonl, "tool").unwrap();
for line in out.lines() {
let _: serde_json::Value = serde_json::from_str(line).expect("each line must be valid JSON");
}
}
#[test]
fn jsonl_unicode_preserved_per_line() {
let f = Finding::new("s", "t", Severity::Medium, "日本語", "詳細").unwrap();
let out = secreport::render(&[f], Format::Jsonl, "tool").unwrap();
assert!(out.contains("日本語"));
assert!(out.contains("詳細"));
}
#[test]
fn jsonl_null_bytes_escaped() {
let f = Finding::new("s", "t", Severity::Low, "a\x00b", "").unwrap();
let out = secreport::render(&[f], Format::Jsonl, "tool").unwrap();
assert!(out.contains("\\u0000"));
}
#[test]
fn jsonl_large_number_of_lines() {
let findings: Vec<Finding> = (0..50_000)
.map(|i| finding(&format!("F{}", i), Severity::Info))
.collect();
let parsed = render_jsonl(&findings);
assert_eq!(parsed.len(), 50_000);
}
#[test]
fn jsonl_evidence_serialized_correctly() {
let f = Finding::builder("s", "t", Severity::High)
.title("T")
.evidence(Evidence::http_status(500).unwrap())
.evidence(Evidence::Banner { raw: "banner".into() })
.build()
.unwrap();
let parsed = render_jsonl(&[f]);
let ev = parsed[0]["evidence"].as_array().unwrap();
assert_eq!(ev.len(), 2);
assert_eq!(ev[0]["type"], "http_response");
assert_eq!(ev[1]["type"], "banner");
}
#[test]
fn jsonl_full_finding_fields() {
let f = finding_full("XSS", Severity::High);
let parsed = render_jsonl(&[f]);
assert_eq!(parsed[0]["title"], "XSS");
assert_eq!(parsed[0]["severity"], "high");
assert_eq!(parsed[0]["confidence"], 0.95);
assert_eq!(parsed[0]["exploit_hint"], "curl -X GET https://example.com/poc");
}