use super::*;
use parlov_core::{OracleClass, OracleResult, OracleVerdict, Severity, Signal, SignalKind};
use crate::context::test_helpers::{exchange_ctx, probe_ctx};
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: "403 (baseline) vs 404 (probe)".into(),
rfc_basis: None,
}],
technique_id: Some("test-tech".into()),
vector: Some(Vector::StatusCodeDiff),
normative_strength: Some(NormativeStrength::Should),
label: Some("Auth differential".into()),
leaks: Some("Resource existence".into()),
rfc_basis: Some("RFC 9110".into()),
}
}
fn not_present() -> OracleResult {
OracleResult {
class: OracleClass::Existence,
verdict: OracleVerdict::NotPresent,
severity: None,
confidence: 0,
impact_class: None,
reasons: vec![],
signals: vec![],
technique_id: None,
vector: None,
normative_strength: None,
label: None,
leaks: None,
rfc_basis: None,
}
}
#[test]
fn single_json_has_schema_version_and_target() {
let json =
render_json("https://x.com/api", &confirmed(), "s1", "n1", "GET").expect("serialization");
let v: serde_json::Value = serde_json::from_str(&json).expect("parse");
assert_eq!(v["schema_version"], "1.2.0");
assert_eq!(v["target_url"], "https://x.com/api");
}
#[test]
fn single_json_has_finding_id_12_hex() {
let json =
render_json("https://x.com", &confirmed(), "s1", "n1", "GET").expect("serialization");
let v: serde_json::Value = serde_json::from_str(&json).expect("parse");
let fid = v["finding"]["finding_id"].as_str().expect("string");
assert_eq!(fid.len(), 12);
assert!(fid.chars().all(|c| c.is_ascii_hexdigit()));
}
#[test]
fn single_json_omits_none_matched_pattern_fields() {
let json =
render_json("https://x.com", ¬_present(), "s1", "n1", "GET").expect("serialization");
let v: serde_json::Value = serde_json::from_str(&json).expect("parse");
let mp = &v["finding"]["matched_pattern"];
assert!(mp.get("label").is_none());
assert!(mp.get("leaks").is_none());
}
#[test]
fn scan_json_uses_findings_array() {
let findings = vec![ScanFinding {
target_url: "https://x.com".to_owned(),
strategy_id: "s1".to_owned(),
strategy_name: "n1".to_owned(),
method: "GET".to_owned(),
result: confirmed(),
repro: None,
probe: probe_ctx("GET", "https://x.com", "https://x.com/9999"),
exchange: exchange_ctx(200, 404),
chain_provenance: None,
}];
let json = render_scan_json("https://x.com", &findings).expect("serialization");
let v: serde_json::Value = serde_json::from_str(&json).expect("parse");
assert!(v["findings"].is_array());
assert_eq!(v["findings"].as_array().expect("array").len(), 1);
}
#[test]
fn scan_json_empty_findings_produces_empty_array() {
let json = render_scan_json("https://x.com", &[]).expect("serialization");
let v: serde_json::Value = serde_json::from_str(&json).expect("parse");
assert!(v["findings"].as_array().expect("array").is_empty());
}
fn confirmed_verdict() -> parlov_core::EndpointVerdict {
use parlov_core::{
ContributingFinding, EndpointStopReason, EndpointVerdict, OracleClass, OracleVerdict,
Severity, StrategyOutcomeKind,
};
EndpointVerdict {
oracle_class: OracleClass::Existence,
posterior_probability: 0.92,
verdict: OracleVerdict::Confirmed,
severity: Some(Severity::High),
strategies_run: 10,
strategies_total: 20,
stop_reason: Some(EndpointStopReason::EarlyAccept),
first_threshold_crossed_by: None,
final_confirming_strategy: None,
contributing_findings: vec![ContributingFinding {
strategy_id: "existence-get-200-404".to_owned(),
strategy_name: "GET 200/404 existence".to_owned(),
outcome_kind: StrategyOutcomeKind::Positive,
log_odds_contribution: 2.77,
block_family: None,
block_reason: None,
}],
observability_status: parlov_core::ObservabilityStatus::EvidenceObserved,
block_summary: None,
}
}
fn not_present_verdict() -> parlov_core::EndpointVerdict {
use parlov_core::{EndpointVerdict, OracleClass, OracleVerdict};
EndpointVerdict {
oracle_class: OracleClass::Existence,
posterior_probability: 0.10,
verdict: OracleVerdict::NotPresent,
severity: None,
strategies_run: 5,
strategies_total: 5,
stop_reason: None,
first_threshold_crossed_by: None,
final_confirming_strategy: None,
contributing_findings: vec![],
observability_status: parlov_core::ObservabilityStatus::ProbedNoEvidence,
block_summary: None,
}
}
#[test]
fn endpoint_verdict_json_has_endpoint_verdict_at_root() {
let json = render_endpoint_verdict_json("https://x.com/api", &confirmed_verdict(), &[])
.expect("serialization");
let v: serde_json::Value = serde_json::from_str(&json).expect("parse");
assert!(
v.get("endpoint_verdict").is_some(),
"missing endpoint_verdict key"
);
}
#[test]
fn endpoint_verdict_json_verdict_is_confirmed_string() {
let json = render_endpoint_verdict_json("https://x.com/api", &confirmed_verdict(), &[])
.expect("serialization");
let v: serde_json::Value = serde_json::from_str(&json).expect("parse");
assert_eq!(v["endpoint_verdict"]["verdict"], "Confirmed");
}
#[test]
fn endpoint_verdict_json_posterior_probability_present() {
let json = render_endpoint_verdict_json("https://x.com/api", &confirmed_verdict(), &[])
.expect("serialization");
let v: serde_json::Value = serde_json::from_str(&json).expect("parse");
let p = v["endpoint_verdict"]["posterior_probability"]
.as_f64()
.expect("f64");
assert!((p - 0.92).abs() < 1e-9);
}
#[test]
fn endpoint_verdict_json_findings_array_present() {
let findings = vec![ScanFinding {
target_url: "https://x.com/api".to_owned(),
strategy_id: "s1".to_owned(),
strategy_name: "n1".to_owned(),
method: "GET".to_owned(),
result: confirmed(),
repro: None,
probe: probe_ctx("GET", "https://x.com/api", "https://x.com/api/9999"),
exchange: exchange_ctx(200, 404),
chain_provenance: None,
}];
let json = render_endpoint_verdict_json("https://x.com/api", &confirmed_verdict(), &findings)
.expect("serialization");
let v: serde_json::Value = serde_json::from_str(&json).expect("parse");
assert!(v["findings"].is_array());
assert_eq!(v["findings"].as_array().expect("array").len(), 1);
}
#[test]
fn endpoint_verdict_json_stop_reason_absent_when_none() {
let json = render_endpoint_verdict_json("https://x.com/api", ¬_present_verdict(), &[])
.expect("serialization");
let v: serde_json::Value = serde_json::from_str(&json).expect("parse");
assert!(
v["endpoint_verdict"].get("stop_reason").is_none(),
"stop_reason should be absent when None"
);
}
#[test]
fn endpoint_verdict_json_stop_reason_present_when_set() {
let json = render_endpoint_verdict_json("https://x.com/api", &confirmed_verdict(), &[])
.expect("serialization");
let v: serde_json::Value = serde_json::from_str(&json).expect("parse");
assert_eq!(v["endpoint_verdict"]["stop_reason"], "EarlyAccept");
}
#[test]
fn endpoint_verdict_json_has_schema_version() {
let json = render_endpoint_verdict_json("https://x.com/api", &confirmed_verdict(), &[])
.expect("serialization");
let v: serde_json::Value = serde_json::from_str(&json).expect("parse");
assert_eq!(
v["schema_version"], "1.2.0",
"schema_version must be 1.2.0 in render_endpoint_verdict_json output"
);
}