helm-schema 0.0.7

Generate an accurate JSON schema for any helm chart
Documentation
use color_eyre::eyre::{self, OptionExt as _};
use serde_json::json;
use test_util::prelude::sim_assert_eq;

use super::{OwnedDefinitions, prune_unreachable_owned_definitions};

#[test]
fn jsonschema_resolver_decodes_the_whole_uri_fragment_before_pointer_splitting() -> eyre::Result<()>
{
    let encoded_separator = json!({
        "$defs": {
            "a": { "b": { "const": "nested" } },
            "a/b": { "const": "flat" },
        },
        "$ref": "#/$defs/a%2Fb",
    });
    let validator = jsonschema::validator_for(&encoded_separator)
        .map_err(|error| eyre::eyre!("compile encoded-separator schema: {error}"))?;
    sim_assert_eq!(have: validator.is_valid(&json!("nested")), want: true);
    sim_assert_eq!(have: validator.is_valid(&json!("flat")), want: false);

    let encoded_tilde_escape = json!({
        "$defs": {
            "a/b": { "const": "slash" },
            "a~1b": { "const": "literal-tilde" },
        },
        "$ref": "#/$defs/a%7E1b",
    });
    let validator = jsonschema::validator_for(&encoded_tilde_escape)
        .map_err(|error| eyre::eyre!("compile encoded-tilde schema: {error}"))?;
    sim_assert_eq!(have: validator.is_valid(&json!("slash")), want: true);
    sim_assert_eq!(have: validator.is_valid(&json!("literal-tilde")), want: false);

    let encoded_pointer = json!({
        "$defs": {
            "name": { "const": "decoded" },
        },
        "$ref": "#%2F$defs%2Fname",
    });
    let error = jsonschema::validator_for(&encoded_pointer)
        .err()
        .ok_or_eyre("encoded anchor unexpectedly compiled as a JSON Pointer")?;
    sim_assert_eq!(
        have: error.to_string().contains("Anchor '%2F$defs%2Fname' does not exist"),
        want: true
    );
    Ok(())
}

#[test]
fn late_prune_closes_transitive_reachability_and_removes_only_orphans() {
    let mut schema = json!({
        "$defs": {
            "kept": { "$ref": "#/$defs/transitive" },
            "orphan": { "type": "number" },
            "transitive": { "type": "string" },
        },
        "properties": {
            "value": { "$ref": "#/$defs/kept" },
        },
        "type": "object",
    });
    let owned = OwnedDefinitions::capture(&schema).retain_unchanged(&schema);

    let removed = prune_unreachable_owned_definitions(&mut schema, &owned);

    sim_assert_eq!(have: removed, want: 1);
    sim_assert_eq!(
        have: schema.get("$defs"),
        want: Some(&json!({
            "kept": { "$ref": "#/$defs/transitive" },
            "transitive": { "type": "string" },
        }))
    );
}

#[test]
fn late_prune_preserves_caller_modified_and_caller_added_definitions() {
    let generated = json!({
        "$defs": {
            "generated": { "type": "string" },
        },
        "type": "object",
    });
    let captured = OwnedDefinitions::capture(&generated);
    let mut overridden = json!({
        "$defs": {
            "caller": { "type": "boolean" },
            "generated": { "type": "integer" },
        },
        "type": "object",
    });
    let owned = captured.retain_unchanged(&overridden);

    let removed = prune_unreachable_owned_definitions(&mut overridden, &owned);

    sim_assert_eq!(have: removed, want: 0);
    sim_assert_eq!(
        have: overridden.get("$defs"),
        want: Some(&json!({
            "caller": { "type": "boolean" },
            "generated": { "type": "integer" },
        }))
    );
}

#[test]
fn retained_caller_definition_keeps_owned_transitive_references() {
    let generated = json!({
        "$defs": {
            "caller": { "type": "string" },
            "owned": { "type": "integer" },
        },
        "type": "object",
    });
    let captured = OwnedDefinitions::capture(&generated);
    let mut overridden = json!({
        "$defs": {
            "caller": { "$ref": "#/$defs/owned" },
            "owned": { "type": "integer" },
        },
        "type": "object",
    });
    let owned = captured.retain_unchanged(&overridden);

    let removed = prune_unreachable_owned_definitions(&mut overridden, &owned);

    sim_assert_eq!(have: removed, want: 0);
    assert!(overridden.pointer("/$defs/owned").is_some());
}

#[test]
fn late_prune_decodes_definition_names_in_json_pointer_refs() {
    let mut schema = json!({
        "$defs": {
            "provider/name~shape": { "type": "string" },
        },
        "$ref": "#/$defs/provider~1name~0shape",
    });
    let owned = OwnedDefinitions::capture(&schema).retain_unchanged(&schema);

    let removed = prune_unreachable_owned_definitions(&mut schema, &owned);

    sim_assert_eq!(have: removed, want: 0);
}

