cargo-allow 0.1.3

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

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

    let Some(defs) = schema.get("$defs").and_then(Value::as_object) else {
        std::panic::panic_any("common schema $defs should be an object");
    };
    let actual = defs.keys().map(String::as_str).collect::<BTreeSet<_>>();
    let expected = [
        "allow_entry",
        "canonical_evidence_prefix",
        "claim_boundary_flag",
        "counts",
        "current_finding",
        "diff",
        "diff_summary",
        "evidence_change",
        "evidence_change_field",
        "evidence_reference",
        "evidence_reference_category",
        "evidence_reference_status",
        "exception_identity_change",
        "exception_identity_change_field",
        "finding",
        "finding_posture_change",
        "finding_posture_kind",
        "governed_source_exception_kind",
        "inventory",
        "inventory_source",
        "lifecycle",
        "local_file_evidence_prefix",
        "lifecycle_change",
        "lifecycle_change_field",
        "match_status",
        "metadata_change",
        "metadata_change_field",
        "occurrence_limit_change",
        "outcome",
        "policy_change",
        "policy_change_kind",
        "policy_change_severity",
        "policy_status_change",
        "policy_migration_inventory",
        "recognized_evidence_prefix",
        "requirement_change",
        "requirement_change_field",
        "scanner_limitation",
        "selector",
        "selector_identity_change",
        "selector_identity_change_field",
        "selector_precision_change",
        "selector_precision_field",
        "selected_finding",
        "scope_change",
        "scope_change_field",
        "span",
        "stale_entry",
        "summary",
        "structural_identity",
        "source_syntax_inventory",
        "source_inventory",
        "source_inventory_family_row",
        "source_inventory_kind_row",
        "trend",
        "traceability_evidence_prefix",
        "work_item",
        "worklist_filters",
    ]
    .into_iter()
    .collect::<BTreeSet<_>>();

    assert_eq!(
        actual, expected,
        "common.v1 shared fragment names are part of the schema compatibility surface"
    );
}

#[test]
fn report_schema_fragments_are_mirrored_in_common_catalog() {
    let common = parse_schema(
        "common",
        include_str!("../../../docs/schemas/common.v1.json"),
    );
    let report = parse_schema(
        "report",
        include_str!("../../../docs/schemas/report.schema.json"),
    );

    let Some(common_defs) = common.get("$defs").and_then(Value::as_object) else {
        std::panic::panic_any("common schema $defs should be an object");
    };
    let Some(report_defs) = report.get("$defs").and_then(Value::as_object) else {
        std::panic::panic_any("report schema $defs should be an object");
    };

    let missing = report_defs
        .keys()
        .filter(|name| !common_defs.contains_key(*name))
        .map(String::as_str)
        .collect::<Vec<_>>();

    assert!(
        missing.is_empty(),
        "report.schema.json $defs should be mirrored in common.v1.json: {missing:?}"
    );
}

#[test]
fn artifact_local_fragments_match_common_wire_shapes() {
    let common = parse_schema(
        "common",
        include_str!("../../../docs/schemas/common.v1.json"),
    );
    let report = parse_schema(
        "report",
        include_str!("../../../docs/schemas/report.schema.json"),
    );
    let receipt = parse_schema(
        "receipt",
        include_str!("../../../docs/schemas/receipt.schema.json"),
    );
    let prune = parse_schema(
        "prune",
        include_str!("../../../docs/schemas/prune.schema.json"),
    );
    let add = parse_schema("add", include_str!("../../../docs/schemas/add.schema.json"));
    let explain = parse_schema(
        "explain",
        include_str!("../../../docs/schemas/explain.schema.json"),
    );
    let worklist = parse_schema(
        "worklist",
        include_str!("../../../docs/schemas/worklist.schema.json"),
    );

    for fragment in [
        "diff",
        "diff_summary",
        "finding",
        "inventory",
        "outcome",
        "summary",
        "trend",
        "source_inventory",
        "source_inventory_kind_row",
        "source_inventory_family_row",
        "finding_posture_change",
        "policy_change",
        "selector_precision_field",
        "selector_precision_change",
        "scope_change_field",
        "scope_change",
        "occurrence_limit_change",
        "lifecycle_change_field",
        "lifecycle_change",
        "evidence_change_field",
        "evidence_change",
        "exception_identity_change_field",
        "exception_identity_change",
        "selector_identity_change_field",
        "selector_identity_change",
        "metadata_change_field",
        "metadata_change",
        "requirement_change_field",
        "requirement_change",
        "policy_status_change",
    ] {
        assert_common_fragment_matches("report", &report, &common, fragment);
    }

    for (schema_name, schema) in [("add", &add), ("explain", &explain)] {
        assert_common_fragment_matches(schema_name, schema, &common, "structural_identity");
        assert_common_fragment_matches(schema_name, schema, &common, "selector");
    }

    assert_common_fragment_matches_named("add", &add, "finding", &common, "selected_finding");
    assert_common_fragment_matches_named(
        "explain",
        &explain,
        "current_finding",
        &common,
        "current_finding",
    );

    for (schema_name, schema) in [("explain", &explain), ("worklist", &worklist)] {
        assert_common_fragment_matches(schema_name, schema, &common, "evidence_reference");
        assert_common_fragment_matches(schema_name, schema, &common, "evidence_reference_category");
        assert_common_fragment_matches(schema_name, schema, &common, "evidence_reference_status");
    }

    assert_common_fragment_matches_named(
        "worklist",
        &worklist,
        "filters",
        &common,
        "worklist_filters",
    );
    assert_common_fragment_matches("worklist", &worklist, &common, "work_item");

    for fragment in ["allow_entry", "lifecycle", "span"] {
        assert_common_fragment_matches("explain", &explain, &common, fragment);
    }

    for fragment in [
        "counts",
        "source_inventory",
        "source_inventory_kind_row",
        "source_inventory_family_row",
    ] {
        assert_common_fragment_matches("receipt", &receipt, &common, fragment);
    }
    assert_common_fragment_matches("prune", &prune, &common, "stale_entry");
    assert_common_fragment_matches_named("explain", &explain, "match_outcome", &common, "outcome");
}

fn assert_common_fragment_matches(
    schema_name: &str,
    schema: &Value,
    common: &Value,
    fragment: &str,
) {
    assert_common_fragment_matches_named(schema_name, schema, fragment, common, fragment);
}

fn assert_common_fragment_matches_named(
    schema_name: &str,
    schema: &Value,
    schema_fragment: &str,
    common: &Value,
    common_fragment: &str,
) {
    let schema_pointer = format!("/$defs/{schema_fragment}");
    let common_pointer = format!("/$defs/{common_fragment}");
    let schema_fragment = required_schema_pointer(schema_name, schema, &schema_pointer);
    let common_fragment = required_schema_pointer("common", common, &common_pointer);
    assert_eq!(
        schema_wire_shape(schema_fragment),
        schema_wire_shape(common_fragment),
        "{schema_name} {schema_pointer} should match common.v1 {common_pointer} wire shape"
    );
}

fn schema_wire_shape(value: &Value) -> Value {
    match value {
        Value::Object(object) => {
            let mut clean = Map::new();
            for (key, child) in object {
                if key != "description" {
                    clean.insert(key.clone(), schema_wire_shape(child));
                }
            }
            Value::Object(clean)
        }
        Value::Array(items) => Value::Array(items.iter().map(schema_wire_shape).collect()),
        _ => value.clone(),
    }
}