protovalidate-buffa 0.2.1

Runtime for protoc-gen-protovalidate-buffa — Validate trait, ValidationError types, CEL integration, rule helpers.
Documentation
//! `AsCelValue` impls for `google.protobuf.FieldMask`, `Timestamp`, and
//! `Duration`. Each test compiles and runs a real CEL expression against the
//! WKT, exercising the same `CelConstraint::eval` path used by
//! plugin-emitted field-level predefined rules.

use buffa_types::google::protobuf::{Duration, FieldMask, Timestamp};
use protovalidate_buffa::cel::CelConstraint;

#[test]
fn field_mask_paths_all_true() {
    static RULE: CelConstraint = CelConstraint::new(
        "test.field_mask.paths_all",
        "every path must be 'display_name' or 'icon'",
        "this.paths.all(p, p == 'display_name' || p == 'icon')",
    );

    let mask = FieldMask {
        paths: vec!["display_name".to_string(), "icon".to_string()],
        ..FieldMask::default()
    };

    RULE.eval(&mask).expect("rule should pass");
}

#[test]
fn field_mask_paths_all_false() {
    static RULE: CelConstraint = CelConstraint::new(
        "test.field_mask.paths_all",
        "path not allowed",
        "this.paths.all(p, p == 'display_name')",
    );

    let mask = FieldMask {
        paths: vec!["display_name".to_string(), "icon".to_string()],
        ..FieldMask::default()
    };

    let err = RULE.eval(&mask).expect_err("rule should reject 'icon'");
    assert_eq!(err.rule_id, "test.field_mask.paths_all");
}

#[test]
fn field_mask_size_zero_empty() {
    static RULE: CelConstraint = CelConstraint::new(
        "test.field_mask.non_empty",
        "field mask must not be empty",
        "size(this.paths) > 0",
    );

    let empty = FieldMask::default();
    RULE.eval(&empty).expect_err("empty mask should fail");

    let nonempty = FieldMask {
        paths: vec!["x".to_string()],
        ..FieldMask::default()
    };
    RULE.eval(&nonempty).expect("non-empty mask should pass");
}

#[test]
fn duration_compare_positive() {
    static RULE: CelConstraint = CelConstraint::new(
        "test.duration.positive",
        "must be greater than zero",
        "this > duration('0s')",
    );

    let positive = Duration {
        seconds: 5,
        nanos: 0,
        ..Duration::default()
    };
    RULE.eval(&positive).expect("positive duration passes");

    let zero = Duration::default();
    RULE.eval(&zero).expect_err("zero duration fails");
}

#[test]
fn duration_nanos_resolution() {
    static RULE: CelConstraint = CelConstraint::new(
        "test.duration.sub_second",
        "must be at least 1ms",
        "this >= duration('0.001s')",
    );

    let one_ms = Duration {
        seconds: 0,
        nanos: 1_000_000,
        ..Duration::default()
    };
    RULE.eval(&one_ms).expect("1ms passes");

    let half_ms = Duration {
        seconds: 0,
        nanos: 500_000,
        ..Duration::default()
    };
    RULE.eval(&half_ms).expect_err("0.5ms fails");
}

#[test]
fn timestamp_compare_before_max() {
    static RULE: CelConstraint = CelConstraint::new(
        "test.timestamp.before_max",
        "must be before 9999-12-31",
        "this < timestamp('9999-12-31T23:59:59Z')",
    );

    let ts = Timestamp {
        seconds: 1_700_000_000,
        nanos: 0,
        ..Timestamp::default()
    };
    RULE.eval(&ts).expect("normal timestamp passes");
}

#[test]
fn timestamp_in_past() {
    static RULE: CelConstraint = CelConstraint::new(
        "test.timestamp.in_past",
        "must be in the past",
        "this < now",
    );

    let past = Timestamp {
        seconds: 1_000_000_000,
        nanos: 0,
        ..Timestamp::default()
    };
    RULE.eval(&past).expect("past timestamp passes");

    let future = Timestamp {
        seconds: 99_999_999_999,
        nanos: 0,
        ..Timestamp::default()
    };
    RULE.eval(&future).expect_err("future timestamp fails");
}