feat: add Azure Policy builtins with YAML test suite (#630)

* feat: add Azure Policy builtins with YAML test suite

Implement ARM template functions for Azure Policy evaluation:

Builtins:
- String: indexOf, lastIndexOf, trim, format, split, startsWith, endsWith,
  padLeft, concat, replace, toLower, toUpper, substring, guid, uniqueString
- DateTime: dateTimeAdd, dateTimeFromEpoch, dateTimeToEpoch, addDays
- Collection: intersection, union, take, skip, first, last, min, max,
  range, items, tryGet, tryIndexFromEnd, empty, array, createObject
- Encoding: base64, base64ToString, base64ToJson, uri, uriComponent,
  uriComponentToString, dataUri, dataUriToString
- Numeric: int, float, intDiv, intMod
- Misc: json, join, bool, string, coalesce, if, getParameter, resolveField
- Logic: logicAll, logicAny

Key implementation details:
- Unicode case-insensitive search via ICU4X case folding with single-pass
  fold_with_char_map() for indexOf/lastIndexOf
- .NET composite formatting (System.String.Format) with alignment, standard
  and custom datetime format specifiers, numeric format specifiers
- DateTime round-trip preserves input shape (Z vs +00:00, T vs space,
  fractional seconds) when no explicit output format is supplied
- Zero-cost as_str() helper borrows directly from Value::String(Rc<str>)
- BTreeSet<&Value> in array union avoids redundant cloning

Test suite:
- 53 YAML test files exercising all builtins via direct BUILTINS registry
- Coverage for edge cases: empty inputs, Unicode, fractional seconds,
  invalid alignment, unknown format specifiers, RFC3339 offset shapes

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>

* fix: address PR review comments

- Fix percent_encode to only uppercase hex digits, not entire string
- Remove guid/uniqueString (unsupported); delete custom SHA-1 impl
- Replace unwrap_or(0) with proper error in format placeholder parsing
- Hoist CaseMapper into static CaseMapperBorrowed for zero per-call overhead
- Pre-allocate Vec in range() with_capacity
- Update bindings/ffi and bindings/ruby Cargo.lock
- Fix uri_component test expectations for correct case preservation

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>

* fix: address second round of PR review comments

- float(): return Undefined when as_f64() fails instead of leaking
  the original non-f64 representation
- createObject(): reject odd number of arguments with an error
  (ARM-template parity)
- format(): error on unknown numeric format specifiers instead of
  silently passing through (matches .NET FormatException behavior)
- format(): cap alignment width at 10,000 to prevent DoS from
  user-controlled format strings like {0,1000000000}
- Add YAML test cases for all new error behaviors

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>

* fix: address third round of PR review comments

- percent_decode: reject incomplete % escapes (e.g. "%", "%2") instead
  of treating them as literal characters
- parse_iso8601_duration: reject leftover digits without a unit designator
  at T boundary and end-of-input (e.g. "P1", "P1T2H")
- yaml_to_value: panic on unsupported YAML numeric representations instead
  of silently mapping to Null
- Revert unused src/languages/mod.rs changes (module is defined inline in
  lib.rs)

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>

* fix: add missing edge-case tests and fix empty-delimiter panic

- fn_split: return input as single-element array for empty string
  delimiter instead of panicking (Rust's str::split("") panics)
- format: add test for F3 higher precision ({0:F3} + 1.23456 → 1.235)
- format: add test for N2 float with thousands separator
- format: add test for negative index error ({-1})
- split: add test for empty-string delimiter
- uri: add tests for query string and fragment in relative URI
- createObject: add test for non-string (numeric) keys

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>

* fix: address fourth round of PR review comments

- Add MAX_VARIADIC_ARGS (64) constant for variadic builtin arity
  instead of registering with 0 (logic_all, logic_any, min, max,
  format, intersection, union, coalesce, createObject); set
  dateTimeAdd to exact arity 3

- Switch indexOf/lastIndexOf to UTF-16 code-unit indices to match
  .NET String.IndexOf semantics (track ch.len_utf16() in
  fold_with_char_map, use encode_utf16().count() for empty-needle
  lastIndexOf)

- Use DateTime::<Utc>::from_timestamp for explicit timezone type

- Remove stale docs/azure-policy/casing.md link from module doc

- Fix misleading comment in want_error test branch (code bails on
  Undefined, not accepts it)

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>

---------

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>
This commit is contained in:
Anand Krishnamoorthi
2026-03-25 17:32:39 -05:00
committed by GitHub
parent f69974dc1b
commit 5b60daabd9
73 changed files with 5894 additions and 1 deletions
@@ -0,0 +1,46 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
# Tests for azure.policy.fn.add_days
builtin: azure.policy.fn.add_days
cases:
- note: add_one_day
args: ["2024-01-15T12:00:00Z", 1]
want: "2024-01-16T12:00:00Z"
- note: add_zero_days
args: ["2024-01-15T12:00:00Z", 0]
want: "2024-01-15T12:00:00Z"
- note: subtract_one_day
args: ["2024-01-15T12:00:00Z", -1]
want: "2024-01-14T12:00:00Z"
- note: add_thirty_days
args: ["2024-01-01T00:00:00Z", 30]
want: "2024-01-31T00:00:00Z"
- note: add_days_across_month
args: ["2024-01-31T00:00:00Z", 1]
want: "2024-02-01T00:00:00Z"
- note: add_days_leap_year
args: ["2024-02-28T00:00:00Z", 1]
want: "2024-02-29T00:00:00Z"
- note: add_days_non_leap_year
args: ["2023-02-28T00:00:00Z", 1]
want: "2023-03-01T00:00:00Z"
- note: invalid_base_datetime
args: ["not-a-date", 5]
want_undefined: true
- note: non_integer_days
args: ["2024-01-15T12:00:00Z", "abc"]
want_undefined: true
- note: add_365_days
args: ["2024-01-01T00:00:00Z", 365]
want: "2024-12-31T00:00:00Z"
@@ -0,0 +1,34 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
# Tests for azure.policy.fn.array
builtin: azure.policy.fn.array
cases:
- note: array_from_int
args: [42]
want: [42]
- note: array_from_string
args: ["hello"]
want: ["hello"]
- note: array_from_bool
args: [true]
want: [true]
- note: array_from_null
args: [null]
want: [null]
- note: array_from_array
args: [[1, 2, 3]]
want: [1, 2, 3]
- note: array_from_empty_array
args: [[]]
want: []
- note: array_from_object
args: [{"a": 1}]
want: [{"a": 1}]
@@ -0,0 +1,38 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
# Tests for azure.policy.fn.base64 and azure.policy.fn.base64_to_string
builtin: azure.policy.fn.base64
cases:
- note: base64_encode_hello
args: ["hello"]
want: "aGVsbG8="
- note: base64_encode_empty
args: [""]
want: ""
- note: base64_encode_single_char
args: ["a"]
want: "YQ=="
- note: base64_encode_two_chars
args: ["ab"]
want: "YWI="
- note: base64_encode_three_chars
args: ["abc"]
want: "YWJj"
- note: base64_encode_json_object
args: ["{\"key\":\"value\"}"]
want: "eyJrZXkiOiJ2YWx1ZSJ9"
- note: base64_encode_numbers
args: ["12345"]
want: "MTIzNDU="
- note: base64_encode_special_chars
args: ["hello world!"]
want: "aGVsbG8gd29ybGQh"
@@ -0,0 +1,39 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
# Tests for azure.policy.fn.base64_to_json
builtin: azure.policy.fn.base64_to_json
cases:
- note: base64_to_json_object
args: ["eyJrZXkiOiJ2YWx1ZSJ9"]
want:
key: "value"
- note: base64_to_json_array
args: ["WzEsMiwzXQ=="]
want: [1, 2, 3]
- note: base64_to_json_string
args: ["ImhlbGxvIg=="]
want: "hello"
- note: base64_to_json_number
args: ["NDI="]
want: 42
- note: base64_to_json_null
args: ["bnVsbA=="]
want_null: true
- note: base64_to_json_bool
args: ["dHJ1ZQ=="]
want: true
- note: base64_to_json_invalid_base64
args: ["!!!"]
want_undefined: true
- note: base64_to_json_invalid_json
args: ["bm90LWpzb24="]
want_undefined: true
@@ -0,0 +1,30 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
# Tests for azure.policy.fn.base64_to_string
builtin: azure.policy.fn.base64_to_string
cases:
- note: base64_decode_hello
args: ["aGVsbG8="]
want: "hello"
- note: base64_decode_empty
args: [""]
want: ""
- note: base64_decode_single_char
args: ["YQ=="]
want: "a"
- note: base64_decode_no_padding
args: ["YWJj"]
want: "abc"
- note: base64_decode_json
args: ["eyJrZXkiOiJ2YWx1ZSJ9"]
want: "{\"key\":\"value\"}"
- note: base64_decode_invalid
args: ["!!!"]
want_undefined: true
@@ -0,0 +1,70 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
# Tests for azure.policy.fn.bool
builtin: azure.policy.fn.bool
cases:
# ── From booleans ───────────────────────────────────────────────────
- note: bool_from_true
args: [true]
want: true
- note: bool_from_false
args: [false]
want: false
# ── From strings ────────────────────────────────────────────────────
- note: bool_from_string_true
args: ["true"]
want: true
- note: bool_from_string_false
args: ["false"]
want: false
- note: bool_from_string_TRUE
args: ["TRUE"]
want: true
- note: bool_from_string_False
args: ["False"]
want: false
- note: bool_from_string_1
args: ["1"]
want: true
- note: bool_from_string_0
args: ["0"]
want: false
- note: bool_from_string_invalid
args: ["yes"]
want_undefined: true
- note: bool_from_string_empty
args: [""]
want_undefined: true
# ── From numbers ────────────────────────────────────────────────────
- note: bool_from_number_1
args: [1]
want: true
- note: bool_from_number_0
args: [0]
want: false
- note: bool_from_number_negative
args: [-1]
want: true
- note: bool_from_number_42
args: [42]
want: true
# ── Other types ─────────────────────────────────────────────────────
- note: bool_from_null
args: [null]
want_undefined: true
@@ -0,0 +1,42 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
# Tests for azure.policy.fn.coalesce
builtin: azure.policy.fn.coalesce
cases:
- note: coalesce_first_non_null
args: [null, null, "hello"]
want: "hello"
- note: coalesce_first_is_value
args: ["first", "second"]
want: "first"
- note: coalesce_all_null
args: [null, null, null]
want_null: true
- note: coalesce_number
args: [null, 42]
want: 42
- note: coalesce_false_is_not_null
args: [null, false, "hello"]
want: false
- note: coalesce_zero_is_not_null
args: [null, 0, "hello"]
want: 0
- note: coalesce_empty_string_is_not_null
args: [null, "", "hello"]
want: ""
- note: coalesce_single_value
args: [42]
want: 42
- note: coalesce_single_null
args: [null]
want_null: true
@@ -0,0 +1,34 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
# Tests for azure.policy.fn.create_object
builtin: azure.policy.fn.create_object
cases:
- note: create_object_single_pair
args: ["name", "Alice"]
want: {"name": "Alice"}
- note: create_object_two_pairs
args: ["name", "Alice", "age", 30]
want: {"age": 30, "name": "Alice"}
- note: create_object_mixed_types
args: ["flag", true, "count", 5, "label", "test"]
want: {"count": 5, "flag": true, "label": "test"}
- note: create_object_empty
args: []
want: {}
- note: create_object_nested_value
args: ["data", {"inner": "value"}]
want: {"data": {"inner": "value"}}
- note: create_object_odd_args_error
args: ["key1", "val1", "key2"]
want_error: "expected an even number of arguments"
- note: create_object_numeric_key
args: [1, "one", 2, "two"]
want: {1: "one", 2: "two"}
@@ -0,0 +1,18 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
# Tests for azure.policy.fn.data_uri and azure.policy.fn.data_uri_to_string
builtin: azure.policy.fn.data_uri
cases:
- note: data_uri_hello
args: ["hello"]
want: "data:text/plain;charset=utf8;base64,aGVsbG8="
- note: data_uri_empty
args: [""]
want: "data:text/plain;charset=utf8;base64,"
- note: data_uri_json
args: ["{\"key\":\"value\"}"]
want: "data:text/plain;charset=utf8;base64,eyJrZXkiOiJ2YWx1ZSJ9"
@@ -0,0 +1,18 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
# Tests for azure.policy.fn.data_uri_to_string
builtin: azure.policy.fn.data_uri_to_string
cases:
- note: data_uri_to_string_hello
args: ["data:text/plain;charset=utf8;base64,aGVsbG8="]
want: "hello"
- note: data_uri_to_string_json
args: ["data:text/plain;charset=utf8;base64,eyJrZXkiOiJ2YWx1ZSJ9"]
want: "{\"key\":\"value\"}"
- note: data_uri_to_string_invalid
args: ["not-a-data-uri"]
want_undefined: true
@@ -0,0 +1,161 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
# Tests for azure.policy.fn.date_time_add
builtin: azure.policy.fn.date_time_add
cases:
- note: add_one_day
args: ["2024-01-15T12:00:00Z", "P1D"]
want: "2024-01-16T12:00:00Z"
- note: add_one_hour
args: ["2024-01-15T12:00:00Z", "PT1H"]
want: "2024-01-15T13:00:00Z"
- note: add_thirty_minutes
args: ["2024-01-15T12:00:00Z", "PT30M"]
want: "2024-01-15T12:30:00Z"
- note: add_negative_one_day
args: ["2024-01-15T12:00:00Z", "-P1D"]
want: "2024-01-14T12:00:00Z"
- note: add_complex_duration
args: ["2024-01-15T12:00:00Z", "P1DT2H30M"]
want: "2024-01-16T14:30:00Z"
- note: add_one_year_approx
args: ["2024-01-01T00:00:00Z", "P1Y"]
want: "2024-12-31T00:00:00Z"
- note: add_zero_duration
args: ["2024-06-15T10:30:00Z", "PT0S"]
want: "2024-06-15T10:30:00Z"
- note: invalid_base_datetime
args: ["not-a-date", "P1D"]
want_undefined: true
- note: invalid_duration
args: ["2024-01-15T12:00:00Z", "invalid"]
want_undefined: true
- note: datetime_without_timezone
args: ["2024-01-15T12:00:00", "P1D"]
want: "2024-01-16T12:00:00"
- note: add_one_week
args: ["2024-01-15T00:00:00Z", "P1W"]
want: "2024-01-22T00:00:00Z"
- note: space_separated_datetime
args: ["2020-04-07 14:55:59", "P3D"]
want: "2020-04-10 14:55:59"
- note: space_separated_datetime_with_z
args: ["2020-04-07 14:55:59Z", "P3D"]
want: "2020-04-10 14:55:59Z"
- note: space_separated_datetime_with_offset
args: ["2020-04-07 14:55:59+05:30", "P1D"]
want: "2020-04-08 14:55:59+05:30"
- note: output_format_date_only
args: ["2020-04-07T14:55:59Z", "P3Y2M", "yyyy-MM-dd"]
want: "2023-06-06"
- note: output_format_custom
args: ["2024-01-15T12:00:00Z", "PT1H", "yyyy-MM-dd HH:mm:ss"]
want: "2024-01-15 13:00:00"
- note: output_format_with_k_utc
args: ["2024-01-15T12:00:00Z", "P0D", "yyyy-MM-ddTHH:mm:ssK"]
want: "2024-01-15T12:00:00Z"
# Standard .NET format specifiers
- note: output_standard_format_d
args: ["2024-01-15T12:30:00Z", "P1D", "d"]
want: "01/16/2024"
- note: output_standard_format_G
args: ["2024-01-15T12:30:00Z", "PT1H", "G"]
want: "01/15/2024 13:30:00"
- note: output_standard_format_o
args: ["2024-01-15T12:00:00Z", "P0D", "o"]
want: "2024-01-15T12:00:00.0000000Z"
- note: output_standard_format_o_with_nanos
args: ["2024-01-15T12:00:00.123456700Z", "P0D", "o"]
want: "2024-01-15T12:00:00.1234567Z"
- note: output_standard_format_u
args: ["2024-01-15T12:00:00Z", "P0D", "u"]
want: "2024-01-15 12:00:00Z"
- note: output_standard_format_u_with_offset
args: ["2024-01-15T12:00:00+05:30", "P0D", "u"]
want: "2024-01-15 06:30:00Z"
# Fractional seconds preservation (default round-trip, no explicit format)
- note: roundtrip_iso_no_tz_with_frac
args: ["2024-01-15T12:00:00.123", "P0D"]
want: "2024-01-15T12:00:00.123"
- note: roundtrip_iso_no_tz_with_frac_add
args: ["2024-01-15T12:00:00.500", "P1D"]
want: "2024-01-16T12:00:00.500"
- note: roundtrip_space_no_tz_with_frac
args: ["2024-01-15 12:00:00.456", "P0D"]
want: "2024-01-15 12:00:00.456"
- note: roundtrip_space_z_with_frac
args: ["2024-01-15 12:00:00.789Z", "P0D"]
want: "2024-01-15 12:00:00.789Z"
- note: roundtrip_space_offset_with_frac
args: ["2024-01-15 12:00:00.123+05:30", "P0D"]
want: "2024-01-15 12:00:00.123+05:30"
- note: roundtrip_no_frac_stays_clean
args: ["2024-01-15T12:00:00", "P0D"]
want: "2024-01-15T12:00:00"
- note: roundtrip_rfc3339_utc_with_frac
args: ["2024-01-15T12:00:00.123Z", "P0D"]
want: "2024-01-15T12:00:00.123Z"
- note: roundtrip_rfc3339_offset_with_frac
args: ["2024-01-15T12:00:00.456+05:30", "P0D"]
want: "2024-01-15T12:00:00.456+05:30"
- note: roundtrip_rfc3339_utc_no_frac_stays_clean
args: ["2024-01-15T12:00:00Z", "P0D"]
want: "2024-01-15T12:00:00Z"
- note: roundtrip_rfc3339_explicit_zero_offset
args: ["2024-01-15T12:00:00+00:00", "P0D"]
want: "2024-01-15T12:00:00+00:00"
- note: roundtrip_rfc3339_explicit_zero_offset_with_frac
args: ["2024-01-15T12:00:00.500+00:00", "P0D"]
want: "2024-01-15T12:00:00.500+00:00"
# Unknown single-char format specifier is a real error
- note: unknown_format_specifier_q
args: ["2024-01-15T12:00:00Z", "P0D", "q"]
want_error: "unrecognised standard format specifier 'q'"
- note: output_standard_format_U
args: ["2024-01-15T12:00:00+05:30", "P0D", "U"]
want: "Monday, 15 January 2024 06:30:00"
- note: output_standard_format_s
args: ["2024-01-15T12:00:00Z", "P0D", "s"]
want: "2024-01-15T12:00:00"
- note: output_standard_format_R
args: ["2024-01-15T12:00:00+05:30", "P0D", "R"]
want: "Mon, 15 Jan 2024 06:30:00 GMT"
@@ -0,0 +1,30 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
# Tests for azure.policy.fn.date_time_from_epoch
builtin: azure.policy.fn.date_time_from_epoch
cases:
- note: unix_epoch_zero
args: [0]
want: "1970-01-01T00:00:00Z"
- note: specific_timestamp
args: [1705320000]
want: "2024-01-15T12:00:00Z"
- note: negative_timestamp
args: [-86400]
want: "1969-12-31T00:00:00Z"
- note: max_reasonable_timestamp
args: [4102444800]
want: "2100-01-01T00:00:00Z"
- note: one_second_after_epoch
args: [1]
want: "1970-01-01T00:00:01Z"
- note: non_number_arg
args: ["hello"]
want_undefined: true
@@ -0,0 +1,46 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
# Tests for azure.policy.fn.date_time_to_epoch
builtin: azure.policy.fn.date_time_to_epoch
cases:
- note: epoch_zero
args: ["1970-01-01T00:00:00Z"]
want: 0
- note: specific_datetime
args: ["2024-01-15T12:00:00Z"]
want: 1705320000
- note: datetime_with_offset
args: ["2024-01-15T13:00:00+01:00"]
want: 1705320000
- note: before_epoch
args: ["1969-12-31T00:00:00Z"]
want: -86400
- note: invalid_datetime
args: ["not-a-date"]
want_undefined: true
- note: non_string_arg
args: [12345]
want_undefined: true
- note: datetime_without_tz
args: ["2024-01-15T12:00:00"]
want: 1705320000
- note: space_separated_datetime
args: ["2024-01-15 12:00:00"]
want: 1705320000
- note: space_separated_datetime_with_z
args: ["2024-01-15 12:00:00Z"]
want: 1705320000
- note: space_separated_datetime_with_offset
args: ["2024-01-15 13:00:00+01:00"]
want: 1705320000
@@ -0,0 +1,55 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
# Tests for azure.policy.fn.empty
builtin: azure.policy.fn.empty
cases:
# ── Strings ─────────────────────────────────────────────────────────
- note: empty_string_true
args: [""]
want: true
- note: empty_string_false
args: ["hello"]
want: false
- note: empty_string_space
args: [" "]
want: false
# ── Arrays ──────────────────────────────────────────────────────────
- note: empty_array_true
args: [[]]
want: true
- note: empty_array_false
args: [[1, 2]]
want: false
# ── Objects ─────────────────────────────────────────────────────────
- note: empty_object_true
args: [{}]
want: true
- note: empty_object_false
args: [{"a": 1}]
want: false
# ── Null / special values ───────────────────────────────────────────
- note: empty_null
args: [null]
want: true
# ── Numbers and bools (not empty) ──────────────────────────────────
- note: empty_number
args: [0]
want: false
- note: empty_bool_false
args: [false]
want: false
- note: empty_bool_true
args: [true]
want: false
@@ -0,0 +1,38 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
# Tests for azure.policy.fn.ends_with
builtin: azure.policy.fn.ends_with
cases:
- note: ends_with_true
args: ["hello world", "world"]
want: true
- note: ends_with_false
args: ["hello world", "hello"]
want: false
- note: ends_with_case_insensitive
args: ["Hello World", "WORLD"]
want: true
- note: ends_with_empty_needle
args: ["hello", ""]
want: true
- note: ends_with_empty_haystack
args: ["", "hello"]
want: false
- note: ends_with_both_empty
args: ["", ""]
want: true
- note: ends_with_exact_match
args: ["hello", "hello"]
want: true
- note: ends_with_longer_needle
args: ["hi", "hello"]
want: false
@@ -0,0 +1,44 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
# Tests for azure.policy.fn.first
builtin: azure.policy.fn.first
cases:
# ── Arrays ──────────────────────────────────────────────────────────
- note: first_array_single
args: [[42]]
want: 42
- note: first_array_multiple
args: [[10, 20, 30]]
want: 10
- note: first_array_strings
args: [["alpha", "beta", "gamma"]]
want: "alpha"
- note: first_array_mixed
args: [[true, 1, "x"]]
want: true
- note: first_array_empty
args: [[]]
want_null: true
# ── Strings ─────────────────────────────────────────────────────────
- note: first_string
args: ["hello"]
want: "h"
- note: first_string_single_char
args: ["x"]
want: "x"
- note: first_string_empty
args: [""]
want: ""
- note: first_string_unicode
args: ["über"]
want: "ü"
@@ -0,0 +1,38 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
# Tests for azure.policy.fn.float
builtin: azure.policy.fn.float
cases:
- note: float_from_int
args: [42]
want: 42.0
- note: float_from_float
args: [3.14]
want: 3.14
- note: float_from_zero
args: [0]
want: 0.0
- note: float_from_negative
args: [-5]
want: -5.0
- note: float_from_string
args: ["3.14"]
want: 3.14
- note: float_from_string_int
args: ["42"]
want: 42.0
- note: float_from_string_invalid
args: ["hello"]
want_undefined: true
- note: float_from_null
args: [null]
want_undefined: true
@@ -0,0 +1,131 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
# Tests for azure.policy.fn.format
builtin: azure.policy.fn.format
cases:
- note: format_single_placeholder
args: ["Hello {0}!", "world"]
want: "Hello world!"
- note: format_multiple_placeholders
args: ["{0} is {1}", "sky", "blue"]
want: "sky is blue"
- note: format_repeated_placeholder
args: ["{0} and {0}", "hello"]
want: "hello and hello"
- note: format_number_arg
args: ["Count: {0}", 42]
want: "Count: 42"
- note: format_bool_arg
args: ["Value: {0}", true]
want: "Value: True"
- note: format_no_placeholders
args: ["no placeholders"]
want: "no placeholders"
- note: format_three_args
args: ["{0}-{1}-{2}", "a", "b", "c"]
want: "a-b-c"
- note: format_missing_placeholder_error
args: ["{0} and {1}", "only_first"]
want_error: "placeholder {1} references argument index 1"
- note: format_empty_template
args: [""]
want: ""
- note: format_escaped_braces
args: ["{{literal braces}}"]
want: "{literal braces}"
- note: format_mixed_escaped_and_placeholder
args: ["{{{0}}}", "value"]
want: "{value}"
- note: format_alignment_right
args: ["{0,10}", "hi"]
want: " hi"
- note: format_alignment_left
args: ["{0,-10}", "hi"]
want: "hi "
- note: format_numeric_fixed_point
args: ["{0:F2}", 3.14159]
want: "3.14"
- note: format_numeric_with_thousands
args: ["{0:N0}", 1234567]
want: "1,234,567"
- note: format_float_with_thousands
args: ["{0:N2}", 1234.5678]
want: "1,234.57"
- note: format_hex_upper
args: ["{0:X}", 255]
want: "FF"
- note: format_hex_lower_padded
args: ["{0:x4}", 255]
want: "00ff"
- note: format_decimal_padded
args: ["{0:D5}", 42]
want: "00042"
- note: format_percent
args: ["{0:P1}", 0.1234]
want: "12.3 %"
- note: format_unmatched_closing_brace
args: ["hello } world"]
want_error: "unmatched closing brace"
- note: format_invalid_placeholder_no_index
args: ["{abc}"]
want_error: "invalid placeholder"
- note: format_unmatched_opening_brace
args: ["{0", "value"]
want_error: "unmatched opening brace"
- note: format_alignment_non_ascii
args: ["{0,6}", "café"]
want: " café"
# Invalid alignment clauses (must be rejected)
- note: format_alignment_empty
args: ["{0,}", "hi"]
want_error: "empty alignment value"
- note: format_alignment_non_numeric
args: ["{0,abc}", "hi"]
want_error: "invalid alignment"
- note: format_alignment_trailing_junk
args: ["{0, 1x}", "hi"]
want_error: "invalid alignment"
- note: format_unknown_numeric_specifier_error
args: ["{0:Z}", 42]
want_error: "invalid numeric format specifier"
- note: format_alignment_width_exceeds_max
args: ["{0,1000000000}", "hi"]
want_error: "exceeds maximum allowed"
- note: format_fixed_point_higher_precision
args: ["{0:F3}", 1.23456]
want: "1.235"
- note: format_negative_index_error
args: ["{-1}", "value"]
want_error: "invalid placeholder"
@@ -0,0 +1,46 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
# Tests for azure.policy.get_parameter
builtin: azure.policy.get_parameter
cases:
- note: param_from_user_params
args:
- allowedLocations: ["eastus", "westus"]
- allowedLocations: ["centralus"]
- "allowedLocations"
want: ["eastus", "westus"]
- note: param_falls_back_to_defaults
args:
- {}
- effect: "Deny"
- "effect"
want: "Deny"
- note: param_not_in_either
args:
- {}
- {}
- "missing"
want_undefined: true
- note: param_override_default
args:
- maxAge: 90
- maxAge: 365
- "maxAge"
want: 90
- note: param_with_nested_value
args:
- config:
enabled: true
threshold: 50
- config:
enabled: false
- "config"
want:
enabled: true
threshold: 50
+38
View File
@@ -0,0 +1,38 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
# Tests for azure.policy.if (ARM template if() function)
builtin: azure.policy.if
cases:
- note: condition_true_returns_when_true
args: [true, "yes", "no"]
want: "yes"
- note: condition_false_returns_when_false
args: [false, "yes", "no"]
want: "no"
- note: true_with_numbers
args: [true, 1, 0]
want: 1
- note: false_with_numbers
args: [false, 1, 0]
want: 0
- note: true_with_arrays
args: [true, [1, 2], [3, 4]]
want: [1, 2]
- note: false_with_arrays
args: [false, [1, 2], [3, 4]]
want: [3, 4]
- note: true_with_null_branches
args: [true, null, "fallback"]
want_null: true
- note: false_with_null_branch
args: [false, "value", null]
want_null: true
@@ -0,0 +1,42 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
# Tests for azure.policy.fn.index_from_end
builtin: azure.policy.fn.index_from_end
cases:
- note: last_element
args: [["a", "b", "c", "d"], 1]
want: "d"
- note: second_from_end
args: [["a", "b", "c", "d"], 2]
want: "c"
- note: third_from_end
args: [["a", "b", "c", "d"], 3]
want: "b"
- note: first_element_via_end
args: [["a", "b", "c", "d"], 4]
want: "a"
- note: single_element_array
args: [["only"], 1]
want: "only"
- note: index_zero_error
args: [["a", "b"], 0]
want_error: "out of bounds"
- note: index_exceeds_length
args: [["a", "b"], 3]
want_error: "out of bounds"
- note: non_array_arg
args: ["not_an_array", 1]
want_undefined: true
- note: numeric_array
args: [[10, 20, 30], 1]
want: 30
@@ -0,0 +1,54 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
# Tests for azure.policy.fn.index_of
builtin: azure.policy.fn.index_of
cases:
- note: index_of_found
args: ["hello world", "world"]
want: 6
- note: index_of_beginning
args: ["hello", "hello"]
want: 0
- note: index_of_not_found
args: ["hello", "xyz"]
want: -1
- note: index_of_empty_needle
args: ["hello", ""]
want: 0
- note: index_of_empty_haystack
args: ["", "hello"]
want: -1
- note: index_of_both_empty
args: ["", ""]
want: 0
- note: index_of_single_char
args: ["abcdef", "d"]
want: 3
- note: index_of_first_occurrence
args: ["abcabc", "bc"]
want: 1
- note: index_of_case_insensitive
args: ["Hello", "hello"]
want: 0
- note: index_of_case_insensitive_mixed
args: ["Hello World", "WORLD"]
want: 6
- note: index_of_unicode_char_index
args: ["café latte", "latte"]
want: 5
- note: index_of_unicode_case_fold
args: ["Straße", "STRASSE"]
want: 0
@@ -0,0 +1,57 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
# Tests for azure.policy.fn.int
builtin: azure.policy.fn.int
cases:
# ── From numbers ────────────────────────────────────────────────────
- note: int_from_integer
args: [42]
want: 42
- note: int_from_negative
args: [-7]
want: -7
- note: int_from_zero
args: [0]
want: 0
- note: int_from_float_truncates
args: [3.9]
want: 3
- note: int_from_negative_float
args: [-2.7]
want: -2
# ── From strings ────────────────────────────────────────────────────
- note: int_from_string_integer
args: ["42"]
want: 42
- note: int_from_string_negative
args: ["-10"]
want: -10
- note: int_from_string_float
args: ["3.14"]
want: 3
- note: int_from_string_invalid
args: ["hello"]
want_undefined: true
- note: int_from_string_empty
args: [""]
want_undefined: true
# ── Other types ────────────────────────────────────────────────────
- note: int_from_bool
args: [true]
want_undefined: true
- note: int_from_null
args: [null]
want_undefined: true
@@ -0,0 +1,38 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
# Tests for azure.policy.fn.int_div
builtin: azure.policy.fn.int_div
cases:
- note: int_div_basic
args: [10, 3]
want: 3
- note: int_div_exact
args: [10, 5]
want: 2
- note: int_div_one
args: [7, 1]
want: 7
- note: int_div_negative
args: [-10, 3]
want: -3
- note: int_div_both_negative
args: [-10, -3]
want: 3
- note: int_div_zero_numerator
args: [0, 5]
want: 0
- note: int_div_by_zero
args: [10, 0]
want_undefined: true
- note: int_div_large
args: [1000000, 7]
want: 142857
@@ -0,0 +1,38 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
# Tests for azure.policy.fn.int_mod
builtin: azure.policy.fn.int_mod
cases:
- note: int_mod_basic
args: [10, 3]
want: 1
- note: int_mod_exact
args: [10, 5]
want: 0
- note: int_mod_one
args: [7, 1]
want: 0
- note: int_mod_negative
args: [-10, 3]
want: -1
- note: int_mod_both_negative
args: [-10, -3]
want: -1
- note: int_mod_zero_numerator
args: [0, 5]
want: 0
- note: int_mod_by_zero
args: [10, 0]
want_undefined: true
- note: int_mod_large
args: [1000000, 7]
want: 1
@@ -0,0 +1,58 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
# Tests for azure.policy.fn.intersection
builtin: azure.policy.fn.intersection
cases:
# ── Array intersection ──────────────────────────────────────────────
- note: intersection_arrays_overlap
args: [[1, 2, 3], [2, 3, 4]]
want: [2, 3]
- note: intersection_arrays_no_overlap
args: [[1, 2], [3, 4]]
want: []
- note: intersection_arrays_identical
args: [[1, 2, 3], [1, 2, 3]]
want: [1, 2, 3]
- note: intersection_arrays_empty_first
args: [[], [1, 2]]
want: []
- note: intersection_arrays_empty_second
args: [[1, 2], []]
want: []
- note: intersection_arrays_three
args: [[1, 2, 3, 4], [2, 3, 4, 5], [3, 4, 5, 6]]
want: [3, 4]
- note: intersection_arrays_strings
args: [["a", "b", "c"], ["b", "c", "d"]]
want: ["b", "c"]
# ── Object intersection ─────────────────────────────────────────────
# Azure semantics: a key is kept only when it exists in ALL objects
# AND the value is the same across all of them.
- note: intersection_objects_same_values
args: [{"a": 1, "b": 2, "c": 3}, {"b": 2, "c": 3, "d": 4}]
want: {"b": 2, "c": 3}
- note: intersection_objects_different_values
args: [{"a": 1, "b": 2, "c": 3}, {"b": 20, "c": 30, "d": 40}]
want: {}
- note: intersection_objects_no_overlap
args: [{"a": 1}, {"b": 2}]
want: {}
- note: intersection_objects_identical
args: [{"x": 1}, {"x": 1}]
want: {"x": 1}
- note: intersection_objects_partial_value_match
args: [{"a": 1, "b": 2}, {"a": 1, "b": 99}]
want: {"a": 1}
@@ -0,0 +1,65 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
# Tests for azure.policy.fn.ip_range_contains
builtin: azure.policy.fn.ip_range_contains
cases:
# ── IPv4 ────────────────────────────────────────────────────────────
- note: ipv4_contains_ip_true
args: ["10.0.0.0/8", "10.1.2.3"]
want: true
- note: ipv4_contains_ip_false
args: ["10.0.0.0/8", "192.168.1.1"]
want: false
- note: ipv4_contains_subnet_true
args: ["10.0.0.0/8", "10.0.0.0/16"]
want: true
- note: ipv4_contains_subnet_false
args: ["10.0.0.0/16", "10.0.0.0/8"]
want: false
- note: ipv4_exact_match
args: ["192.168.1.0/24", "192.168.1.100"]
want: true
- note: ipv4_edge_network_address
args: ["192.168.1.0/24", "192.168.1.0"]
want: true
- note: ipv4_edge_broadcast
args: ["192.168.1.0/24", "192.168.1.255"]
want: true
- note: ipv4_just_outside
args: ["192.168.1.0/24", "192.168.2.0"]
want: false
- note: ipv4_slash_32
args: ["10.0.0.1/32", "10.0.0.1"]
want: true
- note: ipv4_slash_32_miss
args: ["10.0.0.1/32", "10.0.0.2"]
want: false
# ── IPv6 ────────────────────────────────────────────────────────────
- note: ipv6_contains_ip_true
args: ["fd00::/8", "fd12:3456::1"]
want: true
- note: ipv6_contains_ip_false
args: ["fd00::/8", "2001:db8::1"]
want: false
# ── Invalid inputs ──────────────────────────────────────────────────
- note: ip_invalid_range
args: ["not-a-cidr", "10.0.0.1"]
want: false
- note: ip_invalid_target
args: ["10.0.0.0/8", "not-an-ip"]
want: false
@@ -0,0 +1,40 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
# Tests for azure.policy.fn.items
builtin: azure.policy.fn.items
cases:
- note: items_simple_object
args:
- name: "test"
value: 42
want:
- key: "name"
value: "test"
- key: "value"
value: 42
- note: items_single_key
args:
- a: 1
want:
- key: "a"
value: 1
- note: items_nested_value
args:
- x:
inner: true
want:
- key: "x"
value:
inner: true
- note: items_non_object
args: ["not_an_object"]
want_undefined: true
- note: items_number_arg
args: [42]
want_undefined: true
@@ -0,0 +1,42 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
# Tests for azure.policy.fn.join
builtin: azure.policy.fn.join
cases:
- note: join_strings_comma
args: [["a", "b", "c"], ","]
want: "a,b,c"
- note: join_strings_space
args: [["hello", "world"], " "]
want: "hello world"
- note: join_single_element
args: [["only"], ","]
want: "only"
- note: join_empty_array
args: [[], ","]
want: ""
- note: join_empty_delimiter
args: [["a", "b", "c"], ""]
want: "abc"
- note: join_with_numbers
args: [[1, 2, 3], "-"]
want: "1-2-3"
- note: join_with_booleans
args: [[true, false], "|"]
want: "true|false"
- note: join_with_null
args: [["a", null, "b"], ","]
want: "a,null,b"
- note: join_multichar_delimiter
args: [["x", "y", "z"], " :: "]
want: "x :: y :: z"
@@ -0,0 +1,51 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
# Tests for azure.policy.fn.json
builtin: azure.policy.fn.json
cases:
- note: parse_object
args: ['{"name":"test","value":42}']
want:
name: test
value: 42
- note: parse_array
args: ['[1,2,3]']
want: [1, 2, 3]
- note: parse_string
args: ['"hello"']
want: "hello"
- note: parse_number
args: ["42"]
want: 42
- note: parse_boolean_true
args: ["true"]
want: true
- note: parse_boolean_false
args: ["false"]
want: false
- note: parse_null
args: ["null"]
want_null: true
- note: parse_nested_object
args: ['{"a":{"b":{"c":1}}}']
want:
a:
b:
c: 1
- note: invalid_json
args: ["{not valid json}"]
want_error: "json()"
- note: non_string_arg
args: [42]
want_undefined: true
@@ -0,0 +1,40 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
# Tests for azure.policy.fn.last
builtin: azure.policy.fn.last
cases:
# ── Arrays ──────────────────────────────────────────────────────────
- note: last_array_single
args: [[42]]
want: 42
- note: last_array_multiple
args: [[10, 20, 30]]
want: 30
- note: last_array_strings
args: [["alpha", "beta", "gamma"]]
want: "gamma"
- note: last_array_empty
args: [[]]
want_null: true
# ── Strings ─────────────────────────────────────────────────────────
- note: last_string
args: ["hello"]
want: "o"
- note: last_string_single_char
args: ["x"]
want: "x"
- note: last_string_empty
args: [""]
want: ""
- note: last_string_unicode
args: ["café"]
want: "é"
@@ -0,0 +1,34 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
# Tests for azure.policy.fn.last_index_of
builtin: azure.policy.fn.last_index_of
cases:
- note: last_index_of_found
args: ["hello world hello", "hello"]
want: 12
- note: last_index_of_single_occurrence
args: ["hello world", "world"]
want: 6
- note: last_index_of_not_found
args: ["hello", "xyz"]
want: -1
- note: last_index_of_empty_needle
args: ["hello", ""]
want: 5
- note: last_index_of_repeated
args: ["abcabcabc", "abc"]
want: 6
- note: last_index_of_single_char
args: ["abcabc", "c"]
want: 5
- note: last_index_of_case_insensitive
args: ["Hello HELLO hello", "hello"]
want: 12
@@ -0,0 +1,38 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
# Tests for azure.policy.logic_all (ARM template and() function)
builtin: azure.policy.logic_all
cases:
- note: all_true
args: [true, true, true]
want: true
- note: one_false
args: [true, false, true]
want: false
- note: all_false
args: [false, false, false]
want: false
- note: single_true
args: [true]
want: true
- note: single_false
args: [false]
want: false
- note: two_args_both_true
args: [true, true]
want: true
- note: two_args_one_false
args: [true, false]
want: false
- note: empty_args_vacuously_true
args: []
want: true
@@ -0,0 +1,34 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
# Tests for azure.policy.logic_any (ARM template or() function)
builtin: azure.policy.logic_any
cases:
- note: all_true
args: [true, true, true]
want: true
- note: one_true
args: [false, true, false]
want: true
- note: all_false
args: [false, false, false]
want: false
- note: single_true
args: [true]
want: true
- note: single_false
args: [false]
want: false
- note: two_args_one_true
args: [false, true]
want: true
- note: empty_args_vacuously_false
args: []
want: false
@@ -0,0 +1,42 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
# Tests for azure.policy.fn.max
builtin: azure.policy.fn.max
cases:
- note: max_two_args
args: [3, 1]
want: 3
- note: max_three_args
args: [5, 2, 8]
want: 8
- note: max_equal_args
args: [3, 3, 3]
want: 3
- note: max_negative
args: [-5, 0, 5]
want: 5
- note: max_single_arg
args: [42]
want: 42
- note: max_array
args: [[3, 1, 4, 1, 5]]
want: 5
- note: max_array_single
args: [[99]]
want: 99
- note: max_array_negatives
args: [[-10, -20, -5]]
want: -5
- note: max_array_empty
args: [[]]
want_undefined: true
@@ -0,0 +1,44 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
# Tests for azure.policy.fn.min
builtin: azure.policy.fn.min
cases:
# ── Multiple arguments ──────────────────────────────────────────────
- note: min_two_args
args: [3, 1]
want: 1
- note: min_three_args
args: [5, 2, 8]
want: 2
- note: min_equal_args
args: [3, 3, 3]
want: 3
- note: min_negative
args: [-5, 0, 5]
want: -5
- note: min_single_arg
args: [42]
want: 42
# ── Array argument ──────────────────────────────────────────────────
- note: min_array
args: [[3, 1, 4, 1, 5]]
want: 1
- note: min_array_single
args: [[99]]
want: 99
- note: min_array_negatives
args: [[-10, -20, -5]]
want: -20
- note: min_array_empty
args: [[]]
want_undefined: true
@@ -0,0 +1,38 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
# Tests for azure.policy.fn.pad_left
builtin: azure.policy.fn.pad_left
cases:
- note: pad_left_basic
args: ["42", 5, "0"]
want: "00042"
- note: pad_left_no_padding_needed
args: ["hello", 3, "x"]
want: "hello"
- note: pad_left_exact_width
args: ["abc", 3, "x"]
want: "abc"
- note: pad_left_default_space
args: ["42", 5, " "]
want: " 42"
- note: pad_left_single_char
args: ["x", 5, "-"]
want: "----x"
- note: pad_left_empty_string
args: ["", 3, "0"]
want: "000"
- note: pad_left_width_1
args: ["abc", 1, "0"]
want: "abc"
- note: pad_left_width_0
args: ["abc", 0, "0"]
want: "abc"
@@ -0,0 +1,38 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
# Tests for azure.policy.fn.range
builtin: azure.policy.fn.range
cases:
- note: range_basic
args: [0, 5]
want: [0, 1, 2, 3, 4]
- note: range_from_offset
args: [3, 4]
want: [3, 4, 5, 6]
- note: range_single
args: [0, 1]
want: [0]
- note: range_zero_count
args: [5, 0]
want: []
- note: range_negative_start
args: [-2, 4]
want: [-2, -1, 0, 1]
- note: range_large_start
args: [100, 3]
want: [100, 101, 102]
- note: range_count_exceeds_10000
args: [0, 10001]
want_error: "exceeds maximum of 10000"
- note: range_sum_exceeds_i32_max
args: [2147483640, 10]
want_error: "exceeds maximum of 2147483647"
@@ -0,0 +1,49 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
# Tests for azure.policy.resolve_field
builtin: azure.policy.resolve_field
cases:
- note: simple_field
args:
- name: "myResource"
type: "Microsoft.Compute/virtualMachines"
- "name"
want: "myResource"
- note: nested_field_with_dot_path
args:
- properties:
securityRules:
enabled: true
- "properties.securityRules.enabled"
want: true
- note: missing_field
args:
- name: "myResource"
- "nonExistent"
want_undefined: true
- note: deeply_nested
args:
- a:
b:
c:
d: "deep"
- "a.b.c.d"
want: "deep"
- note: top_level_type
args:
- type: "Microsoft.Storage/storageAccounts"
kind: "StorageV2"
- "type"
want: "Microsoft.Storage/storageAccounts"
- note: non_string_path
args:
- name: "test"
- 42
want_undefined: true
@@ -0,0 +1,48 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
# Tests for azure.policy.fn.skip
builtin: azure.policy.fn.skip
cases:
# ── Arrays ──────────────────────────────────────────────────────────
- note: skip_array_basic
args: [[1, 2, 3, 4, 5], 2]
want: [3, 4, 5]
- note: skip_array_zero
args: [[1, 2, 3], 0]
want: [1, 2, 3]
- note: skip_array_all
args: [[1, 2, 3], 3]
want: []
- note: skip_array_more_than_length
args: [[1, 2], 5]
want: []
- note: skip_array_one
args: [[10, 20, 30], 1]
want: [20, 30]
- note: skip_array_empty
args: [[], 3]
want: []
# ── Strings ─────────────────────────────────────────────────────────
- note: skip_string_basic
args: ["hello world", 6]
want: "world"
- note: skip_string_zero
args: ["hello", 0]
want: "hello"
- note: skip_string_all
args: ["hello", 5]
want: ""
- note: skip_string_more_than_length
args: ["hi", 10]
want: ""
@@ -0,0 +1,61 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
# Tests for azure.policy.fn.split
builtin: azure.policy.fn.split
cases:
# ── Basic splitting ─────────────────────────────────────────────────
- note: split_simple_comma
args: ["hello,world", ","]
want: ["hello", "world"]
- note: split_multiple_segments
args: ["a.b.c.d", "."]
want: ["a", "b", "c", "d"]
- note: split_no_match
args: ["hello", ","]
want: ["hello"]
- note: split_empty_string
args: ["", ","]
want: [""]
- note: split_delimiter_at_edges
args: [",hello,", ","]
want: ["", "hello", ""]
- note: split_consecutive_delimiters
args: ["a,,b", ","]
want: ["a", "", "b"]
- note: split_multi_char_delimiter
args: ["a::b::c", "::"]
want: ["a", "b", "c"]
- note: split_single_char_string
args: ["x", "x"]
want: ["", ""]
# ── Array of delimiters ────────────────────────────────────────────
- note: split_array_delimiters
args: ["a.b/c", [".", "/"]]
want: ["a", "b", "c"]
- note: split_array_single_delimiter
args: ["a-b-c", ["-"]]
want: ["a", "b", "c"]
# ── Edge cases ──────────────────────────────────────────────────────
- note: split_unicode
args: ["café☕bar", "☕"]
want: ["café", "bar"]
- note: split_entire_string_is_delimiter
args: ["abc", "abc"]
want: ["", ""]
- note: split_empty_delimiter
args: ["abc", ""]
want: ["abc"]
@@ -0,0 +1,38 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
# Tests for azure.policy.fn.starts_with
builtin: azure.policy.fn.starts_with
cases:
- note: starts_with_true
args: ["hello world", "hello"]
want: true
- note: starts_with_false
args: ["hello world", "world"]
want: false
- note: starts_with_case_insensitive
args: ["Hello World", "hello"]
want: true
- note: starts_with_empty_needle
args: ["hello", ""]
want: true
- note: starts_with_empty_haystack
args: ["", "hello"]
want: false
- note: starts_with_both_empty
args: ["", ""]
want: true
- note: starts_with_exact_match
args: ["hello", "hello"]
want: true
- note: starts_with_longer_needle
args: ["hi", "hello"]
want: false
@@ -0,0 +1,38 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
# Tests for azure.policy.fn.string
builtin: azure.policy.fn.string
cases:
- note: string_from_string
args: ["hello"]
want: "hello"
- note: string_from_integer
args: [42]
want: "42"
- note: string_from_negative
args: [-7]
want: "-7"
- note: string_from_bool_true
args: [true]
want: "true"
- note: string_from_bool_false
args: [false]
want: "false"
- note: string_from_null
args: [null]
want: "null"
- note: string_from_zero
args: [0]
want: "0"
- note: string_from_empty
args: [""]
want: ""
@@ -0,0 +1,48 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
# Tests for azure.policy.fn.take
builtin: azure.policy.fn.take
cases:
# ── Arrays ──────────────────────────────────────────────────────────
- note: take_array_basic
args: [[1, 2, 3, 4, 5], 3]
want: [1, 2, 3]
- note: take_array_zero
args: [[1, 2, 3], 0]
want: []
- note: take_array_more_than_length
args: [[1, 2], 5]
want: [1, 2]
- note: take_array_all
args: [[1, 2, 3], 3]
want: [1, 2, 3]
- note: take_array_one
args: [[10, 20, 30], 1]
want: [10]
- note: take_array_empty
args: [[], 3]
want: []
# ── Strings ─────────────────────────────────────────────────────────
- note: take_string_basic
args: ["hello world", 5]
want: "hello"
- note: take_string_zero
args: ["hello", 0]
want: ""
- note: take_string_more_than_length
args: ["hi", 10]
want: "hi"
- note: take_string_one
args: ["hello", 1]
want: "h"
@@ -0,0 +1,38 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
# Tests for azure.policy.fn.trim
builtin: azure.policy.fn.trim
cases:
- note: trim_whitespace
args: [" hello "]
want: "hello"
- note: trim_tabs_and_newlines
args: ["\thello\n"]
want: "hello"
- note: trim_no_whitespace
args: ["hello"]
want: "hello"
- note: trim_empty
args: [""]
want: ""
- note: trim_only_whitespace
args: [" "]
want: ""
- note: trim_left_only
args: [" hello"]
want: "hello"
- note: trim_right_only
args: ["hello "]
want: "hello"
- note: trim_inner_whitespace_preserved
args: [" hello world "]
want: "hello world"
@@ -0,0 +1,51 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
# Tests for azure.policy.fn.try_get
builtin: azure.policy.fn.try_get
cases:
- note: get_existing_key
args:
- name: "test"
value: 42
- "name"
want: "test"
- note: get_missing_key
args:
- name: "test"
- "missing"
want_null: true
- note: get_array_index
args: [["a", "b", "c"], 1]
want: "b"
- note: get_array_out_of_bounds
args: [["a", "b"], 5]
want_null: true
- note: get_from_non_container
args: ["not_an_object", "key"]
want_null: true
- note: get_null_item
args: [null, "key"]
want_null: true
- note: get_nested_value
args:
- data:
inner: true
- "data"
want:
inner: true
- note: get_number_item
args: [42, "key"]
want_null: true
- note: get_array_first
args: [[10, 20, 30], 0]
want: 10
@@ -0,0 +1,38 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
# Tests for azure.policy.fn.try_index_from_end
builtin: azure.policy.fn.try_index_from_end
cases:
- note: last_element
args: [["a", "b", "c", "d"], 1]
want: "d"
- note: second_from_end
args: [["a", "b", "c", "d"], 2]
want: "c"
- note: first_element_via_end
args: [["a", "b", "c", "d"], 4]
want: "a"
- note: index_zero_returns_null
args: [["a", "b"], 0]
want_null: true
- note: index_exceeds_length_returns_null
args: [["a", "b"], 5]
want_null: true
- note: non_array_returns_null
args: ["not_an_array", 1]
want_null: true
- note: single_element
args: [["only"], 1]
want: "only"
- note: numeric_array
args: [[10, 20, 30], 2]
want: 20
@@ -0,0 +1,89 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
# Tests for azure.policy.fn.union
builtin: azure.policy.fn.union
cases:
# ── Array union ─────────────────────────────────────────────────────
- note: union_arrays_basic
args: [[1, 2], [2, 3]]
want: [1, 2, 3]
- note: union_arrays_no_overlap
args: [[1, 2], [3, 4]]
want: [1, 2, 3, 4]
- note: union_arrays_identical
args: [[1, 2], [1, 2]]
want: [1, 2]
- note: union_arrays_empty
args: [[], [1, 2]]
want: [1, 2]
- note: union_arrays_both_empty
args: [[], []]
want: []
- note: union_arrays_three
args: [[1], [2], [3]]
want: [1, 2, 3]
- note: union_arrays_strings
args: [["a", "b"], ["b", "c"]]
want: ["a", "b", "c"]
# ── Object union ────────────────────────────────────────────────────
- note: union_objects_merge
args: [{"a": 1}, {"b": 2}]
want: {"a": 1, "b": 2}
- note: union_objects_overwrite
args: [{"a": 1, "b": 2}, {"b": 20, "c": 30}]
want: {"a": 1, "b": 20, "c": 30}
- note: union_objects_empty
args: [{}, {"a": 1}]
want: {"a": 1}
- note: union_objects_recursive_merge
args:
- outer:
inner_a: 1
inner_b: 2
- outer:
inner_b: 20
inner_c: 30
want:
outer:
inner_a: 1
inner_b: 20
inner_c: 30
- note: union_objects_deeply_nested_merge
args:
- level1:
level2:
a: 1
b: 2
- level1:
level2:
b: 20
c: 30
want:
level1:
level2:
a: 1
b: 20
c: 30
- note: union_objects_array_replaces_not_merges
args:
- data:
items: [1, 2, 3]
- data:
items: [4, 5]
want:
data:
items: [4, 5]
@@ -0,0 +1,54 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
# Tests for azure.policy.fn.uri
builtin: azure.policy.fn.uri
cases:
- note: base_with_trailing_slash
args: ["https://example.com/", "path/to/resource"]
want: "https://example.com/path/to/resource"
- note: base_without_trailing_slash
args: ["https://example.com/base", "relative"]
want: "https://example.com/relative"
- note: relative_is_absolute_url
args: ["https://example.com/base", "https://other.com/path"]
want: "https://other.com/path"
- note: relative_with_leading_slash
args: ["https://example.com/", "/path"]
want: "https://example.com/path"
- note: base_with_path_components
args: ["https://example.com/a/b/c", "d"]
want: "https://example.com/a/b/d"
- note: non_string_base
args: [42, "relative"]
want_undefined: true
- note: non_string_relative
args: ["https://example.com/", 42]
want_undefined: true
- note: simple_base
args: ["https://example.com/", "api/v1"]
want: "https://example.com/api/v1"
- note: authority_only_base
args: ["https://example.com", "api/v1"]
want: "https://example.com/api/v1"
- note: authority_only_base_with_leading_slash
args: ["https://example.com", "/api/v1"]
want: "https://example.com/api/v1"
- note: uri_relative_with_query_string
args: ["https://example.com/api", "v2?key=value"]
want: "https://example.com/v2?key=value"
- note: uri_relative_with_fragment
args: ["https://example.com/docs/", "page#section"]
want: "https://example.com/docs/page#section"
@@ -0,0 +1,30 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
# Tests for azure.policy.fn.uri_component and azure.policy.fn.uri_component_to_string
builtin: azure.policy.fn.uri_component
cases:
- note: uri_encode_simple
args: ["hello world"]
want: "hello%20world"
- note: uri_encode_special_chars
args: ["a=b&c=d"]
want: "a%3Db%26c%3Dd"
- note: uri_encode_already_safe
args: ["hello"]
want: "hello"
- note: uri_encode_empty
args: [""]
want: ""
- note: uri_encode_slash
args: ["path/to/resource"]
want: "path%2Fto%2Fresource"
- note: uri_encode_spaces_and_plus
args: ["hello+world 2"]
want: "hello%2Bworld%202"
@@ -0,0 +1,26 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
# Tests for azure.policy.fn.uri_component_to_string
builtin: azure.policy.fn.uri_component_to_string
cases:
- note: uri_decode_simple
args: ["hello%20world"]
want: "hello world"
- note: uri_decode_special_chars
args: ["a%3Db%26c%3Dd"]
want: "a=b&c=d"
- note: uri_decode_no_encoding
args: ["hello"]
want: "hello"
- note: uri_decode_empty
args: [""]
want: ""
- note: uri_decode_plus_literal
args: ["hello%2Bworld"]
want: "hello+world"
+215
View File
@@ -0,0 +1,215 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
//! YAML-based test runner for Azure Policy builtins.
//!
//! Each YAML file contains a `builtin` field naming the function under test
//! and a list of `cases`. Every case specifies `args` (positional arguments)
//! and either `want` (expected return value) or `want_undefined` (the builtin
//! should return `Value::Undefined`).
//!
//! Tests call builtins directly via the registry (`BUILTINS` map) instead of
//! going through Rego evaluation. This avoids issues with builtin names that
//! collide with Rego keywords (e.g. `azure.policy.if`).
use anyhow::{bail, Result};
use regorus::unstable::{Source, Span, BUILTINS};
use regorus::Value;
use serde::Deserialize;
use test_generator::test_resources;
// ── YAML schema ───────────────────────────────────────────────────────
#[derive(Debug, Deserialize)]
struct YamlTestFile {
/// Dotted builtin name, e.g. `azure.policy.fn.split`.
builtin: String,
cases: Vec<TestCase>,
}
#[derive(Debug, Deserialize)]
struct TestCase {
/// Short human-readable label.
note: String,
/// Positional arguments fed to the builtin.
args: Vec<serde_yaml::Value>,
/// Expected return value (`null` for JSON null).
want: Option<serde_yaml::Value>,
/// If true, the builtin is expected to return null.
/// (Needed because `want: null` in YAML deserializes as Option::None.)
#[serde(default)]
want_null: bool,
/// If true, the builtin is expected to produce Undefined (no result).
#[serde(default)]
want_undefined: bool,
/// If set, the builtin should return an error containing this substring.
want_error: Option<String>,
/// Skip this case.
#[serde(default)]
skip: bool,
}
// ── Helpers ───────────────────────────────────────────────────────────
/// Convert a serde_yaml::Value to a regorus Value.
fn yaml_to_value(v: &serde_yaml::Value) -> Value {
match v {
serde_yaml::Value::Null => Value::Null,
serde_yaml::Value::Bool(b) => Value::Bool(*b),
serde_yaml::Value::Number(n) => {
if let Some(i) = n.as_i64() {
Value::from(i)
} else if let Some(f) = n.as_f64() {
Value::from(f)
} else {
panic!("unsupported YAML numeric representation: {n:?}")
}
}
serde_yaml::Value::String(s) => Value::String(s.as_str().into()),
serde_yaml::Value::Sequence(items) => {
let vals: Vec<Value> = items.iter().map(yaml_to_value).collect();
Value::from(vals)
}
serde_yaml::Value::Mapping(map) => {
let mut obj = Value::new_object();
{
let m = obj.as_object_mut().unwrap();
for (k, v) in map {
m.insert(yaml_to_value(k), yaml_to_value(v));
}
}
obj
}
serde_yaml::Value::Tagged(t) => yaml_to_value(&t.value),
}
}
/// Create a dummy Span for calling builtins outside of normal evaluation.
fn dummy_span() -> Span {
let source = Source::from_contents("<test>".to_string(), String::new())
.expect("creating dummy source should not fail");
Span {
source,
line: 1,
col: 1,
start: 0,
end: 0,
}
}
// ── Test runner ───────────────────────────────────────────────────────
fn run_yaml_test(path: &str) -> Result<()> {
let content = std::fs::read_to_string(path)?;
let test_file: YamlTestFile = serde_yaml::from_str(&content)?;
let filter = std::env::var("TEST_CASE_FILTER").ok();
// Look up the builtin function once for all cases.
let builtin_entry = BUILTINS.get(test_file.builtin.as_str()).unwrap_or_else(|| {
panic!(
"builtin {:?} not found in BUILTINS registry",
test_file.builtin
)
});
let builtin_fn = builtin_entry.0;
let span = dummy_span();
for case in &test_file.cases {
if case.skip {
continue;
}
if let Some(ref f) = filter {
if !case.note.contains(f.as_str()) {
continue;
}
}
// Convert YAML args to Value args.
let args: Vec<Value> = case.args.iter().map(yaml_to_value).collect();
// Call the builtin directly.
let call_result = builtin_fn(&span, &[], &args, false);
// Check error expectations.
if let Some(ref want_err) = case.want_error {
match call_result {
Err(e) => {
let msg = format!("{e:#}");
assert!(
msg.contains(want_err.as_str()),
"[{builtin} / {note}] expected error containing {want_err:?}, got: {msg}",
builtin = test_file.builtin,
note = case.note,
);
}
Ok(ref v) if matches!(v, Value::Undefined) => {
// `want_error` specifically expects an error message string;
// Undefined is not acceptable here — bail.
bail!(
"[{builtin} / {note}] expected error containing {want_err:?} but got Undefined",
builtin = test_file.builtin,
note = case.note,
);
}
Ok(v) => {
bail!(
"[{builtin} / {note}] expected error containing {want_err:?} but got: {v}",
builtin = test_file.builtin,
note = case.note,
);
}
}
continue;
}
// Not expecting an error — unwrap the result.
let actual = call_result.map_err(|e| {
anyhow::anyhow!(
"[{builtin} / {note}] builtin returned error: {e:#}",
builtin = test_file.builtin,
note = case.note,
)
})?;
if case.want_undefined {
assert!(
actual == Value::Undefined,
"[{builtin} / {note}] expected Undefined but got: {actual}",
builtin = test_file.builtin,
note = case.note,
);
continue;
}
// We expect a concrete result.
let expected = if case.want_null {
Value::Null
} else {
let want = case.want.as_ref().unwrap_or_else(|| {
panic!(
"[{} / {}] test case must have `want`, `want_null`, or `want_undefined`",
test_file.builtin, case.note
)
});
yaml_to_value(want)
};
assert!(
actual == expected,
"[{builtin} / {note}]\n expected: {expected}\n actual: {actual}",
builtin = test_file.builtin,
note = case.note,
);
}
Ok(())
}
// ── Test entry point ──────────────────────────────────────────────────
#[test_resources("tests/azure_policy_builtins/cases/*.yaml")]
fn azure_policy_builtin_yaml(path: &str) {
run_yaml_test(path).unwrap();
}
+3
View File
@@ -1,6 +1,9 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
#[cfg(feature = "azure_policy")]
mod azure_policy_builtins;
#[cfg(feature = "coverage")]
mod coverage;