use crate::models::GenericFinding;
use crate::render::json::{render_json_generic, render_jsonl_generic, render_sarif_generic};
use crate::render::markdown::escape_markdown_text;
use crate::render::markdown::render_markdown_generic;
use crate::render::summary::{colored_label, render_summary_generic, strip_ansi};
use secfinding::{Evidence, Finding, FindingKind, Severity};
use std::sync::Arc;
#[test]
fn json_empty_array() {
let out = render_json_generic(&[]).unwrap();
assert_eq!(out.trim(), "[]");
}
#[test]
fn json_single_object_structure() {
let gf = GenericFinding::builder("scanner", "target", Severity::High)
.title("Title")
.detail("Detail")
.rule_id("RULE-1")
.build();
let out = render_json_generic(&[gf]).unwrap();
let parsed: Vec<serde_json::Value> = serde_json::from_str(&out).unwrap();
assert_eq!(parsed.len(), 1);
assert_eq!(parsed[0]["scanner"], "scanner");
assert_eq!(parsed[0]["severity"], "high");
assert_eq!(parsed[0]["title"], "Title");
assert_eq!(parsed[0]["detail"], "Detail");
assert_eq!(parsed[0]["rule_id"], "RULE-1");
}
#[test]
fn json_multiple_findings_preserve_order() {
let gfs: Vec<GenericFinding> = (0..5)
.map(|i| {
let title: &'static str = Box::leak(format!("T{}", i).into_boxed_str());
let rule_id: &'static str = Box::leak(format!("R{}", i).into_boxed_str());
GenericFinding::builder("s", "t", Severity::Medium)
.title(title)
.rule_id(rule_id)
.build()
})
.collect();
let out = render_json_generic(&gfs).unwrap();
let parsed: Vec<serde_json::Value> = serde_json::from_str(&out).unwrap();
assert_eq!(parsed.len(), 5);
for (i, item) in parsed.iter().enumerate() {
assert_eq!(item["title"], format!("T{}", i));
}
}
#[test]
fn json_roundtrip_matches_json_value() {
let evidence = vec![Evidence::http_status(500).unwrap()];
let cwe = vec![Arc::from("CWE-89")];
let cve = vec![Arc::from("CVE-2024-1")];
let tags = vec![Arc::from("tag")];
let gf = GenericFinding::builder("s", "t", Severity::Critical)
.title("T")
.detail("D")
.cwe_ids(&cwe)
.cve_ids(&cve)
.tags(&tags)
.confidence(Some(0.95))
.rule_id("R")
.sarif_level("error")
.exploit_hint(Some("hint"))
.evidence(&evidence)
.kind(FindingKind::Vulnerability)
.build();
let v = gf.json_value();
let json = serde_json::to_string(&v).unwrap();
let back: serde_json::Value = serde_json::from_str(&json).unwrap();
assert_eq!(back["scanner"], "s");
assert_eq!(back["target"], "t");
assert_eq!(back["severity"], "critical");
assert_eq!(back["title"], "T");
assert_eq!(back["detail"], "D");
assert_eq!(back["rule_id"], "R");
assert_eq!(back["confidence"], 0.95);
assert_eq!(back["exploit_hint"], "hint");
assert_eq!(back["kind"], "Vulnerability");
let back_cwe = back["cwe_ids"].as_array().unwrap();
assert_eq!(back_cwe.len(), 1);
assert_eq!(back_cwe[0], "CWE-89");
}
#[test]
fn jsonl_empty_is_empty_string() {
let out = render_jsonl_generic(&[]).unwrap();
assert_eq!(out, "");
}
#[test]
fn jsonl_single_line() {
let gf = GenericFinding::builder("s", "t", Severity::Low)
.title("T")
.rule_id("R")
.build();
let out = render_jsonl_generic(&[gf]).unwrap();
let lines: Vec<&str> = out.lines().collect();
assert_eq!(lines.len(), 1);
let parsed: serde_json::Value = serde_json::from_str(lines[0]).unwrap();
assert_eq!(parsed["title"], "T");
}
#[test]
fn jsonl_multiple_lines_match_order() {
let gfs: Vec<GenericFinding> = (0..3)
.map(|i| {
let title: &'static str = Box::leak(format!("T{}", i).into_boxed_str());
let rule_id: &'static str = Box::leak(format!("R{}", i).into_boxed_str());
GenericFinding::builder("s", "t", Severity::Info)
.title(title)
.rule_id(rule_id)
.build()
})
.collect();
let out = render_jsonl_generic(&gfs).unwrap();
let lines: Vec<&str> = out.lines().collect();
assert_eq!(lines.len(), 3);
for (i, line) in lines.iter().enumerate() {
let parsed: serde_json::Value = serde_json::from_str(line).unwrap();
assert_eq!(parsed["title"], format!("T{}", i));
}
}
#[test]
fn jsonl_lines_contain_no_literal_newlines() {
let gf = GenericFinding::builder("s", "t", Severity::High)
.title("Line1\nLine2")
.rule_id("R")
.build();
let out = render_jsonl_generic(&[gf]).unwrap();
assert_eq!(out.lines().count(), 1, "JSONL must have exactly one line per finding");
let parsed: serde_json::Value = serde_json::from_str(&out).unwrap();
assert_eq!(parsed["title"], "Line1\nLine2");
}
fn assert_sarif_top_level_structure(value: &serde_json::Value) {
assert_eq!(
value["$schema"],
"https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json"
);
assert_eq!(value["version"], "2.1.0");
let runs = value["runs"].as_array().expect("runs must be an array");
assert_eq!(runs.len(), 1, "SARIF must contain exactly one run");
}
fn assert_sarif_run_structure(run: &serde_json::Value, expected_tool_name: &str) {
let tool = &run["tool"]["driver"];
assert_eq!(tool["name"], expected_tool_name);
assert!(tool["rules"].is_array());
assert!(run["results"].is_array());
}
fn assert_sarif_rule_structure(rule: &serde_json::Value) {
assert!(
rule["id"].is_string(),
"rule id must be a string: {:?}",
rule
);
assert!(rule["name"].is_string());
assert!(rule["shortDescription"]["text"].is_string());
assert!(rule["fullDescription"]["text"].is_string());
assert!(rule["help"]["text"].is_string());
assert!(rule["help"]["markdown"].is_string());
assert!(rule["properties"].is_object());
}
fn assert_sarif_result_structure(result: &serde_json::Value) {
assert!(result["ruleId"].is_string());
assert!(result["level"].is_string());
assert!(result["message"]["text"].is_string());
assert!(result["locations"].is_array());
assert!(result["partialFingerprints"].is_object());
assert!(
result["partialFingerprints"]["primaryLocationLineHash"]
.as_str()
.is_some()
);
assert!(result["codeFlows"].is_array());
assert!(result["properties"].is_object());
}
#[test]
fn sarif_empty_findings_has_empty_results() {
let out = render_sarif_generic(&[], "tool").unwrap();
let parsed: serde_json::Value = serde_json::from_str(&out).unwrap();
assert_sarif_top_level_structure(&parsed);
let run = &parsed["runs"][0];
assert_sarif_run_structure(run, "tool");
let results = run["results"].as_array().unwrap();
assert!(results.is_empty());
}
#[test]
fn sarif_single_finding_structure() {
let gf = GenericFinding::builder("s", "t", Severity::High)
.title("T")
.detail("D")
.rule_id("RULE-1")
.build();
let out = render_sarif_generic(&[gf], "tool").unwrap();
let parsed: serde_json::Value = serde_json::from_str(&out).unwrap();
assert_sarif_top_level_structure(&parsed);
let run = &parsed["runs"][0];
assert_sarif_run_structure(run, "tool");
let rules = run["tool"]["driver"]["rules"].as_array().unwrap();
assert_eq!(rules.len(), 1);
assert_sarif_rule_structure(&rules[0]);
let results = run["results"].as_array().unwrap();
assert_eq!(results.len(), 1);
assert_sarif_result_structure(&results[0]);
assert_eq!(results[0]["ruleId"], "RULE-1");
assert_eq!(results[0]["level"], "error");
assert_eq!(results[0]["message"]["text"], "T\nD");
assert_eq!(
results[0]["locations"][0]["physicalLocation"]["artifactLocation"]["uri"],
"t"
);
}
#[test]
fn sarif_multiple_findings_deduplicate_rules() {
let gfs = vec![
GenericFinding::builder("s", "t", Severity::Medium)
.title("T")
.detail("D1")
.rule_id("SHARED")
.build(),
GenericFinding::builder("s", "t", Severity::Medium)
.title("T")
.detail("D2")
.rule_id("SHARED")
.build(),
];
let out = render_sarif_generic(&gfs, "tool").unwrap();
let parsed: serde_json::Value = serde_json::from_str(&out).unwrap();
let run = &parsed["runs"][0];
let rules = run["tool"]["driver"]["rules"].as_array().unwrap();
let results = run["results"].as_array().unwrap();
assert_eq!(rules.len(), 1, "duplicate rule_ids should be deduplicated");
assert_eq!(results.len(), 2);
}
#[test]
fn sarif_rule_properties_contain_expected_fields() {
let cwe = vec![Arc::from("CWE-79")];
let cve = vec![Arc::from("CVE-2024-12345")];
let tags = vec![Arc::from("xss"), Arc::from("web")];
let gf = GenericFinding::builder("s", "t", Severity::High)
.title("T")
.detail("D")
.rule_id("R")
.cwe_ids(&cwe)
.cve_ids(&cve)
.tags(&tags)
.confidence(Some(0.85))
.kind(FindingKind::Vulnerability)
.build();
let out = render_sarif_generic(&[gf], "tool").unwrap();
let parsed: serde_json::Value = serde_json::from_str(&out).unwrap();
let rule = &parsed["runs"][0]["tool"]["driver"]["rules"][0];
let props = &rule["properties"];
assert_eq!(props["tags"], serde_json::json!(tags));
assert_eq!(props["severity"], "high");
assert_eq!(props["category"], "Vulnerability");
assert_eq!(props["precision"], "high");
assert_eq!(props["cwe"], serde_json::json!(cwe));
assert_eq!(props["cve"], serde_json::json!(cve));
}
#[test]
fn sarif_result_properties_contain_expected_fields() {
let evidence = vec![Evidence::http_status(403).unwrap()];
let tags = vec![Arc::from("tag1")];
let cwe_ids = vec![Arc::from("CWE-1")];
let cve_ids = vec![Arc::from("CVE-2024-1")];
let gf = GenericFinding::builder("s", "t", Severity::Low)
.title("T")
.detail("D")
.rule_id("R")
.tags(&tags)
.confidence(Some(0.5))
.cwe_ids(&cwe_ids)
.cve_ids(&cve_ids)
.exploit_hint(Some("hint"))
.evidence(&evidence)
.kind(FindingKind::Misconfiguration)
.build();
let out = render_sarif_generic(&[gf], "tool").unwrap();
let parsed: serde_json::Value = serde_json::from_str(&out).unwrap();
let result = &parsed["runs"][0]["results"][0];
let props = &result["properties"];
assert_eq!(props["severity"], "low");
assert_eq!(props["kind"], "Misconfiguration");
assert_eq!(props["confidence"], 0.5);
assert_eq!(props["exploit_hint"], "hint");
assert!(props["evidence"].is_array());
}
#[test]
fn sarif_fallback_rule_id_when_empty() {
let gf = GenericFinding::builder("my-scanner", "https://example.com", Severity::High)
.title("Fallback Rule")
.rule_id("")
.build();
let out = render_sarif_generic(&[gf], "tool").unwrap();
let parsed: serde_json::Value = serde_json::from_str(&out).unwrap();
let rule = &parsed["runs"][0]["tool"]["driver"]["rules"][0];
assert_eq!(rule["id"], "my-scanner/fallback-rule");
let result = &parsed["runs"][0]["results"][0];
assert_eq!(result["ruleId"], "my-scanner/fallback-rule");
}
#[test]
fn sarif_fingerprint_16_hex_chars() {
let gf = GenericFinding::builder("s", "t", Severity::High)
.title("T")
.detail("D")
.rule_id("R")
.build();
let out = render_sarif_generic(&[gf], "tool").unwrap();
let parsed: serde_json::Value = serde_json::from_str(&out).unwrap();
let fp = parsed["runs"][0]["results"][0]["partialFingerprints"]
["primaryLocationLineHash"]
.as_str()
.unwrap();
assert_eq!(fp.len(), 16);
assert!(fp.chars().all(|c| c.is_ascii_hexdigit()));
}
#[test]
fn sarif_codeflows_structure() {
let gf = GenericFinding::builder("s", "t", Severity::High)
.title("T")
.detail("D")
.rule_id("R")
.build();
let out = render_sarif_generic(&[gf], "tool").unwrap();
let parsed: serde_json::Value = serde_json::from_str(&out).unwrap();
let code_flows = parsed["runs"][0]["results"][0]["codeFlows"].as_array().unwrap();
assert_eq!(code_flows.len(), 1);
let thread_flows = code_flows[0]["threadFlows"].as_array().unwrap();
assert_eq!(thread_flows.len(), 1);
let locations = thread_flows[0]["locations"].as_array().unwrap();
assert_eq!(locations.len(), 1);
assert!(locations[0]["location"]["physicalLocation"]["artifactLocation"]["uri"].is_string());
}
#[test]
fn markdown_header_contains_tool_and_target() {
let f = Finding::new("s", "t", Severity::Info, "T", "D").unwrap();
let out = render_markdown_generic(&[crate::models::GenericFinding::try_from_reportable(&f).unwrap()], "MyTool");
assert!(out.starts_with("# MyTool Security Report"));
assert!(out.contains("t"));
}
#[test]
fn markdown_risk_summary_present() {
let f = Finding::new("s", "t", Severity::High, "T", "D").unwrap();
let gf = GenericFinding::try_from_reportable(&f).unwrap();
let out = render_markdown_generic(&[gf], "tool");
assert!(out.contains("## Risk Summary"));
assert!(out.contains("| Severity | Count |"));
assert!(out.contains("| HIGH | 1 |"));
}
#[test]
fn markdown_sections_omitted_when_empty() {
let f = Finding::new("s", "t", Severity::Info, "T", "D").unwrap();
let gf = GenericFinding::try_from_reportable(&f).unwrap();
let out = render_markdown_generic(&[gf], "tool");
assert!(!out.contains("## Critical Findings"));
assert!(!out.contains("## High Findings"));
assert!(!out.contains("## Medium Findings"));
assert!(!out.contains("## Low Findings"));
assert!(out.contains("## Informational"));
}
#[test]
fn markdown_per_finding_fields() {
let f = Finding::builder("scanner-name", "https://target.com", Severity::Medium)
.title("The Title")
.detail("The Detail")
.tag("tag-a")
.tag("tag-b")
.cwe("CWE-89")
.cve("CVE-2024-1")
.exploit_hint("curl -v http://x")
.build()
.unwrap();
let gf = GenericFinding::try_from_reportable(&f).unwrap();
let out = render_markdown_generic(&[gf], "tool");
assert!(out.contains("### The Title"));
assert!(out.contains("**Target:** `https://target.com`"));
assert!(out.contains("**Scanner:** scanner\\-name"));
assert!(out.contains("**Category:** Unclassified"));
assert!(out.contains("`tag\\-a`"));
assert!(out.contains("`tag\\-b`"));
assert!(out.contains("**CWE:** CWE\\-89"));
assert!(out.contains("**CVE:** CVE\\-2024\\-1"));
assert!(out.contains("The Detail"));
assert!(out.contains("`bash\ncurl -v http://x\n`"));
}
#[test]
fn markdown_footer_link_percent_encoded() {
let gf = GenericFinding::builder("s", "t", Severity::Low)
.title("T")
.build();
let out = render_markdown_generic(&[gf], "tool name");
assert!(out.contains("https://github.com/santh-io/tool%20name"));
}
#[test]
fn markdown_escapes_in_per_finding_fields() {
let gf = GenericFinding::builder("scan*ner", "t", Severity::High)
.title("`code`")
.detail("a | b")
.build();
let out = render_markdown_generic(&[gf], "tool");
assert!(out.contains(r"\`code\`"));
assert!(out.contains(r"a \| b"));
assert!(out.contains(r"scan\*ner"));
}
#[test]
fn markdown_exploit_hint_fence_length() {
let gf = GenericFinding::builder("s", "t", Severity::High)
.title("T")
.exploit_hint(Some("```"))
.build();
let out = render_markdown_generic(&[gf], "tool");
assert!(out.contains("````bash"));
}
#[test]
fn text_empty_findings_message() {
let out = crate::render::summary::render_text_generic(&[]);
assert_eq!(out, "\u{001b}[90mNo findings.\u{001b}[0m\n");
}
#[test]
fn text_contains_colored_severity_labels() {
let f = Finding::new("s", "t", Severity::Critical, "T", "").unwrap();
let gf = GenericFinding::try_from_reportable(&f).unwrap();
let out = crate::render::summary::render_text_generic(&[gf]);
assert!(out.contains("CRIT"));
assert!(out.contains("\u{001b}[31;1m"));
}
#[test]
fn text_summary_counts_and_totals() {
let findings = vec![
Finding::new("s", "t", Severity::Critical, "C", "").unwrap(),
Finding::new("s", "t", Severity::High, "H", "").unwrap(),
Finding::new("s", "t", Severity::Medium, "M", "").unwrap(),
Finding::new("s", "t", Severity::Low, "L", "").unwrap(),
Finding::new("s", "t", Severity::Info, "I", "").unwrap(),
];
let gfs: Vec<GenericFinding> = findings
.iter()
.map(|f| GenericFinding::try_from_reportable(f).unwrap())
.collect();
let out = render_summary_generic(&gfs);
assert!(out.contains(" 1 critical"));
assert!(out.contains(" 1 high"));
assert!(out.contains(" 1 medium"));
assert!(out.contains(" 1 low"));
assert!(out.contains(" 1 info"));
assert!(out.contains("Total: \u{001b}[1m5\u{001b}[0m findings"));
}
#[test]
fn text_summary_by_scanner_sorted() {
let findings = vec![
Finding::new("z", "t", Severity::Info, "Z", "").unwrap(),
Finding::new("a", "t", Severity::Info, "A", "").unwrap(),
Finding::new("m", "t", Severity::Info, "M", "").unwrap(),
];
let gfs: Vec<GenericFinding> = findings
.iter()
.map(|f| GenericFinding::try_from_reportable(f).unwrap())
.collect();
let out = render_summary_generic(&gfs);
let scanner_line_pos = out.find("By scanner:").unwrap();
let scanner_line = &out[scanner_line_pos..];
assert!(scanner_line.contains("a\u{001b}[0m:1"));
assert!(scanner_line.contains("m\u{001b}[0m:1"));
assert!(scanner_line.contains("z\u{001b}[0m:1"));
}
#[test]
fn text_summary_by_kind_sorted() {
let findings = vec![
Finding::builder("s", "t", Severity::Info)
.title("Z")
.kind(FindingKind::Vulnerability)
.build()
.unwrap(),
Finding::builder("s", "t", Severity::Info)
.title("A")
.kind(FindingKind::Exposure)
.build()
.unwrap(),
];
let gfs: Vec<GenericFinding> = findings
.iter()
.map(|f| GenericFinding::try_from_reportable(f).unwrap())
.collect();
let out = render_summary_generic(&gfs);
let kind_line_pos = out.find("By category:").unwrap();
let kind_line = &out[kind_line_pos..];
assert!(kind_line.contains("Exposure\u{001b}[0m:1"));
assert!(kind_line.contains("Vulnerability\u{001b}[0m:1"));
}
#[test]
fn text_evidence_banner_truncated_to_80_chars() {
let raw = "x".repeat(200);
let f = Finding::builder("s", "t", Severity::High)
.title("T")
.evidence(Evidence::Banner { raw: raw.clone().into() })
.build()
.unwrap();
let gf = GenericFinding::try_from_reportable(&f).unwrap();
let out = crate::render::summary::render_text_generic(&[gf]);
let line_start = out.find("Banner:").unwrap();
let line = &out[line_start..];
let line_end = line.find('\n').unwrap();
let banner_line = &line[..line_end];
assert!(banner_line.len() < 120, "banner line should be truncated in display");
}
#[test]
fn strip_ansi_no_ansi_returns_unchanged() {
let s = "hello world";
assert_eq!(strip_ansi(s), "hello world");
}
#[test]
fn strip_ansi_removes_simple_sequences() {
assert_eq!(strip_ansi("\u{001b}[31mred\u{001b}[0m"), "red");
assert_eq!(strip_ansi("\u{001b}[1mbold\u{001b}[0m"), "bold");
}
#[test]
fn strip_ansi_removes_multiple_sequences() {
let s = "\u{001b}[31mA\u{001b}[0m \u{001b}[32mB\u{001b}[0m";
assert_eq!(strip_ansi(s), "A B");
}
#[test]
fn strip_ansi_nested_or_unclosed() {
let s = "\u{001b}[31munfinished";
assert_eq!(strip_ansi(s), "unfinished");
}
#[test]
fn colored_label_all_severities() {
assert!(colored_label(Severity::Critical).contains("CRIT"));
assert!(colored_label(Severity::High).contains("HIGH"));
assert!(colored_label(Severity::Medium).contains("MED"));
assert!(colored_label(Severity::Low).contains("LOW"));
assert!(colored_label(Severity::Info).contains("INFO"));
}
#[test]
fn escape_markdown_text_escapes_special_chars() {
assert_eq!(escape_markdown_text("*"), r"\*");
assert_eq!(escape_markdown_text("_"), r"\_");
assert_eq!(escape_markdown_text("`"), r"\`");
assert_eq!(escape_markdown_text("#"), r"\#");
assert_eq!(escape_markdown_text("|"), r"\|");
}
#[test]
fn escape_markdown_text_escapes_html_tags() {
let out = escape_markdown_text("<script>");
assert!(out.contains(r"\<script\>"));
}
#[test]
fn escape_markdown_text_neutralizes_links() {
let out = escape_markdown_text("[label](http://example.com)");
assert!(!out.contains("[label](http://example.com)"));
assert!(out.contains(r"\[label\]"));
assert!(out.contains(r"\<http://example.com\>"));
}
#[test]
fn escape_markdown_text_preserves_non_special_text() {
assert_eq!(escape_markdown_text("hello world 123"), "hello world 123");
}
#[test]
fn generic_finding_json_value_preserves_all_evidence_types() {
let evidence = vec![
Evidence::HttpResponse {
status: 200,
headers: vec![("h".into(), "v".into())],
body_excerpt: Some("body".into()),
},
Evidence::DnsRecord {
record_type: "A".into(),
value: "1.2.3.4".into(),
},
Evidence::Banner {
raw: "banner".into(),
},
Evidence::JsSnippet {
url: "u".into(),
line: 1,
snippet: "js".into(),
},
Evidence::Certificate {
subject: "CN=x".into(),
san: vec!["x".into()],
issuer: "i".into(),
expires: "2025-01-01".into(),
},
Evidence::CodeSnippet {
file: "f".into(),
line: 10,
column: Some(5),
snippet: "code".into(),
language: Some("rust".into()),
},
Evidence::HttpRequest {
method: "GET".into(),
url: "u".into(),
headers: vec![],
body: None,
},
Evidence::PatternMatch {
pattern: "p".into(),
matched: "m".into(),
},
];
let cwe_ids = vec![Arc::from("CWE-1")];
let cve_ids = vec![Arc::from("CVE-2024-1")];
let tags = vec![Arc::from("tag")];
let gf = GenericFinding::builder("s", "t", Severity::Critical)
.title("T")
.detail("D")
.cwe_ids(&cwe_ids)
.cve_ids(&cve_ids)
.tags(&tags)
.confidence(Some(1.0))
.rule_id("R")
.sarif_level("error")
.exploit_hint(Some("hint"))
.evidence(&evidence)
.kind(FindingKind::Vulnerability)
.build();
let v = gf.json_value();
let json = serde_json::to_string(&v).unwrap();
let back: serde_json::Value = serde_json::from_str(&json).unwrap();
let ev_arr = back["evidence"].as_array().unwrap();
assert_eq!(ev_arr.len(), evidence.len());
for (i, ev) in ev_arr.iter().enumerate() {
let expected = serde_json::to_value(&evidence[i]).unwrap();
assert_eq!(
*ev, expected,
"evidence item {} did not roundtrip correctly",
i
);
}
}
#[test]
fn generic_finding_json_value_null_optional_fields() {
let gf = GenericFinding::builder("s", "t", Severity::Low)
.title("T")
.build();
let v = gf.json_value();
assert_eq!(v["confidence"], serde_json::Value::Null);
assert_eq!(v["exploit_hint"], serde_json::Value::Null);
assert!(v["cwe_ids"].as_array().unwrap().is_empty());
assert!(v["cve_ids"].as_array().unwrap().is_empty());
assert!(v["tags"].as_array().unwrap().is_empty());
assert!(v["evidence"].as_array().unwrap().is_empty());
}