use std::collections::BTreeMap;
use parlov_core::{OracleClass, OracleResult, OracleVerdict, Severity, Signal, SignalKind, Vector};
use crate::{
render_scan_json, render_scan_sarif, render_scan_table, BodySamplesBundle, ChainProvenance,
ExchangeContext, HeadersBundle, ProbeContext, ScanFinding,
};
const TARGET: &str = "http://x/y";
fn confirmed() -> OracleResult {
OracleResult {
class: OracleClass::Existence,
verdict: OracleVerdict::Confirmed,
severity: Some(Severity::High),
confidence: 85,
impact_class: None,
reasons: vec![],
signals: vec![Signal {
kind: SignalKind::StatusCodeDiff,
evidence: "200 vs 404".into(),
rfc_basis: None,
}],
technique_id: Some("get-200-404".into()),
vector: Some(Vector::StatusCodeDiff),
normative_strength: None,
label: None,
leaks: None,
rfc_basis: None,
}
}
fn make_finding(
verbose: bool,
chain_provenance: Option<ChainProvenance>,
verdict: OracleVerdict,
) -> ScanFinding {
let mut headers = BTreeMap::new();
headers.insert("if-match".to_owned(), "W/\"v1\"".to_owned());
let probe_headers = if verbose {
Some(HeadersBundle {
baseline: headers.clone(),
probe: headers,
})
} else {
None
};
let resp_headers = if verbose {
let mut h = BTreeMap::new();
h.insert(
"www-authenticate".to_owned(),
"Bearer realm=\"api\"".to_owned(),
);
Some(HeadersBundle {
baseline: h.clone(),
probe: h,
})
} else {
None
};
let body_samples = if verbose {
Some(BodySamplesBundle {
baseline: "small body".to_owned(),
probe: "small body".to_owned(),
})
} else {
None
};
let mut result = confirmed();
result.verdict = verdict;
ScanFinding {
target_url: TARGET.to_owned(),
strategy_id: "if-match-elicit".to_owned(),
strategy_name: "If-Match elicitation".to_owned(),
method: "PATCH".to_owned(),
result,
repro: None,
probe: ProbeContext {
baseline_url: format!("{TARGET}/1"),
probe_url: format!("{TARGET}/9999"),
method: "PATCH".to_owned(),
headers: probe_headers,
},
exchange: ExchangeContext {
baseline_status: 401,
probe_status: 401,
headers: resp_headers,
body_samples,
},
chain_provenance,
}
}
fn etag_provenance() -> ChainProvenance {
ChainProvenance {
producer_kind: "Etag".to_owned(),
producer_value: "W/\"abc\"".to_owned(),
}
}
#[test]
fn json_finding_includes_probe_and_exchange_default() {
let findings = vec![make_finding(false, None, OracleVerdict::Confirmed)];
let json = render_scan_json(TARGET, &findings).expect("render");
let v: serde_json::Value = serde_json::from_str(&json).expect("parse");
let f = &v["findings"][0];
assert_eq!(f["probe"]["method"], "PATCH");
assert_eq!(f["probe"]["baseline_url"], format!("{TARGET}/1"));
assert_eq!(f["probe"]["probe_url"], format!("{TARGET}/9999"));
assert_eq!(f["exchange"]["baseline_status"], 401);
assert_eq!(f["exchange"]["probe_status"], 401);
assert!(
f["probe"].get("headers").is_none(),
"headers should be omitted by default"
);
assert!(
f["exchange"].get("body_samples").is_none(),
"body_samples should be omitted by default"
);
}
#[test]
fn json_phase2_finding_includes_chain_provenance() {
let findings = vec![make_finding(
false,
Some(etag_provenance()),
OracleVerdict::Confirmed,
)];
let json = render_scan_json(TARGET, &findings).expect("render");
let v: serde_json::Value = serde_json::from_str(&json).expect("parse");
let prov = &v["findings"][0]["chain_provenance"];
assert_eq!(prov["producer_kind"], "Etag");
assert_eq!(prov["producer_value"], "W/\"abc\"");
}
#[test]
fn json_phase1_finding_omits_chain_provenance() {
let findings = vec![make_finding(false, None, OracleVerdict::Confirmed)];
let json = render_scan_json(TARGET, &findings).expect("render");
let v: serde_json::Value = serde_json::from_str(&json).expect("parse");
assert!(v["findings"][0].get("chain_provenance").is_none());
}
#[test]
fn json_verbose_finding_includes_filtered_headers() {
let findings = vec![make_finding(true, None, OracleVerdict::Confirmed)];
let json = render_scan_json(TARGET, &findings).expect("render");
let v: serde_json::Value = serde_json::from_str(&json).expect("parse");
let baseline_hdrs = &v["findings"][0]["probe"]["headers"]["baseline"];
assert_eq!(baseline_hdrs["if-match"], "W/\"v1\"");
}
#[test]
fn json_verbose_finding_includes_body_samples() {
let findings = vec![make_finding(true, None, OracleVerdict::Confirmed)];
let json = render_scan_json(TARGET, &findings).expect("render");
let v: serde_json::Value = serde_json::from_str(&json).expect("parse");
let samples = &v["findings"][0]["exchange"]["body_samples"];
assert_eq!(samples["baseline"], "small body");
}
#[test]
fn json_schema_version_is_1_2_0() {
let findings = vec![make_finding(false, None, OracleVerdict::Confirmed)];
let json = render_scan_json(TARGET, &findings).expect("render");
let v: serde_json::Value = serde_json::from_str(&json).expect("parse");
assert_eq!(v["schema_version"], "1.2.0");
}
#[test]
fn sarif_finding_has_probe_and_exchange_properties() {
let findings = vec![make_finding(false, None, OracleVerdict::Confirmed)];
let json = render_scan_sarif(TARGET, &findings).expect("render");
let v: serde_json::Value = serde_json::from_str(&json).expect("parse");
let props = &v["runs"][0]["results"][0]["properties"];
assert_eq!(props["probe"]["method"], "PATCH");
assert_eq!(props["exchange"]["baseline_status"], 401);
}
#[test]
fn sarif_phase2_finding_has_chain_provenance_property() {
let findings = vec![make_finding(
false,
Some(etag_provenance()),
OracleVerdict::Confirmed,
)];
let json = render_scan_sarif(TARGET, &findings).expect("render");
let v: serde_json::Value = serde_json::from_str(&json).expect("parse");
let props = &v["runs"][0]["results"][0]["properties"];
assert_eq!(props["chain_provenance"]["producer_kind"], "Etag");
}
#[test]
fn table_renders_probe_row_under_non_not_present_finding() {
let findings = vec![make_finding(false, None, OracleVerdict::Confirmed)];
let table = render_scan_table(&findings);
assert!(
table.contains("Probe: PATCH"),
"probe row missing:\n{table}"
);
}
#[test]
fn table_does_not_render_probe_row_under_not_present_finding() {
let findings = vec![make_finding(false, None, OracleVerdict::NotPresent)];
let table = render_scan_table(&findings);
assert!(
!table.contains("Probe: PATCH"),
"probe row should suppress:\n{table}"
);
}
#[test]
fn table_renders_chain_row_when_provenance_present() {
let findings = vec![make_finding(
false,
Some(etag_provenance()),
OracleVerdict::Confirmed,
)];
let table = render_scan_table(&findings);
assert!(
table.contains("Chain: derived from Etag="),
"chain row missing:\n{table}"
);
}
#[test]
fn table_verbose_adds_headers_section() {
let findings = vec![make_finding(true, None, OracleVerdict::Confirmed)];
let table = render_scan_table(&findings);
assert!(
table.contains("Request headers (baseline)"),
"verbose request header section missing:\n{table}"
);
assert!(
table.contains("if-match"),
"filtered if-match value should appear:\n{table}"
);
}