cargo-allow 0.1.3

Source-tree exception ledger and policy scanner for Rust repositories.
use crate::artifact_schema_support::{
    assert_enum_equals, assert_required_fields, assert_schema_type_equals, governed_kind_enum,
    parse_schema, required_schema_pointer,
};
use serde_json::Value;
use std::collections::BTreeSet;

#[test]
fn add_schema_locks_selected_finding_and_review_contract() {
    let schema = parse_schema("add", include_str!("../../../docs/schemas/add.schema.json"));

    assert_required_fields(
        "add",
        &schema,
        &[
            "schema_version",
            "schema_id",
            "tool",
            "command",
            "claim_boundary",
            "scanner_limitations",
            "inventory",
            "options",
            "summary",
            "allow_entry",
            "selected_finding",
        ],
    );

    let options = required_schema_pointer("add", &schema, "/properties/options");
    assert_eq!(
        options.get("additionalProperties").and_then(Value::as_bool),
        Some(false),
        "add options should reject unknown fields"
    );
    assert!(
        options.get("required").is_none(),
        "add option fields should stay optional for add.v1 compatibility"
    );
    assert_add_option_properties(options);
    assert_schema_type_equals(
        "add options policy_output",
        &schema,
        "/properties/options/properties/policy_output/type",
        &["string", "null"],
    );
    assert_eq!(
        schema
            .pointer("/properties/options/properties/force/type")
            .and_then(Value::as_str),
        Some("boolean"),
        "add force should be boolean"
    );

    let summary = required_schema_pointer("add", &schema, "/properties/summary");
    assert_eq!(
        summary.get("additionalProperties").and_then(Value::as_bool),
        Some(false),
        "add summary should reject unknown fields"
    );
    assert_required_fields(
        "add summary",
        summary,
        &["entry_id", "selected_finding", "human_review_required"],
    );
    assert_eq!(
        schema
            .pointer("/properties/summary/properties/human_review_required/const")
            .and_then(Value::as_bool),
        Some(true),
        "add summaries should always require human review"
    );

    let allow_entry = required_schema_pointer("add", &schema, "/properties/allow_entry");
    assert_eq!(
        allow_entry
            .get("additionalProperties")
            .and_then(Value::as_bool),
        Some(false),
        "add allow_entry should reject unknown fields"
    );
    assert_required_fields(
        "add allow_entry",
        allow_entry,
        &[
            "id",
            "kind",
            "family",
            "path",
            "glob",
            "owner",
            "classification",
            "reason",
            "review_after",
            "expires",
            "evidence_count",
            "selector",
            "last_seen",
        ],
    );
    assert_enum_equals(
        "add allow_entry kind",
        &schema,
        "/properties/allow_entry/properties/kind/enum",
        &governed_kind_enum(),
    );
    assert_eq!(
        schema
            .pointer("/properties/allow_entry/properties/evidence_count/type")
            .and_then(Value::as_str),
        Some("integer"),
        "add allow_entry evidence_count should be an integer"
    );

    assert_eq!(
        schema
            .pointer("/properties/selected_finding/$ref")
            .and_then(Value::as_str),
        Some("#/$defs/finding"),
        "add selected_finding should use finding rows"
    );
    let selected_finding = required_schema_pointer("add", &schema, "/$defs/finding");
    assert_eq!(
        selected_finding
            .get("additionalProperties")
            .and_then(Value::as_bool),
        Some(false),
        "add selected findings should reject unknown fields"
    );
    assert_required_fields(
        "add selected finding",
        selected_finding,
        &[
            "status",
            "kind",
            "family",
            "path",
            "line",
            "column",
            "source_package",
            "identity",
            "message",
        ],
    );
    assert_enum_equals(
        "add selected finding kind",
        &schema,
        "/$defs/finding/properties/kind/enum",
        &governed_kind_enum(),
    );
    assert_eq!(
        schema
            .pointer("/$defs/finding/properties/status/const")
            .and_then(Value::as_str),
        Some("selected"),
        "add selected finding status should stay selected"
    );
    assert_eq!(
        schema
            .pointer("/$defs/finding/properties/identity/$ref")
            .and_then(Value::as_str),
        Some("#/$defs/structural_identity"),
        "add selected finding identity should use structural identity rows"
    );
    let structural_identity = required_schema_pointer("add", &schema, "/$defs/structural_identity");
    assert_eq!(
        structural_identity
            .get("additionalProperties")
            .and_then(Value::as_bool),
        Some(false),
        "add structural identity should reject unknown fields"
    );
    assert_required_fields(
        "add structural identity",
        structural_identity,
        &[
            "language",
            "crate_name",
            "module",
            "container",
            "ast_kind",
            "symbol",
            "callee",
            "macro_name",
            "lint",
            "receiver_fingerprint",
            "target_fingerprint",
            "normalized_snippet_hash",
            "line_hint",
            "column_hint",
        ],
    );
    assert_schema_type_equals(
        "add selected finding source_package",
        &schema,
        "/$defs/finding/properties/source_package/type",
        &["string", "null"],
    );
}

fn assert_add_option_properties(options: &Value) {
    let Some(properties) = options.get("properties").and_then(Value::as_object) else {
        std::panic::panic_any("add options properties should be an object");
    };
    let actual = properties
        .keys()
        .map(String::as_str)
        .collect::<BTreeSet<_>>();
    let expected = ["policy_output", "force"]
        .into_iter()
        .collect::<BTreeSet<_>>();

    assert_eq!(actual, expected, "add option schema properties");
}