cargo-allow 0.1.3

Source-tree exception ledger and policy scanner for Rust repositories.
use crate::artifact_contract_samples::{
    ArtifactSample, command_artifact_samples, core_artifact_samples,
};
use crate::artifact_contract_support::parse_json_artifact;
use crate::artifact_schema_support::{parse_schema, schema_contracts};
use serde_json::Value;
use std::collections::BTreeSet;

#[test]
fn command_artifacts_keep_explicit_top_level_contracts() {
    let samples = command_artifact_samples();
    for sample in samples {
        assert_artifact_contract(&sample);
    }
}

#[test]
fn fixed_command_artifacts_have_top_level_sample_coverage() {
    let mut samples = command_artifact_samples();
    samples.extend(core_artifact_samples());
    assert_sample_coverage_matches_fixed_command_contracts(&samples);
}

#[test]
fn report_artifacts_have_top_level_sample_coverage_for_each_report_command() {
    let samples = core_artifact_samples();
    let expected = allow_report::REPORT_COMMANDS
        .iter()
        .copied()
        .collect::<BTreeSet<_>>();
    let actual = samples
        .iter()
        .filter(|sample| sample.schema_name == "report")
        .map(|sample| sample.expected_command)
        .collect::<BTreeSet<_>>();

    assert_eq!(
        actual, expected,
        "the shared report schema should have top-level sample coverage for every producer command"
    );
}

#[test]
fn core_artifacts_keep_explicit_top_level_contracts() {
    for sample in core_artifact_samples() {
        assert_artifact_contract(&sample);
    }
}

#[test]
fn artifact_samples_keep_nested_keys_covered_by_schemas() {
    let mut samples = command_artifact_samples();
    samples.extend(core_artifact_samples());

    for sample in samples {
        assert_artifact_nested_schema_contract(&sample);
    }
}

fn assert_sample_coverage_matches_fixed_command_contracts(samples: &[ArtifactSample]) {
    let expected = schema_contracts()
        .into_iter()
        .filter(|contract| contract.fixed_command.is_some())
        .map(|contract| contract.name)
        .collect::<BTreeSet<_>>();
    let actual = samples
        .iter()
        .map(|sample| sample.schema_name)
        .filter(|schema_name| expected.contains(schema_name))
        .collect::<BTreeSet<_>>();
    assert_eq!(
        actual, expected,
        "every fixed-command artifact contract should have top-level sample coverage"
    );
}

fn schema_contract_by_name(name: &str) -> crate::artifact_schema_support::SchemaContract {
    schema_contracts()
        .into_iter()
        .find(|contract| contract.name == name)
        .unwrap_or_else(|| std::panic::panic_any(format!("missing schema contract {name}")))
}

fn assert_artifact_contract(sample: &ArtifactSample) {
    let contract = schema_contract_by_name(sample.schema_name);
    let value = parse_json_artifact(
        sample.name,
        &sample.json,
        contract.schema_id,
        sample.expected_command,
    );
    assert_eq!(
        value.get("tool").and_then(Value::as_str),
        Some("cargo-allow"),
        "{} tool",
        sample.name
    );
    assert_top_level_keys(sample.name, &value, sample.expected_top_level_keys);
    assert_schema_covers_sample_top_level_keys(sample.name, contract.name, &value);
    assert_sample_inventory_scanner_matches_schema(sample.name, contract.name, &value);
    assert_string_array_eq(
        sample.name,
        &value,
        "claim_boundary",
        allow_report::CLAIM_BOUNDARY,
    );
    assert_string_array_eq(
        sample.name,
        &value,
        "scanner_limitations",
        allow_report::SCANNER_LIMITATIONS,
    );
}

fn assert_artifact_nested_schema_contract(sample: &ArtifactSample) {
    let contract = schema_contract_by_name(sample.schema_name);
    let value = parse_json_artifact(
        sample.name,
        &sample.json,
        contract.schema_id,
        sample.expected_command,
    );
    let schema = parse_schema(contract.name, contract.schema);

    if let Err(message) = crate::artifact_sample_schema_support::schema_covers_sample_value(
        &schema, &schema, &value, "$",
    ) {
        std::panic::panic_any(format!(
            "{} sample emitted JSON outside {} schema: {message}",
            sample.name, contract.name
        ));
    }
}

fn assert_sample_inventory_scanner_matches_schema(name: &str, schema_name: &str, value: &Value) {
    let contract = schema_contract_by_name(schema_name);
    assert_eq!(
        value.pointer("/inventory/scanner").and_then(Value::as_str),
        Some(contract.inventory_scanner),
        "{name} inventory scanner"
    );
}

fn assert_top_level_keys(name: &str, value: &Value, expected: &[&str]) {
    let Some(object) = value.as_object() else {
        std::panic::panic_any(format!("{name} artifact should be a JSON object"));
    };
    let actual = object.keys().map(String::as_str).collect::<BTreeSet<_>>();
    let expected = expected.iter().copied().collect::<BTreeSet<_>>();
    assert_eq!(actual, expected, "{name} top-level keys");
}

fn assert_schema_covers_sample_top_level_keys(name: &str, schema_name: &str, value: &Value) {
    let contract = schema_contract_by_name(schema_name);
    let schema = parse_schema(contract.name, contract.schema);
    let Some(sample) = value.as_object() else {
        std::panic::panic_any(format!("{name} sample should be a JSON object"));
    };
    let Some(properties) = schema.get("properties").and_then(Value::as_object) else {
        std::panic::panic_any(format!("{name} schema properties should be an object"));
    };
    let allowed = properties
        .keys()
        .map(String::as_str)
        .collect::<BTreeSet<_>>();
    let actual = sample.keys().map(String::as_str).collect::<BTreeSet<_>>();
    let unknown = actual.difference(&allowed).copied().collect::<Vec<_>>();
    assert!(
        unknown.is_empty(),
        "{name} sample emitted top-level keys absent from schema properties: {}",
        unknown.join(", ")
    );

    let Some(required) = schema.get("required").and_then(Value::as_array) else {
        std::panic::panic_any(format!("{name} schema required should be an array"));
    };
    let missing = required
        .iter()
        .map(|field| {
            field.as_str().unwrap_or_else(|| {
                std::panic::panic_any(format!("{name} schema required entries should be strings"))
            })
        })
        .filter(|field| !actual.contains(field))
        .collect::<Vec<_>>();
    assert!(
        missing.is_empty(),
        "{name} sample omitted schema-required top-level keys: {}",
        missing.join(", ")
    );
}

fn assert_string_array_eq(name: &str, value: &Value, field: &str, expected: &[&str]) {
    let Some(items) = value.get(field).and_then(Value::as_array) else {
        std::panic::panic_any(format!("{name} {field} should be an array"));
    };
    let actual = items
        .iter()
        .map(|item| {
            item.as_str().unwrap_or_else(|| {
                std::panic::panic_any(format!("{name} {field} entries should be strings"))
            })
        })
        .collect::<Vec<_>>();
    assert_eq!(actual, expected, "{name} {field}");
}