parlov-output 0.8.0

Output formatters for parlov: SARIF, terminal table, and raw JSON.
Documentation
use super::*;
use parlov_core::{
    BlockFamily, BlockSummary, EndpointVerdict, ObservabilityStatus, OracleClass, OracleVerdict,
};

fn make_rule(id: &str) -> ReportingDescriptor {
    ReportingDescriptor::builder().id(id).build()
}

fn blocked_endpoint_verdict() -> EndpointVerdict {
    EndpointVerdict {
        oracle_class: OracleClass::Existence,
        posterior_probability: 0.50,
        verdict: OracleVerdict::Inconclusive,
        severity: None,
        strategies_run: 8,
        strategies_total: 8,
        stop_reason: None,
        first_threshold_crossed_by: None,
        final_confirming_strategy: None,
        contributing_findings: vec![],
        observability_status: ObservabilityStatus::BlockedBeforeOracleLayer,
        block_summary: Some(BlockSummary {
            expected_observation_opportunities: 8,
            blocked_before_oracle_layer: 8,
            blocked_fraction: 1.0,
            dominant_block_family: BlockFamily::Authorization,
            dominant_block_reasons: vec![
                "auth gate fired before technique (no credential provided)".to_owned(),
            ],
            operator_action: Some(
                r#"Retry with --header "Authorization: Bearer <token>""#.to_owned(),
            ),
        }),
    }
}

#[test]
fn deduplicate_rules_removes_duplicates() {
    let rules = vec![
        make_rule("b"),
        make_rule("a"),
        make_rule("b"),
        make_rule("a"),
    ];
    let deduped = deduplicate_rules(rules);
    assert_eq!(deduped.len(), 2);
    let ids: Vec<&str> = deduped.iter().map(|r| r.id.as_str()).collect();
    assert!(ids.contains(&"a"));
    assert!(ids.contains(&"b"));
}

#[test]
fn deduplicate_rules_stable_with_no_duplicates() {
    let rules = vec![make_rule("z"), make_rule("a"), make_rule("m")];
    let deduped = deduplicate_rules(rules);
    assert_eq!(deduped.len(), 3);
}

#[test]
fn deduplicate_rules_empty_input_returns_empty() {
    let deduped = deduplicate_rules(vec![]);
    assert!(deduped.is_empty());
}

#[test]
fn blocked_verdict_run_properties_contain_observability_status() {
    let verdict = blocked_endpoint_verdict();
    let props = build_verdict_run_properties("https://api.example.com/users/1", &verdict);
    let additional = &props.additional_properties;
    let status_val = additional
        .get("observability_status")
        .expect("observability_status must be present in run properties");
    assert_eq!(
        status_val.as_str().unwrap_or(""),
        "BlockedBeforeOracleLayer",
        "observability_status must be 'BlockedBeforeOracleLayer'"
    );
}

#[test]
fn blocked_verdict_run_properties_contain_operator_action() {
    let verdict = blocked_endpoint_verdict();
    let props = build_verdict_run_properties("https://api.example.com/users/1", &verdict);
    let additional = &props.additional_properties;
    let action_val = additional
        .get("operator_action")
        .expect("operator_action must be present when block_summary has Some(operator_action)");
    assert!(
        action_val
            .as_str()
            .unwrap_or("")
            .contains("Authorization: Bearer"),
        "operator_action must mention 'Authorization: Bearer'; got: {action_val}"
    );
}