odcs 0.4.0

Reference implementation of the Open Data Contract Standard (ODCS)
Documentation
//! Negative validation and parser hardening tests from the 0.3.0 bug audit.

use std::fs;

use odcs::{
    codes, parse, parse_strict, validate_strict, DocumentFormat, ParseResult, MAX_PARSE_BYTES,
};

fn fixture_bytes(name: &str) -> Vec<u8> {
    fs::read(
        std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
            .join("tests/fixtures")
            .join(name),
    )
    .expect("read fixture")
}

fn parse_fixture(name: &str) -> ParseResult {
    let format = if name.ends_with(".json") {
        DocumentFormat::Json
    } else {
        DocumentFormat::Yaml
    };
    parse(&fixture_bytes(name), format)
}

fn assert_invalid_with_code(name: &str, code: &str) {
    let report = parse_fixture(name).validate();
    assert!(
        !report.is_valid(),
        "fixture {name} should be invalid: {:?}",
        report.diagnostics
    );
    assert!(
        report.diagnostics.iter().any(|d| d.id == code),
        "fixture {name}: expected {code}, got {:?}",
        report.diagnostics
    );
}

#[test]
fn quality_rules_count_includes_items() {
    let contract = parse_fixture("with-schema-quality-items.yaml")
        .into_contract()
        .expect("valid fixture");
    assert_eq!(contract.quality_rules().len(), 1);
}

#[test]
fn rejects_quality_without_type_and_bad_metric() {
    assert_invalid_with_code(
        "invalid-quality-no-type-bad-metric.yaml",
        codes::INVALID_QUALITY,
    );
}

#[test]
fn rejects_quality_with_deprecated_rule_only() {
    assert_invalid_with_code(
        "invalid-quality-deprecated-rule-only.yaml",
        codes::INVALID_QUALITY,
    );
}

#[test]
fn rejects_quality_with_empty_sql_query() {
    assert_invalid_with_code(
        "invalid-quality-empty-sql-query.yaml",
        codes::INVALID_QUALITY,
    );
}

#[test]
fn rejects_quality_with_unknown_type() {
    assert_invalid_with_code("invalid-quality-unknown-type.yaml", codes::INVALID_QUALITY);
}

#[test]
fn rejects_quality_with_invalid_between_length() {
    assert_invalid_with_code(
        "invalid-quality-between-length.yaml",
        codes::INVALID_QUALITY,
    );
}

#[test]
fn rejects_relationship_empty_composite_member() {
    assert_invalid_with_code(
        "invalid-relationship-empty-composite.yaml",
        codes::UNRESOLVED_REFERENCE,
    );
}

#[test]
fn rejects_relationship_bad_format() {
    assert_invalid_with_code(
        "invalid-relationship-bad-format.yaml",
        codes::UNRESOLVED_REFERENCE,
    );
}

#[test]
fn rejects_relationship_composite_length_mismatch() {
    assert_invalid_with_code(
        "invalid-relationship-length-mismatch.yaml",
        codes::UNRESOLVED_REFERENCE,
    );
}

#[test]
fn rejects_relationship_dangling_reference() {
    assert_invalid_with_code(
        "invalid-relationship-dangling.yaml",
        codes::UNRESOLVED_REFERENCE,
    );
}

#[test]
fn rejects_server_missing_canonical_name() {
    assert_invalid_with_code("invalid-server-typo.yaml", codes::MISSING_REQUIRED_FIELD);
}

#[test]
fn rejects_extension_empty_key_in_servers() {
    assert_invalid_with_code("invalid-extension-empty-key.yaml", codes::INVALID_EXTENSION);
}

#[test]
fn rejects_extension_duplicate_keys() {
    assert_invalid_with_code("invalid-extension-duplicate.yaml", codes::INVALID_EXTENSION);
}

#[test]
fn rejects_schema_array_without_items() {
    assert_invalid_with_code(
        "invalid-schema-array-without-items.yaml",
        codes::INVALID_SCHEMA,
    );
}

#[test]
fn rejects_invalid_stable_id() {
    assert_invalid_with_code("invalid-stable-id.yaml", codes::INVALID_EXTENSION);
}

#[test]
fn rejects_yaml_duplicate_root_key() {
    let report = parse_fixture("invalid-duplicate-key.yaml").report;
    assert!(!report.is_valid());
    assert!(report
        .diagnostics
        .iter()
        .any(|d| d.id == codes::DUPLICATE_KEY));
}

#[test]
fn rejects_json_duplicate_key() {
    let report = parse_fixture("invalid-duplicate-key.json").report;
    assert!(!report.is_valid());
    assert!(report
        .diagnostics
        .iter()
        .any(|d| d.id == codes::DUPLICATE_KEY));
}

