secreport 0.3.0

Output formatters for security findings — JSON, JSONL, SARIF, Markdown, Text
Documentation
//! JSONL output format tests.

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");
}