allow-report 0.1.7

Report and receipt rendering for cargo-allow source exception scans.
Documentation
use super::*;
use allow_core::{Finding, FindingKind, MatchOutcome, MatchStatus, Span, StructuralIdentity};
use std::path::PathBuf;

fn context(source: &'static str) -> ReportContext<'static> {
    ReportContext::source_syntax(source, None, None, None)
}

#[test]
fn sarif_report_emits_non_matched_results_with_locations() {
    let findings = vec![file_finding(
        FindingKind::NonRustFile,
        "shell_script",
        "scripts/new.sh",
    )];
    let outcomes = vec![
        outcome(MatchStatus::Matched, Some(0)),
        MatchOutcome {
            status: MatchStatus::New,
            allow_id: None,
            finding_index: Some(0),
            message: "unreceipted shell script at scripts/new.sh".to_string(),
            score: 0,
        },
    ];

    let sarif =
        render_sarif_with_context("check", &findings, &outcomes, true, context("git_tracked"));

    assert!(sarif.contains("\"version\": \"2.1.0\""));
    assert!(sarif.contains("\"name\": \"cargo-allow\""));
    assert!(sarif.contains("\"ruleId\": \"cargo-allow/new\""));
    assert!(sarif.contains("\"level\": \"error\""));
    assert!(sarif.contains("\"uri\": \"scripts/new.sh\""));
    assert!(sarif.contains("\"startLine\": 1"));
    assert!(sarif.contains("\"source_tree_inventory\""));
    assert!(sarif.contains("\"cargo_commands_not_invoked\""));
    assert!(!sarif.contains("\"ruleId\": \"cargo-allow/matched\""));
}

#[test]
fn sarif_result_properties_include_source_package_context() {
    let mut identity = StructuralIdentity::new("rust", "method_call");
    identity.crate_name = Some("parser".to_string());
    let findings = vec![Finding {
        kind: FindingKind::Panic,
        family: Some("unwrap".to_string()),
        path: PathBuf::from("crates/parser/src/lib.rs"),
        span: Some(Span { line: 4, column: 9 }),
        identity,
        message: "unwrap call".to_string(),
    }];
    let outcomes = vec![MatchOutcome {
        status: MatchStatus::New,
        allow_id: None,
        finding_index: Some(0),
        message: "unreceipted unwrap".to_string(),
        score: 0,
    }];

    let sarif = render_sarif("check", &findings, &outcomes, true);

    assert!(sarif.contains("\"source_package\": \"parser\""));
    assert!(sarif.contains("\"uri\": \"crates/parser/src/lib.rs\""));
}

#[test]
fn sarif_run_properties_include_policy_evidence_health_counts() {
    let mut context = context("git_tracked");
    context.baseline_debt_entries = Some(3);
    context.policy_missing_evidence_entries = Some(2);
    context.broken_evidence_links = Some(1);
    context.weak_evidence_references = Some(1);

    let sarif = render_sarif_with_context("audit", &[], &[], false, context);

    assert!(sarif.contains("\"policy_baseline_debt\": 3"));
    assert!(sarif.contains("\"policy_missing_evidence\": 2"));
    assert!(sarif.contains("\"broken_evidence_links\": 1"));
    assert!(sarif.contains("\"weak_evidence_references\": 1"));
    assert!(sarif.contains("\"evidence_repair_queues\""));
    assert!(sarif.contains("\"signal\": \"broken_evidence_links\""));
    assert!(sarif.contains("\"label\": \"broken evidence links\""));
    assert!(sarif.contains("\"route_kind\": \"worklist_filter\""));
    assert!(sarif.contains("\"item_kind\": \"broken_evidence_link\""));
    assert!(sarif.contains("\"worklist_filter\": \"broken_evidence\""));
    assert!(
        sarif.contains("\"command\": \"cargo-allow worklist --broken-evidence --format json\"")
    );
    assert!(sarif.contains("\"signal\": \"missing_evidence\""));
    assert!(sarif.contains("\"label\": \"missing evidence\""));
    assert!(sarif.contains("\"route_kind\": \"worklist_filter\""));
    assert!(sarif.contains("\"item_kind\": \"missing_evidence\""));
    assert!(sarif.contains("\"worklist_filter\": \"missing_evidence\""));
    assert!(
        sarif.contains("\"command\": \"cargo-allow worklist --missing-evidence --format json\"")
    );
    assert!(sarif.contains("\"signal\": \"weak_evidence_references\""));
    assert!(sarif.contains("\"label\": \"weak evidence references\""));
    assert!(sarif.contains("\"item_kind\": \"weak_evidence_reference\""));
    assert!(sarif.contains("\"worklist_filter\": \"weak_evidence\""));
    assert!(sarif.contains("\"command\": \"cargo-allow worklist --weak-evidence --format json\""));
    assert!(sarif.contains("\"results\": [\n\n      ]"));
}

#[test]
fn sarif_run_properties_route_outcome_evidence_missing_repair_queue() {
    let outcomes = vec![MatchOutcome {
        status: MatchStatus::EvidenceMissing,
        allow_id: Some("allow-unsafe-0001".to_string()),
        finding_index: None,
        message: "unsafe allow entry requires evidence".to_string(),
        score: 0,
    }];

    let sarif = render_sarif_with_context("check", &[], &outcomes, true, context("git_tracked"));

    assert!(sarif.contains("\"evidence_repair_queues\""));
    assert!(sarif.contains("\"signal\": \"missing_evidence\""));
    assert!(sarif.contains("\"label\": \"missing evidence\""));
    assert!(sarif.contains("\"route_kind\": \"worklist_filter\""));
    assert!(sarif.contains("\"item_kind\": \"missing_evidence\""));
    assert!(sarif.contains("\"worklist_filter\": \"missing_evidence\""));
    assert!(sarif.contains("\"count\": 1"));
    assert!(
        sarif.contains("\"command\": \"cargo-allow worklist --missing-evidence --format json\"")
    );
}

#[test]
fn sarif_run_properties_omit_evidence_repair_queues_when_clean() {
    let sarif = render_sarif_with_context("check", &[], &[], false, context("git_tracked"));

    assert!(!sarif.contains("\"evidence_repair_queues\""));
}

fn file_finding(kind: FindingKind, family: &str, path: &str) -> Finding {
    Finding {
        kind,
        family: Some(family.to_string()),
        path: PathBuf::from(path),
        span: Some(Span { line: 1, column: 1 }),
        identity: StructuralIdentity::new("file", "tracked_file"),
        message: "tracked non-Rust file".to_string(),
    }
}

fn outcome(status: MatchStatus, finding_index: Option<usize>) -> MatchOutcome {
    MatchOutcome {
        status,
        allow_id: None,
        finding_index,
        message: String::new(),
        score: 0,
    }
}