#[test]
fn parse_strict_rejects_invalid_kind() {
    let result = parse_strict(&fixture_bytes("invalid-kind.yaml"), DocumentFormat::Yaml);
    assert!(result.is_err());
}

#[test]
fn rejects_oversized_document() {
    let oversized = vec![b' '; MAX_PARSE_BYTES as usize + 1];
    let report = parse(&oversized, DocumentFormat::Yaml).report;
    assert!(!report.is_valid());
    assert!(report
        .diagnostics
        .iter()
        .any(|d| d.id == codes::DOCUMENT_TOO_LARGE));
}

#[test]
fn nested_unknown_field_includes_object_ref() {
    let report = parse_fixture("nested-unknown-field.yaml").report;
    assert!(!report.is_valid());
    assert!(report.diagnostics.iter().any(|d| d.object_ref.is_some()));
}

#[test]
fn api_version_must_be_supported() {
    let yaml = br#"
version: "1.0.0"
apiVersion: "v3.0.2"
kind: "DataContract"
id: "mismatch"
status: "draft"
schema:
  - name: "customers"
    logicalType: "object"
    properties:
      - name: "customer_id"
        logicalType: "string"
"#;
    let report = parse(yaml, DocumentFormat::Yaml).validate();
    assert!(!report.is_valid());
    assert!(report
        .diagnostics
        .iter()
        .any(|d| d.id == codes::UNSUPPORTED_VERSION));
}

#[test]
fn accepts_upstream_document_version_with_supported_api_version() {
    let yaml = br#"
version: "1.0.0"
apiVersion: "v3.1.0"
kind: "DataContract"
id: "upstream-version"
status: "draft"
schema:
  - name: "customers"
    logicalType: "object"
    properties:
      - name: "customer_id"
        logicalType: "string"
"#;
    let report = parse(yaml, DocumentFormat::Yaml).validate();
    assert!(
        report.is_valid(),
        "upstream document version should be accepted: {:?}",
        report.diagnostics
    );
}

#[test]
fn sla_large_integer_preserves_precision() {
    let yaml = br#"
version: "3.1.0"
apiVersion: "v3.1.0"
kind: "DataContract"
id: "sla-large-int"
status: "active"
slaProperties:
  - property: "freshness"
    value: 9007199254740993
schema:
  - name: "customers"
    logicalType: "object"
    properties:
      - name: "customer_id"
        logicalType: "string"
"#;
    let contract = parse(yaml, DocumentFormat::Yaml)
        .into_contract()
        .expect("valid contract");
    let value = serde_json::to_value(&contract.sla_properties[0].value).expect("serialize sla");
    assert_eq!(value, serde_json::json!(9_007_199_254_740_993_i64));
}

#[test]
fn rejects_property_relationship_invalid_from() {
    assert_invalid_with_code(
        "invalid-relationship-from.yaml",
        codes::UNRESOLVED_REFERENCE,
    );
}

#[test]
fn rejects_nested_property_shorthand_reference() {
    assert_invalid_with_code(
        "invalid-nested-property-ref.yaml",
        codes::UNRESOLVED_REFERENCE,
    );
}

#[test]
fn rejects_invalid_server_type() {
    assert_invalid_with_code("invalid-server-type.yaml", codes::INVALID_SCHEMA);
}

#[test]
fn rejects_invalid_quality_dimension_in_default_mode() {
    assert_invalid_with_code("invalid-quality-dimension.yaml", codes::INVALID_QUALITY);
}

#[test]
fn rejects_invalid_logical_type_in_default_mode() {
    assert_invalid_with_code("invalid-logical-type.yaml", codes::INVALID_SCHEMA);
}

#[test]
fn strict_mode_rejects_json_schema_violation() {
    let result = parse_fixture("invalid-json-schema-only.yaml");
    let contract = result.contract.expect("parsed contract");
    let report = validate_strict(&contract);
    assert!(!report.is_valid());
    assert!(report
        .diagnostics
        .iter()
        .any(|d| d.id == codes::INVALID_QUALITY || d.id == codes::JSON_SCHEMA_VIOLATION));
}

#[test]
fn sla_description_and_scheduler_round_trip() {
    let contract = parse_fixture("with-sla-description.yaml")
        .into_contract()
        .expect("valid fixture");
    assert_eq!(
        contract.sla_properties[0].description.as_deref(),
        Some("Data available within 24 hours")
    );
    assert_eq!(
        contract.sla_properties[0].scheduler.as_deref(),
        Some("cron")
    );
}