#[test]
fn late_prune_decodes_percent_encoded_definition_refs() {
    let mut schema = json!({
        "$defs": {
            "plain": { "type": "string" },
            "space name": { "type": "integer" },
        },
        "allOf": [
            { "$ref": "#/%24defs/plain" },
            { "$ref": "#/$defs/space%20name" },
        ],
    });
    let owned = OwnedDefinitions::capture(&schema).retain_unchanged(&schema);

    let removed = prune_unreachable_owned_definitions(&mut schema, &owned);

    sim_assert_eq!(have: removed, want: 0);
}

#[test]
fn late_prune_percent_decodes_before_json_pointer_tilde_decoding() {
    let mut schema = json!({
        "$defs": {
            "a/b": { "type": "string" },
            "a~1b": { "type": "integer" },
        },
        "$ref": "#/$defs/a%7E1b",
    });
    let owned = OwnedDefinitions::capture(&schema).retain_unchanged(&schema);

    let removed = prune_unreachable_owned_definitions(&mut schema, &owned);

    sim_assert_eq!(have: removed, want: 1);
    sim_assert_eq!(
        have: schema.get("$defs"),
        want: Some(&json!({ "a/b": { "type": "string" } }))
    );
}

#[test]
fn late_prune_decodes_the_whole_fragment_before_splitting_pointer_segments() {
    let mut schema = json!({
        "$defs": {
            "provider": {
                "properties": {
                    "name": { "type": "string" },
                },
                "type": "object",
            },
            "provider/name": { "type": "boolean" },
            "orphan": { "type": "integer" },
        },
        "$ref": "#/%24defs/provider%2Fname",
    });
    let owned = OwnedDefinitions::capture(&schema).retain_unchanged(&schema);

    let removed = prune_unreachable_owned_definitions(&mut schema, &owned);

    sim_assert_eq!(have: removed, want: 2);
    sim_assert_eq!(
        have: schema.get("$defs"),
        want: Some(&json!({
            "provider": {
                "properties": {
                    "name": { "type": "string" },
                },
                "type": "object",
            },
        }))
    );
}

#[test]
fn late_prune_preserves_definitions_for_undecodable_local_refs() {
    let mut schema = json!({
        "$defs": {
            "one": { "type": "string" },
            "two": { "type": "integer" },
        },
        "$ref": "#/$defs/%FF",
    });
    let owned = OwnedDefinitions::capture(&schema).retain_unchanged(&schema);

    let removed = prune_unreachable_owned_definitions(&mut schema, &owned);

    sim_assert_eq!(have: removed, want: 0);
}

#[test]
fn late_prune_follows_root_refs_from_nested_definition_scopes() {
    let mut schema = json!({
        "$defs": {
            "rootTarget": { "type": "string" },
        },
        "properties": {
            "value": {
                "$defs": {
                    "local": { "$ref": "#/$defs/rootTarget" },
                },
                "$ref": "#/$defs/local",
            },
        },
        "type": "object",
    });
    let owned = OwnedDefinitions::capture(&schema).retain_unchanged(&schema);

    let removed = prune_unreachable_owned_definitions(&mut schema, &owned);

    sim_assert_eq!(have: removed, want: 0);
    assert!(schema.pointer("/$defs/rootTarget").is_some());
}

#[test]
fn late_prune_is_validation_equivalent_across_definition_keywords() -> eyre::Result<()> {
    let mut after = json!({
        "$defs": {
            "kept": { "type": "string" },
            "orphan": { "type": "boolean" },
        },
        "definitions": {
            "kept": { "type": "integer" },
            "orphan": { "type": "array" },
        },
        "anyOf": [
            { "$ref": "#/$defs/kept" },
            { "$ref": "#/definitions/kept" },
        ],
    });
    let before = after.clone();
    let owned = OwnedDefinitions::capture(&after).retain_unchanged(&after);

    sim_assert_eq!(
        have: prune_unreachable_owned_definitions(&mut after, &owned),
        want: 2
    );
    let before_validator = jsonschema::validator_for(&before)
        .map_err(|error| eyre::eyre!("compile pre-prune schema: {error}"))?;
    let after_validator = jsonschema::validator_for(&after)
        .map_err(|error| eyre::eyre!("compile post-prune schema: {error}"))?;
    for probe in [
        json!(null),
        json!(false),
        json!(0),
        json!(1.5),
        json!("value"),
        json!([]),
        json!({}),
    ] {
        sim_assert_eq!(
            have: after_validator.is_valid(&probe),
            want: before_validator.is_valid(&probe),
            "late reachability pruning changed acceptance for {probe}"
        );
    }
    Ok(())
}