mirror of
https://github.com/microsoft/regorus.git
synced 2026-08-05 02:16:11 +00:00
feat(azure-policy): add alias normalization and denormalization (#635)
* feat: add Azure Policy alias normalization/denormalization Add normalizer and denormalizer for ARM JSON resources, enabling Azure Policy alias short names to become direct paths into a flat structure. - Normalizer: flattens properties wrappers, lowercases keys, resolves per-alias versioned ARM paths, handles sub-resource array flattening, element-level field remaps, and array base renames - Denormalizer: reverses all transformations with casing restoration - AliasRegistry: loads production alias catalogs and data policy manifests - Types: serde deserialization for ARM provider alias formats - YAML test suite: 13 test files covering normalize, denormalize, round-trip, data-plane, edge cases, malformed input, sub-resources, and registry API - Benchmark suite for normalization performance * feat: add FFI and C# bindings for alias normalization - FFI: alias_registry.rs with C-compatible API for loading catalogs, normalizing resources, and denormalizing back to ARM JSON - C#: AliasRegistry wrapper class with NativeMethods P/Invoke bindings and integration tests - Updated Cargo.lock files for new serde_json dependency
This commit is contained in:
committed by
GitHub
parent
35fb5d5953
commit
d36f952133
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,230 @@
|
||||
[
|
||||
{
|
||||
"namespace": "Microsoft.Test",
|
||||
"resourceTypes": [
|
||||
{
|
||||
"resourceType": "versionedResources",
|
||||
"aliases": [
|
||||
{
|
||||
"name": "Microsoft.Test/versionedResources/simpleProp",
|
||||
"defaultPath": "properties.simpleProp",
|
||||
"paths": []
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.Test/versionedResources/accountType",
|
||||
"defaultPath": "properties.accountType",
|
||||
"paths": [
|
||||
{
|
||||
"path": "properties.accountType",
|
||||
"apiVersions": ["2015-06-15", "2015-01-01"]
|
||||
},
|
||||
{
|
||||
"path": "sku.name",
|
||||
"apiVersions": ["2020-01-01", "2019-06-01"]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.Test/versionedResources/config.threshold",
|
||||
"defaultPath": "properties.config.threshold",
|
||||
"paths": [
|
||||
{
|
||||
"path": "properties.config.properties.threshold",
|
||||
"apiVersions": ["2015-06-15", "2015-01-01"]
|
||||
},
|
||||
{
|
||||
"path": "properties.config.threshold",
|
||||
"apiVersions": ["2020-01-01", "2019-06-01"]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.Test/versionedResources/tier",
|
||||
"defaultPath": "sku.tier",
|
||||
"paths": [
|
||||
{
|
||||
"path": "properties.pricingTier",
|
||||
"apiVersions": ["2015-06-15", "2015-01-01"]
|
||||
},
|
||||
{
|
||||
"path": "sku.tier",
|
||||
"apiVersions": ["2020-01-01", "2019-06-01"]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.Test/versionedResources/operationMode",
|
||||
"defaultPath": "properties.mode",
|
||||
"paths": [
|
||||
{
|
||||
"path": "properties.legacyMode",
|
||||
"apiVersions": ["2015-01-01"]
|
||||
},
|
||||
{
|
||||
"path": "properties.settings.mode",
|
||||
"apiVersions": ["2017-06-01"]
|
||||
},
|
||||
{
|
||||
"path": "properties.mode",
|
||||
"apiVersions": ["2020-01-01"]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"resourceType": "complexResources",
|
||||
"aliases": [
|
||||
{
|
||||
"name": "Microsoft.Test/complexResources/enabled",
|
||||
"defaultPath": "properties.enabled",
|
||||
"paths": []
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.Test/complexResources/tags",
|
||||
"defaultPath": "tags",
|
||||
"paths": []
|
||||
},
|
||||
|
||||
{
|
||||
"name": "Microsoft.Test/complexResources/rules",
|
||||
"defaultPath": "properties.rules",
|
||||
"paths": []
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.Test/complexResources/rules[*]",
|
||||
"defaultPath": "properties.rules[*]",
|
||||
"paths": []
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.Test/complexResources/rules[*].name",
|
||||
"defaultPath": "properties.rules[*].name",
|
||||
"paths": []
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.Test/complexResources/rules[*].priority",
|
||||
"defaultPath": "properties.rules[*].properties.priority",
|
||||
"paths": [
|
||||
{
|
||||
"path": "properties.rules[*].properties.priority",
|
||||
"apiVersions": ["2015-01-01"]
|
||||
},
|
||||
{
|
||||
"path": "properties.rules[*].properties.prio",
|
||||
"apiVersions": ["2020-01-01"]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.Test/complexResources/rules[*].action",
|
||||
"defaultPath": "properties.rules[*].properties.action",
|
||||
"paths": []
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.Test/complexResources/rules[*].protocol",
|
||||
"defaultPath": "properties.rules[*].properties.protocol",
|
||||
"paths": []
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.Test/complexResources/rules[*].sourcePort",
|
||||
"defaultPath": "properties.rules[*].properties.sourcePort",
|
||||
"paths": []
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.Test/complexResources/rules[*].destPort",
|
||||
"defaultPath": "properties.rules[*].properties.destPort",
|
||||
"paths": []
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.Test/complexResources/rules[*].direction",
|
||||
"defaultPath": "properties.rules[*].properties.direction",
|
||||
"paths": []
|
||||
},
|
||||
|
||||
{
|
||||
"name": "Microsoft.Test/complexResources/rules[*].targets",
|
||||
"defaultPath": "properties.rules[*].properties.targets",
|
||||
"paths": []
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.Test/complexResources/rules[*].targets[*]",
|
||||
"defaultPath": "properties.rules[*].properties.targets[*]",
|
||||
"paths": []
|
||||
},
|
||||
|
||||
{
|
||||
"name": "Microsoft.Test/complexResources/rules[*].filters",
|
||||
"defaultPath": "properties.rules[*].properties.filters",
|
||||
"paths": []
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.Test/complexResources/rules[*].filters[*]",
|
||||
"defaultPath": "properties.rules[*].properties.filters[*]",
|
||||
"paths": []
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.Test/complexResources/rules[*].filters[*].name",
|
||||
"defaultPath": "properties.rules[*].properties.filters[*].properties.name",
|
||||
"paths": []
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.Test/complexResources/rules[*].filters[*].value",
|
||||
"defaultPath": "properties.rules[*].properties.filters[*].properties.value",
|
||||
"paths": []
|
||||
},
|
||||
|
||||
{
|
||||
"name": "Microsoft.Test/complexResources/allowedIPs",
|
||||
"defaultPath": "properties.allowedIPs",
|
||||
"paths": []
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.Test/complexResources/allowedIPs[*]",
|
||||
"defaultPath": "properties.allowedIPs[*]",
|
||||
"paths": []
|
||||
},
|
||||
|
||||
{
|
||||
"name": "Microsoft.Test/complexResources/config.retryCount",
|
||||
"defaultPath": "properties.config.retryCount",
|
||||
"paths": [
|
||||
{
|
||||
"path": "properties.config.properties.retryCount",
|
||||
"apiVersions": ["2015-01-01"]
|
||||
},
|
||||
{
|
||||
"path": "properties.config.retryCount",
|
||||
"apiVersions": ["2020-01-01"]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.Test/complexResources/config.timeout",
|
||||
"defaultPath": "properties.config.timeout",
|
||||
"paths": []
|
||||
},
|
||||
|
||||
{
|
||||
"name": "Microsoft.Test/complexResources/settings.encryption.enabled",
|
||||
"defaultPath": "properties.settings.encryption.enabled",
|
||||
"paths": [
|
||||
{
|
||||
"path": "properties.settings.encryption.enabled",
|
||||
"apiVersions": ["2020-01-01"]
|
||||
},
|
||||
{
|
||||
"path": "properties.encryptionEnabled",
|
||||
"apiVersions": ["2015-01-01"]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.Test/complexResources/settings.encryption.keyVaultId",
|
||||
"defaultPath": "properties.settings.encryption.keyVaultId",
|
||||
"paths": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,4 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
mod normalization;
|
||||
@@ -0,0 +1,170 @@
|
||||
# Extended data-plane manifest tests covering shapes beyond the basic
|
||||
# KeyVault case: multiple resource types, top-level aliases, nested
|
||||
# objects, array aliases, and schemaVersions.
|
||||
|
||||
data_manifest_json: |
|
||||
{
|
||||
"dataNamespace": "Microsoft.DataFactory.Data",
|
||||
"aliases": [
|
||||
{
|
||||
"name": "Microsoft.DataFactory.Data/factories/pipelines/enabled",
|
||||
"paths": [
|
||||
{
|
||||
"path": "enabled",
|
||||
"apiVersions": ["2018-06-01"]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"resourceTypeAliases": [
|
||||
{
|
||||
"resourceType": "factories/pipelines",
|
||||
"aliases": [
|
||||
{
|
||||
"name": "Microsoft.DataFactory.Data/factories/pipelines/activities[*].type",
|
||||
"paths": [
|
||||
{
|
||||
"path": "activities[*].type",
|
||||
"apiVersions": ["2018-06-01"]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.DataFactory.Data/factories/pipelines/activities[*].name",
|
||||
"paths": [
|
||||
{
|
||||
"path": "activities[*].name",
|
||||
"apiVersions": ["2018-06-01"]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.DataFactory.Data/factories/pipelines/concurrency",
|
||||
"paths": [
|
||||
{
|
||||
"path": "concurrency",
|
||||
"apiVersions": ["2018-06-01"]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.DataFactory.Data/factories/pipelines/folder.name",
|
||||
"paths": [
|
||||
{
|
||||
"path": "folder.name",
|
||||
"apiVersions": ["2018-06-01"]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"resourceType": "factories/datasets",
|
||||
"aliases": [
|
||||
{
|
||||
"name": "Microsoft.DataFactory.Data/factories/datasets/linkedServiceName.referenceName",
|
||||
"paths": [
|
||||
{
|
||||
"path": "linkedServiceName.referenceName",
|
||||
"schemaVersions": ["1"]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.DataFactory.Data/factories/datasets/description",
|
||||
"paths": [
|
||||
{
|
||||
"path": "description",
|
||||
"schemaVersions": ["1"]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
cases:
|
||||
- note: data-plane pipeline with top-level alias and scalar fields
|
||||
use_registry_api: true
|
||||
input:
|
||||
type: "Microsoft.DataFactory.Data/factories/pipelines"
|
||||
enabled: true
|
||||
concurrency: 5
|
||||
folder:
|
||||
name: "etl-jobs"
|
||||
expected_normalized:
|
||||
type: "Microsoft.DataFactory.Data/factories/pipelines"
|
||||
enabled: true
|
||||
concurrency: 5
|
||||
folder:
|
||||
name: "etl-jobs"
|
||||
round_trip: true
|
||||
|
||||
- note: data-plane pipeline with array alias
|
||||
use_registry_api: true
|
||||
input:
|
||||
type: "Microsoft.DataFactory.Data/factories/pipelines"
|
||||
activities:
|
||||
- name: CopyActivity1
|
||||
type: Copy
|
||||
- name: WaitActivity1
|
||||
type: Wait
|
||||
expected_normalized:
|
||||
type: "Microsoft.DataFactory.Data/factories/pipelines"
|
||||
activities:
|
||||
- name: CopyActivity1
|
||||
type: Copy
|
||||
- name: WaitActivity1
|
||||
type: Wait
|
||||
round_trip: true
|
||||
|
||||
- note: data-plane dataset with nested alias path
|
||||
use_registry_api: true
|
||||
input:
|
||||
type: "Microsoft.DataFactory.Data/factories/datasets"
|
||||
description: "Sales data"
|
||||
linkedServiceName:
|
||||
referenceName: "AzureBlobStorage1"
|
||||
type: LinkedServiceReference
|
||||
expected_normalized:
|
||||
type: "Microsoft.DataFactory.Data/factories/datasets"
|
||||
description: "Sales data"
|
||||
linkedservicename:
|
||||
referencename: "AzureBlobStorage1"
|
||||
type: LinkedServiceReference
|
||||
round_trip: true
|
||||
expected_round_trip:
|
||||
type: "Microsoft.DataFactory.Data/factories/datasets"
|
||||
description: "Sales data"
|
||||
linkedServiceName:
|
||||
referenceName: "AzureBlobStorage1"
|
||||
type: LinkedServiceReference
|
||||
|
||||
- note: data-plane without aliases (unknown resource type in namespace)
|
||||
use_registry_api: true
|
||||
input:
|
||||
type: "Microsoft.DataFactory.Data/factories/triggers"
|
||||
recurrence:
|
||||
frequency: Day
|
||||
interval: 1
|
||||
expected_normalized:
|
||||
type: "Microsoft.DataFactory.Data/factories/triggers"
|
||||
recurrence:
|
||||
frequency: Day
|
||||
interval: 1
|
||||
round_trip: true
|
||||
|
||||
- note: data-plane denormalize dataset restores casing
|
||||
use_registry_api: true
|
||||
input:
|
||||
type: "Microsoft.DataFactory.Data/factories/datasets"
|
||||
description: "Sales data"
|
||||
linkedservicename:
|
||||
referencename: "AzureBlobStorage1"
|
||||
expected_denormalized:
|
||||
type: "Microsoft.DataFactory.Data/factories/datasets"
|
||||
description: "Sales data"
|
||||
linkedServiceName:
|
||||
referenceName: "AzureBlobStorage1"
|
||||
reverse_round_trip: true
|
||||
@@ -0,0 +1,154 @@
|
||||
# Broad data-plane compatibility tests covering multiple Azure data-plane
|
||||
# namespaces and resource shapes. Each case exercises a distinct namespace
|
||||
# to ensure the manifest-loading and data-plane normalization paths handle
|
||||
# a variety of real-world patterns.
|
||||
|
||||
data_manifest_json: |
|
||||
{
|
||||
"dataNamespace": "Microsoft.Kubernetes.Data",
|
||||
"aliases": [],
|
||||
"resourceTypeAliases": [
|
||||
{
|
||||
"resourceType": "namespaces",
|
||||
"aliases": [
|
||||
{
|
||||
"name": "Microsoft.Kubernetes.Data/namespaces/labels",
|
||||
"paths": [{ "path": "labels", "apiVersions": ["v1"] }]
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.Kubernetes.Data/namespaces/annotations",
|
||||
"paths": [{ "path": "annotations", "apiVersions": ["v1"] }]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"resourceType": "pods",
|
||||
"aliases": [
|
||||
{
|
||||
"name": "Microsoft.Kubernetes.Data/pods/containers[*].image",
|
||||
"paths": [{ "path": "containers[*].image", "apiVersions": ["v1"] }]
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.Kubernetes.Data/pods/containers[*].name",
|
||||
"paths": [{ "path": "containers[*].name", "apiVersions": ["v1"] }]
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.Kubernetes.Data/pods/containers[*].resources.limits.cpu",
|
||||
"paths": [{ "path": "containers[*].resources.limits.cpu", "apiVersions": ["v1"] }]
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.Kubernetes.Data/pods/hostNetwork",
|
||||
"paths": [{ "path": "hostNetwork", "apiVersions": ["v1"] }]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
cases:
|
||||
# ── Kubernetes namespaces: flat labels/annotations ──
|
||||
- note: "k8s namespace with labels and annotations"
|
||||
use_registry_api: true
|
||||
input:
|
||||
type: "Microsoft.Kubernetes.Data/namespaces"
|
||||
labels:
|
||||
app: web-frontend
|
||||
version: "v2"
|
||||
annotations:
|
||||
owner: team-alpha
|
||||
expected_normalized:
|
||||
type: "Microsoft.Kubernetes.Data/namespaces"
|
||||
labels:
|
||||
app: web-frontend
|
||||
version: "v2"
|
||||
annotations:
|
||||
owner: team-alpha
|
||||
round_trip: true
|
||||
|
||||
# ── Kubernetes pods: arrays with deeply nested fields ──
|
||||
- note: "k8s pod with containers array and resource limits"
|
||||
use_registry_api: true
|
||||
input:
|
||||
type: "Microsoft.Kubernetes.Data/pods"
|
||||
hostNetwork: false
|
||||
containers:
|
||||
- name: app
|
||||
image: "myregistry.azurecr.io/app:latest"
|
||||
resources:
|
||||
limits:
|
||||
cpu: "500m"
|
||||
memory: "256Mi"
|
||||
- name: sidecar
|
||||
image: "myregistry.azurecr.io/sidecar:v1"
|
||||
resources:
|
||||
limits:
|
||||
cpu: "100m"
|
||||
expected_normalized:
|
||||
type: "Microsoft.Kubernetes.Data/pods"
|
||||
hostnetwork: false
|
||||
containers:
|
||||
- name: app
|
||||
image: "myregistry.azurecr.io/app:latest"
|
||||
resources:
|
||||
limits:
|
||||
cpu: "500m"
|
||||
memory: "256Mi"
|
||||
- name: sidecar
|
||||
image: "myregistry.azurecr.io/sidecar:v1"
|
||||
resources:
|
||||
limits:
|
||||
cpu: "100m"
|
||||
round_trip: true
|
||||
|
||||
- note: "k8s pod denormalize restores casing"
|
||||
use_registry_api: true
|
||||
input:
|
||||
type: "Microsoft.Kubernetes.Data/pods"
|
||||
hostnetwork: true
|
||||
containers:
|
||||
- name: app
|
||||
image: "nginx"
|
||||
expected_denormalized:
|
||||
type: "Microsoft.Kubernetes.Data/pods"
|
||||
hostNetwork: true
|
||||
containers:
|
||||
- name: app
|
||||
image: "nginx"
|
||||
reverse_round_trip: true
|
||||
|
||||
# ── Unknown resource type in known namespace ──
|
||||
- note: "k8s unknown resource type passes through unchanged"
|
||||
use_registry_api: true
|
||||
input:
|
||||
type: "Microsoft.Kubernetes.Data/services"
|
||||
clusterIP: "10.0.0.1"
|
||||
ports:
|
||||
- port: 80
|
||||
targetPort: 8080
|
||||
expected_normalized:
|
||||
type: "Microsoft.Kubernetes.Data/services"
|
||||
clusterip: "10.0.0.1"
|
||||
ports:
|
||||
- port: 80
|
||||
targetport: 8080
|
||||
round_trip: true
|
||||
# No aliases for this type, so casing is lost on round-trip.
|
||||
expected_round_trip:
|
||||
type: "Microsoft.Kubernetes.Data/services"
|
||||
clusterip: "10.0.0.1"
|
||||
ports:
|
||||
- port: 80
|
||||
targetport: 8080
|
||||
|
||||
# ── Empty containers array ──
|
||||
- note: "k8s pod with empty containers array"
|
||||
use_registry_api: true
|
||||
input:
|
||||
type: "Microsoft.Kubernetes.Data/pods"
|
||||
hostNetwork: false
|
||||
containers: []
|
||||
expected_normalized:
|
||||
type: "Microsoft.Kubernetes.Data/pods"
|
||||
hostnetwork: false
|
||||
containers: []
|
||||
round_trip: true
|
||||
@@ -0,0 +1,64 @@
|
||||
# Tests that exercise data-plane manifest loading and the data-plane
|
||||
# normalization path (resource type contains ".Data/").
|
||||
# This covers load_data_policy_manifest_json() and the data-plane
|
||||
# normalization branch in normalizer.rs.
|
||||
|
||||
data_manifest_json: |
|
||||
{
|
||||
"dataNamespace": "Microsoft.KeyVault.Data",
|
||||
"aliases": [],
|
||||
"resourceTypeAliases": [
|
||||
{
|
||||
"resourceType": "vaults/certificates",
|
||||
"aliases": [
|
||||
{
|
||||
"name": "Microsoft.KeyVault.Data/vaults/certificates/keySize",
|
||||
"paths": [
|
||||
{
|
||||
"path": "keySize",
|
||||
"apiVersions": ["7.0"]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.KeyVault.Data/vaults/certificates/attributes.expiresOn",
|
||||
"paths": [
|
||||
{
|
||||
"path": "attributes.expiresOn",
|
||||
"apiVersions": ["7.0"]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
cases:
|
||||
- note: data-plane manifest normalize via registry API
|
||||
use_registry_api: true
|
||||
input:
|
||||
type: "Microsoft.KeyVault.Data/vaults/certificates"
|
||||
keySize: 2048
|
||||
attributes:
|
||||
expiresOn: "2025-01-01T00:00:00Z"
|
||||
expected_normalized:
|
||||
type: "Microsoft.KeyVault.Data/vaults/certificates"
|
||||
keysize: 2048
|
||||
attributes:
|
||||
expireson: "2025-01-01T00:00:00Z"
|
||||
round_trip: true
|
||||
|
||||
- note: data-plane manifest round-trip via registry API
|
||||
use_registry_api: true
|
||||
input:
|
||||
type: "Microsoft.KeyVault.Data/vaults/certificates"
|
||||
keySize: 2048
|
||||
attributes:
|
||||
expiresOn: "2025-01-01T00:00:00Z"
|
||||
round_trip: true
|
||||
expected_round_trip:
|
||||
type: "Microsoft.KeyVault.Data/vaults/certificates"
|
||||
keySize: 2048
|
||||
attributes:
|
||||
expiresOn: "2025-01-01T00:00:00Z"
|
||||
@@ -0,0 +1,319 @@
|
||||
aliases_json: |
|
||||
[
|
||||
{
|
||||
"namespace": "Microsoft.Storage",
|
||||
"resourceTypes": [
|
||||
{
|
||||
"resourceType": "storageAccounts",
|
||||
"aliases": [
|
||||
{
|
||||
"name": "Microsoft.Storage/storageAccounts/supportsHttpsTrafficOnly",
|
||||
"defaultPath": "properties.supportsHttpsTrafficOnly",
|
||||
"paths": []
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.Storage/storageAccounts/accessTier",
|
||||
"defaultPath": "properties.accessTier",
|
||||
"paths": []
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.Storage/storageAccounts/sku.name",
|
||||
"defaultPath": "sku.name",
|
||||
"paths": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"namespace": "Microsoft.Network",
|
||||
"resourceTypes": [
|
||||
{
|
||||
"resourceType": "networkSecurityGroups",
|
||||
"aliases": [
|
||||
{
|
||||
"name": "Microsoft.Network/networkSecurityGroups/securityRules[*].protocol",
|
||||
"defaultPath": "properties.securityRules[*].properties.protocol",
|
||||
"paths": []
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.Network/networkSecurityGroups/securityRules[*].access",
|
||||
"defaultPath": "properties.securityRules[*].properties.access",
|
||||
"paths": []
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.Network/networkSecurityGroups/securityRules[*].name",
|
||||
"defaultPath": "properties.securityRules[*].name",
|
||||
"paths": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"resourceType": "virtualNetworks",
|
||||
"aliases": [
|
||||
{
|
||||
"name": "Microsoft.Network/virtualNetworks/subnets[*].addressPrefix",
|
||||
"defaultPath": "properties.subnets[*].properties.addressPrefix",
|
||||
"paths": []
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.Network/virtualNetworks/subnets[*].name",
|
||||
"defaultPath": "properties.subnets[*].name",
|
||||
"paths": []
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.Network/virtualNetworks/subnets[*].ipConfigurations[*].privateIPAddress",
|
||||
"defaultPath": "properties.subnets[*].properties.ipConfigurations[*].properties.privateIPAddress",
|
||||
"paths": []
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.Network/virtualNetworks/subnets[*].ipConfigurations[*].name",
|
||||
"defaultPath": "properties.subnets[*].properties.ipConfigurations[*].name",
|
||||
"paths": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"namespace": "Microsoft.Web",
|
||||
"resourceTypes": [
|
||||
{
|
||||
"resourceType": "sites",
|
||||
"aliases": [
|
||||
{
|
||||
"name": "Microsoft.Web/sites/isEnabled",
|
||||
"defaultPath": "properties.isEnabled",
|
||||
"paths": [
|
||||
{
|
||||
"path": "properties.enabled",
|
||||
"apiVersions": ["2020-01-01"]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"namespace": "test",
|
||||
"resourceTypes": [
|
||||
{
|
||||
"resourceType": "resource",
|
||||
"aliases": [
|
||||
{
|
||||
"name": "test/resource/type",
|
||||
"defaultPath": "properties.type",
|
||||
"paths": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"resourceType": "versionedEnvelope",
|
||||
"aliases": [
|
||||
{
|
||||
"name": "test/versionedEnvelope/items[*].status",
|
||||
"defaultPath": "properties.items[*].properties.status",
|
||||
"paths": [
|
||||
{
|
||||
"path": "properties.items[*].status",
|
||||
"apiVersions": ["2025-01-01"]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "test/versionedEnvelope/items[*].name",
|
||||
"defaultPath": "properties.items[*].name",
|
||||
"paths": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
cases:
|
||||
- note: restores casing from aliases
|
||||
input:
|
||||
name: myStorage
|
||||
type: "Microsoft.Storage/storageAccounts"
|
||||
supportshttpstrafficonly: true
|
||||
accesstier: Hot
|
||||
expected_denormalized:
|
||||
name: myStorage
|
||||
type: "Microsoft.Storage/storageAccounts"
|
||||
properties:
|
||||
supportsHttpsTrafficOnly: true
|
||||
accessTier: Hot
|
||||
reverse_round_trip: true
|
||||
|
||||
- note: versioned path - default
|
||||
input:
|
||||
type: "Microsoft.Web/sites"
|
||||
isenabled: true
|
||||
expected_denormalized:
|
||||
type: "Microsoft.Web/sites"
|
||||
properties:
|
||||
isEnabled: true
|
||||
reverse_round_trip: true
|
||||
|
||||
- note: versioned path - matching version
|
||||
input:
|
||||
type: "Microsoft.Web/sites"
|
||||
isenabled: true
|
||||
api_version: "2020-01-01"
|
||||
expected_denormalized:
|
||||
type: "Microsoft.Web/sites"
|
||||
properties:
|
||||
enabled: true
|
||||
reverse_round_trip: true
|
||||
expected_reverse_round_trip:
|
||||
type: "Microsoft.Web/sites"
|
||||
enabled: true
|
||||
isenabled: true
|
||||
|
||||
- note: collision safe key
|
||||
input:
|
||||
type: "test/resource"
|
||||
_p_type: SubType
|
||||
expected_denormalized:
|
||||
type: "test/resource"
|
||||
properties:
|
||||
type: SubType
|
||||
reverse_round_trip: true
|
||||
|
||||
- note: sub-resource array
|
||||
input:
|
||||
name: myNsg
|
||||
type: "Microsoft.Network/networkSecurityGroups"
|
||||
securityrules:
|
||||
- name: rule1
|
||||
protocol: Tcp
|
||||
access: Allow
|
||||
- name: rule2
|
||||
protocol: "*"
|
||||
access: Deny
|
||||
expected_denormalized:
|
||||
name: myNsg
|
||||
type: "Microsoft.Network/networkSecurityGroups"
|
||||
properties:
|
||||
securityRules:
|
||||
- name: rule1
|
||||
properties:
|
||||
protocol: Tcp
|
||||
access: Allow
|
||||
- name: rule2
|
||||
properties:
|
||||
protocol: "*"
|
||||
access: Deny
|
||||
reverse_round_trip: true
|
||||
|
||||
- note: nested sub-resource arrays
|
||||
input:
|
||||
name: myVnet
|
||||
type: "Microsoft.Network/virtualNetworks"
|
||||
subnets:
|
||||
- name: subnet1
|
||||
addressprefix: "10.0.0.0/24"
|
||||
ipconfigurations:
|
||||
- name: ipconfig1
|
||||
privateipaddress: "10.0.0.4"
|
||||
expected_denormalized:
|
||||
name: myVnet
|
||||
type: "Microsoft.Network/virtualNetworks"
|
||||
properties:
|
||||
subnets:
|
||||
- name: subnet1
|
||||
properties:
|
||||
addressPrefix: "10.0.0.0/24"
|
||||
ipConfigurations:
|
||||
- name: ipconfig1
|
||||
properties:
|
||||
privateIPAddress: "10.0.0.4"
|
||||
reverse_round_trip: true
|
||||
|
||||
- note: root level alias (sku.name)
|
||||
input:
|
||||
type: "Microsoft.Storage/storageAccounts"
|
||||
sku:
|
||||
name: Standard_LRS
|
||||
expected_denormalized:
|
||||
type: "Microsoft.Storage/storageAccounts"
|
||||
sku:
|
||||
name: Standard_LRS
|
||||
reverse_round_trip: true
|
||||
|
||||
- note: versioned envelope classification — default (status under properties)
|
||||
resource_type: "test/versionedEnvelope"
|
||||
sub_resource_arrays: ["items"]
|
||||
input:
|
||||
type: "test/versionedEnvelope"
|
||||
items:
|
||||
- name: item1
|
||||
status: active
|
||||
expected_denormalized:
|
||||
type: "test/versionedEnvelope"
|
||||
properties:
|
||||
items:
|
||||
- name: item1
|
||||
properties:
|
||||
status: active
|
||||
|
||||
- note: versioned envelope classification — version promotes status to envelope
|
||||
resource_type: "test/versionedEnvelope"
|
||||
sub_resource_arrays: ["items"]
|
||||
api_version: "2025-01-01"
|
||||
input:
|
||||
type: "test/versionedEnvelope"
|
||||
items:
|
||||
- name: item1
|
||||
status: active
|
||||
expected_denormalized:
|
||||
type: "test/versionedEnvelope"
|
||||
properties:
|
||||
items:
|
||||
- name: item1
|
||||
status: active
|
||||
|
||||
- note: versioned casing restoration from versioned alias paths
|
||||
aliases_json: |
|
||||
[
|
||||
{
|
||||
"namespace": "Microsoft.Test",
|
||||
"resourceTypes": [
|
||||
{
|
||||
"resourceType": "widgets",
|
||||
"aliases": [
|
||||
{
|
||||
"name": "Microsoft.Test/widgets/items[*].rating",
|
||||
"defaultPath": "properties.items[*].properties.oldField",
|
||||
"paths": [
|
||||
{
|
||||
"path": "properties.items[*].properties.RenamedField",
|
||||
"apiVersions": ["2025-06-01"]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.Test/widgets/items[*].name",
|
||||
"defaultPath": "properties.items[*].name",
|
||||
"paths": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
api_version: "2025-06-01"
|
||||
input:
|
||||
type: "Microsoft.Test/widgets"
|
||||
items:
|
||||
- name: w1
|
||||
rating: 5
|
||||
expected_denormalized:
|
||||
type: "Microsoft.Test/widgets"
|
||||
properties:
|
||||
items:
|
||||
- name: w1
|
||||
properties:
|
||||
RenamedField: 5
|
||||
@@ -0,0 +1,83 @@
|
||||
cases:
|
||||
- note: wraps properties
|
||||
input:
|
||||
name: myStorage
|
||||
type: "Microsoft.Storage/storageAccounts"
|
||||
location: westus2
|
||||
supportshttpstrafficonly: true
|
||||
ishnsenabled: false
|
||||
expected_denormalized:
|
||||
name: myStorage
|
||||
type: "Microsoft.Storage/storageAccounts"
|
||||
location: westus2
|
||||
properties:
|
||||
supportshttpstrafficonly: true
|
||||
ishnsenabled: false
|
||||
reverse_round_trip: true
|
||||
|
||||
- note: non-object returns clone
|
||||
input: "just a string"
|
||||
expected_denormalized: "just a string"
|
||||
reverse_round_trip: true
|
||||
|
||||
- note: empty normalized
|
||||
input: {}
|
||||
expected_denormalized: {}
|
||||
reverse_round_trip: true
|
||||
|
||||
- note: preserves root fields
|
||||
input:
|
||||
name: r
|
||||
type: t
|
||||
location: l
|
||||
kind: k
|
||||
id: "/sub/rg/r"
|
||||
tags:
|
||||
env: prod
|
||||
identity:
|
||||
type: SystemAssigned
|
||||
principalid: pid-123
|
||||
userassignedidentities:
|
||||
/subscriptions/Sub/resourceGroups/Rg/providers/Microsoft.ManagedIdentity/userAssignedIdentities/Uai1: {}
|
||||
sku:
|
||||
name: Basic
|
||||
plan:
|
||||
name: p1
|
||||
zones: ["1", "2"]
|
||||
managedby: "/sub/other"
|
||||
etag: "W/\"abc\""
|
||||
apiversion: "2023-01-01"
|
||||
fullname: parent/child
|
||||
systemdata:
|
||||
createdby: admin
|
||||
extendedlocation:
|
||||
name: edge1
|
||||
type: EdgeZone
|
||||
expected_denormalized:
|
||||
name: r
|
||||
type: t
|
||||
location: l
|
||||
kind: k
|
||||
id: "/sub/rg/r"
|
||||
tags:
|
||||
env: prod
|
||||
identity:
|
||||
type: SystemAssigned
|
||||
principalId: pid-123
|
||||
userAssignedIdentities:
|
||||
/subscriptions/Sub/resourceGroups/Rg/providers/Microsoft.ManagedIdentity/userAssignedIdentities/Uai1: {}
|
||||
sku:
|
||||
name: Basic
|
||||
plan:
|
||||
name: p1
|
||||
zones: ["1", "2"]
|
||||
managedBy: "/sub/other"
|
||||
etag: "W/\"abc\""
|
||||
apiVersion: "2023-01-01"
|
||||
fullName: parent/child
|
||||
systemData:
|
||||
createdBy: admin
|
||||
extendedLocation:
|
||||
name: edge1
|
||||
type: EdgeZone
|
||||
reverse_round_trip: true
|
||||
@@ -0,0 +1,201 @@
|
||||
# Tests pinning behavior for ambiguous or malformed inputs that could
|
||||
# be produced by external callers rather than the normalizer itself.
|
||||
#
|
||||
# The normalizer / denormalizer make assumptions about their input shape
|
||||
# (e.g., keys are fully lowercased after normalization). These tests
|
||||
# document what happens when those assumptions are violated, without
|
||||
# asserting the behavior is "correct" per se — rather, they pin it so
|
||||
# regressions are detected.
|
||||
|
||||
aliases_json: |
|
||||
[
|
||||
{
|
||||
"namespace": "Microsoft.Storage",
|
||||
"resourceTypes": [
|
||||
{
|
||||
"resourceType": "storageAccounts",
|
||||
"aliases": [
|
||||
{
|
||||
"name": "Microsoft.Storage/storageAccounts/supportsHttpsTrafficOnly",
|
||||
"defaultPath": "properties.supportsHttpsTrafficOnly",
|
||||
"paths": []
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.Storage/storageAccounts/accessTier",
|
||||
"defaultPath": "properties.accessTier",
|
||||
"paths": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"namespace": "Microsoft.Network",
|
||||
"resourceTypes": [
|
||||
{
|
||||
"resourceType": "networkSecurityGroups",
|
||||
"aliases": [
|
||||
{
|
||||
"name": "Microsoft.Network/networkSecurityGroups/securityRules[*].protocol",
|
||||
"defaultPath": "properties.securityRules[*].properties.protocol",
|
||||
"paths": []
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.Network/networkSecurityGroups/securityRules[*].access",
|
||||
"defaultPath": "properties.securityRules[*].properties.access",
|
||||
"paths": []
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.Network/networkSecurityGroups/securityRules[*].name",
|
||||
"defaultPath": "properties.securityRules[*].name",
|
||||
"paths": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"namespace": "Microsoft.Web",
|
||||
"resourceTypes": [
|
||||
{
|
||||
"resourceType": "sites",
|
||||
"aliases": [
|
||||
{
|
||||
"name": "Microsoft.Web/sites/isEnabled",
|
||||
"defaultPath": "properties.isEnabled",
|
||||
"paths": [
|
||||
{
|
||||
"path": "properties.enabled",
|
||||
"apiVersions": ["2020-01-01"]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
cases:
|
||||
# ── Normalize: ARM input with both root and properties having same field ──
|
||||
|
||||
- note: "normalize: both root 'name' and properties.name (root wins)"
|
||||
input:
|
||||
name: root-name
|
||||
type: "Microsoft.Storage/storageAccounts"
|
||||
properties:
|
||||
name: props-name
|
||||
accessTier: Hot
|
||||
expected_normalized:
|
||||
name: root-name
|
||||
type: "Microsoft.Storage/storageAccounts"
|
||||
accesstier: Hot
|
||||
|
||||
# ── Normalize: extra fields not in alias catalog or ROOT_FIELDS ──
|
||||
|
||||
- note: "normalize: extra non-aliased properties are kept (lowercased)"
|
||||
input:
|
||||
name: test
|
||||
type: "Microsoft.Storage/storageAccounts"
|
||||
properties:
|
||||
supportsHttpsTrafficOnly: true
|
||||
customUnknownField: 42
|
||||
expected_normalized:
|
||||
name: test
|
||||
type: "Microsoft.Storage/storageAccounts"
|
||||
supportshttpstrafficonly: true
|
||||
customunknownfield: 42
|
||||
|
||||
# ── Denormalize: input has mixed casing (externally constructed) ──
|
||||
|
||||
- note: "denormalize: mixed-case keys still resolve via aliases"
|
||||
input:
|
||||
name: test
|
||||
type: "Microsoft.Storage/storageAccounts"
|
||||
SupportsHttpsTrafficOnly: true
|
||||
expected_denormalized:
|
||||
name: test
|
||||
type: "Microsoft.Storage/storageAccounts"
|
||||
properties:
|
||||
supportsHttpsTrafficOnly: true
|
||||
|
||||
# ── Denormalize: input has extra fields not in alias catalog ──
|
||||
|
||||
- note: "denormalize: unknown fields go under properties (control-plane)"
|
||||
input:
|
||||
name: test
|
||||
type: "Microsoft.Storage/storageAccounts"
|
||||
unknownfield: 123
|
||||
accesstier: Hot
|
||||
expected_denormalized:
|
||||
name: test
|
||||
type: "Microsoft.Storage/storageAccounts"
|
||||
properties:
|
||||
unknownfield: 123
|
||||
accessTier: Hot
|
||||
|
||||
# ── Denormalize with versioned path: both alias-named and ARM-named present ──
|
||||
|
||||
- note: "denormalize: both isenabled and enabled present, alias wins"
|
||||
input:
|
||||
type: "Microsoft.Web/sites"
|
||||
isenabled: true
|
||||
enabled: false
|
||||
api_version: "2020-01-01"
|
||||
expected_denormalized:
|
||||
type: "Microsoft.Web/sites"
|
||||
properties:
|
||||
enabled: true
|
||||
|
||||
- note: "denormalize: both isenabled and enabled present, default path"
|
||||
input:
|
||||
type: "Microsoft.Web/sites"
|
||||
isenabled: true
|
||||
enabled: false
|
||||
expected_denormalized:
|
||||
type: "Microsoft.Web/sites"
|
||||
properties:
|
||||
isEnabled: true
|
||||
enabled: false
|
||||
|
||||
# ── Normalize: ARM resource with empty type ──
|
||||
|
||||
- note: "normalize: empty type field, no alias match"
|
||||
input:
|
||||
name: test
|
||||
type: ""
|
||||
properties:
|
||||
foo: bar
|
||||
expected_normalized:
|
||||
name: test
|
||||
type: ""
|
||||
foo: bar
|
||||
|
||||
# ── Normalize: ARM resource with null properties ──
|
||||
|
||||
- note: "normalize: null properties value"
|
||||
input:
|
||||
name: test
|
||||
properties: null
|
||||
expected_normalized:
|
||||
name: test
|
||||
|
||||
# ── Denormalize: sub-resource array with elements already containing properties ──
|
||||
|
||||
- note: "denormalize: elements already have properties wrapper (double-wrap)"
|
||||
input:
|
||||
name: myNsg
|
||||
type: "Microsoft.Network/networkSecurityGroups"
|
||||
securityrules:
|
||||
- name: rule1
|
||||
properties:
|
||||
protocol: Tcp
|
||||
expected_denormalized:
|
||||
name: myNsg
|
||||
type: "Microsoft.Network/networkSecurityGroups"
|
||||
properties:
|
||||
securityRules:
|
||||
- name: rule1
|
||||
properties:
|
||||
properties:
|
||||
protocol: Tcp
|
||||
@@ -0,0 +1,46 @@
|
||||
aliases_json: |
|
||||
[
|
||||
{
|
||||
"namespace": "Microsoft.Network",
|
||||
"resourceTypes": [
|
||||
{
|
||||
"resourceType": "networkSecurityGroups",
|
||||
"aliases": [
|
||||
{
|
||||
"name": "Microsoft.Network/networkSecurityGroups/securityRules[*].protocol",
|
||||
"defaultPath": "properties.securityRules[*].properties.protocol",
|
||||
"paths": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
cases:
|
||||
- note: normalize-and-wrap full pipeline
|
||||
input:
|
||||
name: myNsg
|
||||
type: "Microsoft.Network/networkSecurityGroups"
|
||||
properties:
|
||||
securityRules:
|
||||
- name: rule1
|
||||
properties:
|
||||
protocol: Tcp
|
||||
context:
|
||||
resourceGroup:
|
||||
name: rg1
|
||||
parameters:
|
||||
env: prod
|
||||
expected_envelope:
|
||||
resource:
|
||||
name: myNsg
|
||||
type: "Microsoft.Network/networkSecurityGroups"
|
||||
securityrules:
|
||||
- name: rule1
|
||||
protocol: Tcp
|
||||
context:
|
||||
resourceGroup:
|
||||
name: rg1
|
||||
parameters:
|
||||
env: prod
|
||||
@@ -0,0 +1,233 @@
|
||||
# Tests for malformed or unexpected *normalized* input fed to the
|
||||
# denormalizer. These pin the current behavior when the denormalizer
|
||||
# receives externally-produced JSON that violates the normalizer's
|
||||
# implicit contract (e.g., keys not lowercased, wrong value types,
|
||||
# missing type field, non-object input).
|
||||
#
|
||||
# These are NOT correctness assertions -- they document what the
|
||||
# denormalizer actually produces so regressions are caught.
|
||||
|
||||
aliases_json: |
|
||||
[
|
||||
{
|
||||
"namespace": "Microsoft.Storage",
|
||||
"resourceTypes": [
|
||||
{
|
||||
"resourceType": "storageAccounts",
|
||||
"aliases": [
|
||||
{
|
||||
"name": "Microsoft.Storage/storageAccounts/supportsHttpsTrafficOnly",
|
||||
"defaultPath": "properties.supportsHttpsTrafficOnly",
|
||||
"paths": []
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.Storage/storageAccounts/accessTier",
|
||||
"defaultPath": "properties.accessTier",
|
||||
"paths": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"namespace": "Microsoft.Network",
|
||||
"resourceTypes": [
|
||||
{
|
||||
"resourceType": "networkSecurityGroups",
|
||||
"aliases": [
|
||||
{
|
||||
"name": "Microsoft.Network/networkSecurityGroups/securityRules[*].protocol",
|
||||
"defaultPath": "properties.securityRules[*].properties.protocol",
|
||||
"paths": []
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.Network/networkSecurityGroups/securityRules[*].name",
|
||||
"defaultPath": "properties.securityRules[*].name",
|
||||
"paths": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
cases:
|
||||
# ── Non-object input ──
|
||||
# The denormalizer should return the value as-is when it's not an object.
|
||||
|
||||
- note: "denormalize: string input returned as-is"
|
||||
input: "just a string"
|
||||
expected_denormalized: "just a string"
|
||||
|
||||
- note: "denormalize: integer input returned as-is"
|
||||
input: 42
|
||||
expected_denormalized: 42
|
||||
|
||||
- note: "denormalize: array input returned as-is"
|
||||
input: [1, 2, 3]
|
||||
expected_denormalized: [1, 2, 3]
|
||||
|
||||
- note: "denormalize: null input returned as-is"
|
||||
input: null
|
||||
expected_denormalized: null
|
||||
|
||||
- note: "denormalize: boolean input returned as-is"
|
||||
input: true
|
||||
expected_denormalized: true
|
||||
|
||||
# ── Missing type field ──
|
||||
# Without a type, no alias resolution occurs; fields are still placed
|
||||
# under `properties` (control-plane default) but casing is not restored.
|
||||
|
||||
- note: "denormalize: no type field, fields go under properties"
|
||||
input:
|
||||
name: test
|
||||
unknownfield: 42
|
||||
expected_denormalized:
|
||||
name: test
|
||||
properties:
|
||||
unknownfield: 42
|
||||
|
||||
# ── Empty object ──
|
||||
|
||||
- note: "denormalize: empty object produces empty object"
|
||||
input: {}
|
||||
expected_denormalized: {}
|
||||
|
||||
# ── Object with only type field ──
|
||||
|
||||
- note: "denormalize: only type field, empty properties"
|
||||
input:
|
||||
type: "Microsoft.Storage/storageAccounts"
|
||||
expected_denormalized:
|
||||
type: "Microsoft.Storage/storageAccounts"
|
||||
|
||||
# ── Normalize: non-object input ──
|
||||
|
||||
- note: "normalize: string input returned as-is"
|
||||
input: "just a string"
|
||||
expected_normalized: "just a string"
|
||||
|
||||
- note: "normalize: integer input returned as-is"
|
||||
input: 42
|
||||
expected_normalized: 42
|
||||
|
||||
- note: "normalize: null input returned as-is"
|
||||
input: null
|
||||
expected_normalized: null
|
||||
|
||||
# ── Normalize: empty properties object ──
|
||||
|
||||
- note: "normalize: empty properties object"
|
||||
input:
|
||||
name: test
|
||||
type: "Microsoft.Storage/storageAccounts"
|
||||
properties: {}
|
||||
expected_normalized:
|
||||
name: test
|
||||
type: "Microsoft.Storage/storageAccounts"
|
||||
|
||||
# ── Normalize: properties is a non-object value ──
|
||||
|
||||
- note: "normalize: properties is a string (treated as non-object, ignored)"
|
||||
input:
|
||||
name: test
|
||||
type: "Microsoft.Storage/storageAccounts"
|
||||
properties: "not an object"
|
||||
expected_normalized:
|
||||
name: test
|
||||
type: "Microsoft.Storage/storageAccounts"
|
||||
|
||||
- note: "normalize: properties is an array (treated as non-object, ignored)"
|
||||
input:
|
||||
name: test
|
||||
type: "Microsoft.Storage/storageAccounts"
|
||||
properties: [1, 2, 3]
|
||||
expected_normalized:
|
||||
name: test
|
||||
type: "Microsoft.Storage/storageAccounts"
|
||||
|
||||
# ── Denormalize: value types preserved through round trip ──
|
||||
|
||||
- note: "denormalize: numeric value zero"
|
||||
input:
|
||||
type: "Microsoft.Storage/storageAccounts"
|
||||
accesstier: 0
|
||||
expected_denormalized:
|
||||
type: "Microsoft.Storage/storageAccounts"
|
||||
properties:
|
||||
accessTier: 0
|
||||
|
||||
- note: "denormalize: boolean false aliased field"
|
||||
input:
|
||||
type: "Microsoft.Storage/storageAccounts"
|
||||
supportshttpstrafficonly: false
|
||||
expected_denormalized:
|
||||
type: "Microsoft.Storage/storageAccounts"
|
||||
properties:
|
||||
supportsHttpsTrafficOnly: false
|
||||
|
||||
- note: "denormalize: null-valued aliased field"
|
||||
input:
|
||||
type: "Microsoft.Storage/storageAccounts"
|
||||
supportshttpstrafficonly: null
|
||||
expected_denormalized:
|
||||
type: "Microsoft.Storage/storageAccounts"
|
||||
properties:
|
||||
supportsHttpsTrafficOnly: null
|
||||
|
||||
# ── Sub-resource array with non-array value ──
|
||||
|
||||
- note: "denormalize: expected sub-resource array is a string"
|
||||
input:
|
||||
name: myNsg
|
||||
type: "Microsoft.Network/networkSecurityGroups"
|
||||
securityrules: "not an array"
|
||||
expected_denormalized:
|
||||
name: myNsg
|
||||
type: "Microsoft.Network/networkSecurityGroups"
|
||||
properties:
|
||||
securityRules: "not an array"
|
||||
|
||||
- note: "denormalize: expected sub-resource array is null"
|
||||
input:
|
||||
name: myNsg
|
||||
type: "Microsoft.Network/networkSecurityGroups"
|
||||
securityrules: null
|
||||
expected_denormalized:
|
||||
name: myNsg
|
||||
type: "Microsoft.Network/networkSecurityGroups"
|
||||
properties:
|
||||
securityRules: null
|
||||
|
||||
# ── Sub-resource array with non-object elements ──
|
||||
|
||||
- note: "denormalize: sub-resource contains scalar elements"
|
||||
input:
|
||||
name: myNsg
|
||||
type: "Microsoft.Network/networkSecurityGroups"
|
||||
securityrules:
|
||||
- "just a string"
|
||||
- 42
|
||||
expected_denormalized:
|
||||
name: myNsg
|
||||
type: "Microsoft.Network/networkSecurityGroups"
|
||||
properties:
|
||||
securityRules:
|
||||
- "just a string"
|
||||
- 42
|
||||
|
||||
# ── Deeply nested null in normalize path ──
|
||||
|
||||
- note: "normalize: deeply nested null value preserved"
|
||||
input:
|
||||
name: test
|
||||
type: "Microsoft.Storage/storageAccounts"
|
||||
properties:
|
||||
supportsHttpsTrafficOnly: null
|
||||
accessTier: null
|
||||
expected_normalized:
|
||||
name: test
|
||||
type: "Microsoft.Storage/storageAccounts"
|
||||
supportshttpstrafficonly: null
|
||||
accesstier: null
|
||||
@@ -0,0 +1,258 @@
|
||||
cases:
|
||||
- note: flattens root properties
|
||||
input:
|
||||
name: myStorage
|
||||
type: "Microsoft.Storage/storageAccounts"
|
||||
location: westus2
|
||||
properties:
|
||||
supportsHttpsTrafficOnly: true
|
||||
isHnsEnabled: false
|
||||
expected_normalized:
|
||||
name: myStorage
|
||||
type: "Microsoft.Storage/storageAccounts"
|
||||
location: westus2
|
||||
supportshttpstrafficonly: true
|
||||
ishnsenabled: false
|
||||
round_trip: true
|
||||
expected_round_trip:
|
||||
name: myStorage
|
||||
type: "Microsoft.Storage/storageAccounts"
|
||||
location: westus2
|
||||
properties:
|
||||
supportshttpstrafficonly: true
|
||||
ishnsenabled: false
|
||||
|
||||
- note: preserves root level precedence
|
||||
input:
|
||||
name: root-name
|
||||
properties:
|
||||
name: props-name
|
||||
expected_normalized:
|
||||
name: root-name
|
||||
round_trip: true
|
||||
expected_round_trip:
|
||||
name: root-name
|
||||
|
||||
- note: leaves plain arrays alone
|
||||
input:
|
||||
name: test
|
||||
properties:
|
||||
networkAcls:
|
||||
ipRules:
|
||||
- value: "10.0.0.1"
|
||||
action: Allow
|
||||
- value: "10.0.0.2"
|
||||
action: Deny
|
||||
expected_normalized:
|
||||
name: test
|
||||
networkacls:
|
||||
iprules:
|
||||
- value: "10.0.0.1"
|
||||
action: Allow
|
||||
- value: "10.0.0.2"
|
||||
action: Deny
|
||||
round_trip: true
|
||||
expected_round_trip:
|
||||
name: test
|
||||
properties:
|
||||
networkacls:
|
||||
iprules:
|
||||
- value: "10.0.0.1"
|
||||
action: Allow
|
||||
- value: "10.0.0.2"
|
||||
action: Deny
|
||||
|
||||
- note: handles sku at root
|
||||
input:
|
||||
name: test
|
||||
sku:
|
||||
name: Standard_LRS
|
||||
tier: Standard
|
||||
properties:
|
||||
supportsHttpsTrafficOnly: true
|
||||
expected_normalized:
|
||||
name: test
|
||||
sku:
|
||||
name: Standard_LRS
|
||||
tier: Standard
|
||||
supportshttpstrafficonly: true
|
||||
round_trip: true
|
||||
expected_round_trip:
|
||||
name: test
|
||||
sku:
|
||||
name: Standard_LRS
|
||||
tier: Standard
|
||||
properties:
|
||||
supportshttpstrafficonly: true
|
||||
|
||||
- note: non-object returns clone
|
||||
input: "just a string"
|
||||
expected_normalized: "just a string"
|
||||
round_trip: true
|
||||
|
||||
- note: empty properties
|
||||
input:
|
||||
name: test
|
||||
properties: {}
|
||||
expected_normalized:
|
||||
name: test
|
||||
round_trip: true
|
||||
expected_round_trip:
|
||||
name: test
|
||||
|
||||
- note: missing properties
|
||||
input:
|
||||
name: test
|
||||
location: eastus
|
||||
expected_normalized:
|
||||
name: test
|
||||
location: eastus
|
||||
round_trip: true
|
||||
|
||||
- note: preserves all root fields
|
||||
input:
|
||||
name: r
|
||||
type: t
|
||||
location: l
|
||||
kind: k
|
||||
id: "/sub/rg/r"
|
||||
tags:
|
||||
env: prod
|
||||
identity:
|
||||
type: SystemAssigned
|
||||
principalId: pid-123
|
||||
userAssignedIdentities:
|
||||
/subscriptions/Sub/resourceGroups/Rg/providers/Microsoft.ManagedIdentity/userAssignedIdentities/Uai1: {}
|
||||
sku:
|
||||
name: Basic
|
||||
plan:
|
||||
name: p1
|
||||
zones: ["1", "2"]
|
||||
managedBy: "/sub/other"
|
||||
etag: "W/\"abc\""
|
||||
apiVersion: "2023-01-01"
|
||||
fullName: parent/child
|
||||
systemData:
|
||||
createdBy: admin
|
||||
extendedLocation:
|
||||
name: edge1
|
||||
type: EdgeZone
|
||||
properties:
|
||||
someProp: true
|
||||
expected_normalized:
|
||||
name: r
|
||||
type: t
|
||||
location: l
|
||||
kind: k
|
||||
id: "/sub/rg/r"
|
||||
tags:
|
||||
env: prod
|
||||
identity:
|
||||
type: SystemAssigned
|
||||
principalid: pid-123
|
||||
userassignedidentities:
|
||||
/subscriptions/Sub/resourceGroups/Rg/providers/Microsoft.ManagedIdentity/userAssignedIdentities/Uai1: {}
|
||||
sku:
|
||||
name: Basic
|
||||
plan:
|
||||
name: p1
|
||||
zones: ["1", "2"]
|
||||
managedby: "/sub/other"
|
||||
etag: "W/\"abc\""
|
||||
apiversion: "2023-01-01"
|
||||
fullname: parent/child
|
||||
systemdata:
|
||||
createdby: admin
|
||||
extendedlocation:
|
||||
name: edge1
|
||||
type: EdgeZone
|
||||
someprop: true
|
||||
round_trip: true
|
||||
expected_round_trip:
|
||||
name: r
|
||||
type: t
|
||||
location: l
|
||||
kind: k
|
||||
id: "/sub/rg/r"
|
||||
tags:
|
||||
env: prod
|
||||
identity:
|
||||
type: SystemAssigned
|
||||
principalId: pid-123
|
||||
userAssignedIdentities:
|
||||
/subscriptions/Sub/resourceGroups/Rg/providers/Microsoft.ManagedIdentity/userAssignedIdentities/Uai1: {}
|
||||
sku:
|
||||
name: Basic
|
||||
plan:
|
||||
name: p1
|
||||
zones: ["1", "2"]
|
||||
managedBy: "/sub/other"
|
||||
etag: "W/\"abc\""
|
||||
apiVersion: "2023-01-01"
|
||||
fullName: parent/child
|
||||
systemData:
|
||||
createdBy: admin
|
||||
extendedLocation:
|
||||
name: edge1
|
||||
type: EdgeZone
|
||||
properties:
|
||||
someprop: true
|
||||
|
||||
- note: primitive array pass through
|
||||
input:
|
||||
name: test
|
||||
properties:
|
||||
allowedIPs: ["10.0.0.1", "10.0.0.2", "10.0.0.3"]
|
||||
expected_normalized:
|
||||
name: test
|
||||
allowedips: ["10.0.0.1", "10.0.0.2", "10.0.0.3"]
|
||||
round_trip: true
|
||||
expected_round_trip:
|
||||
name: test
|
||||
properties:
|
||||
allowedips: ["10.0.0.1", "10.0.0.2", "10.0.0.3"]
|
||||
|
||||
- note: deeply nested object no sub-resource
|
||||
input:
|
||||
name: test
|
||||
properties:
|
||||
networkAcls:
|
||||
defaultAction: Deny
|
||||
virtualNetworkRules:
|
||||
- id: "/vnet/subnet1"
|
||||
action: Allow
|
||||
expected_normalized:
|
||||
name: test
|
||||
networkacls:
|
||||
defaultaction: Deny
|
||||
virtualnetworkrules:
|
||||
- id: "/vnet/subnet1"
|
||||
action: Allow
|
||||
round_trip: true
|
||||
expected_round_trip:
|
||||
name: test
|
||||
properties:
|
||||
networkacls:
|
||||
defaultaction: Deny
|
||||
virtualnetworkrules:
|
||||
- id: "/vnet/subnet1"
|
||||
action: Allow
|
||||
|
||||
- note: unknown root fields are dropped (not in ROOT_FIELDS)
|
||||
input:
|
||||
name: test
|
||||
type: "Microsoft.Foo/bars"
|
||||
fooExtension:
|
||||
barBaz: 1
|
||||
properties:
|
||||
enabled: true
|
||||
expected_normalized:
|
||||
name: test
|
||||
type: "Microsoft.Foo/bars"
|
||||
enabled: true
|
||||
round_trip: true
|
||||
expected_round_trip:
|
||||
name: test
|
||||
type: "Microsoft.Foo/bars"
|
||||
properties:
|
||||
enabled: true
|
||||
@@ -0,0 +1,26 @@
|
||||
cases:
|
||||
- note: envelope defaults
|
||||
input:
|
||||
name: x
|
||||
expected_envelope:
|
||||
resource:
|
||||
name: x
|
||||
context: {}
|
||||
parameters: {}
|
||||
|
||||
- note: envelope with context and parameters
|
||||
input:
|
||||
name: x
|
||||
context:
|
||||
resourceGroup:
|
||||
name: rg1
|
||||
parameters:
|
||||
env: prod
|
||||
expected_envelope:
|
||||
resource:
|
||||
name: x
|
||||
context:
|
||||
resourceGroup:
|
||||
name: rg1
|
||||
parameters:
|
||||
env: prod
|
||||
@@ -0,0 +1,123 @@
|
||||
cases:
|
||||
- note: flattens sub-resource arrays
|
||||
input:
|
||||
name: myNsg
|
||||
type: "Microsoft.Network/networkSecurityGroups"
|
||||
properties:
|
||||
securityRules:
|
||||
- name: rule1
|
||||
properties:
|
||||
protocol: Tcp
|
||||
access: Allow
|
||||
- name: rule2
|
||||
properties:
|
||||
protocol: "*"
|
||||
access: Deny
|
||||
sub_resource_arrays: ["securityRules"]
|
||||
resource_type: "Microsoft.Network/networkSecurityGroups"
|
||||
expected_normalized:
|
||||
name: myNsg
|
||||
type: "Microsoft.Network/networkSecurityGroups"
|
||||
securityrules:
|
||||
- name: rule1
|
||||
protocol: Tcp
|
||||
access: Allow
|
||||
- name: rule2
|
||||
protocol: "*"
|
||||
access: Deny
|
||||
round_trip: true
|
||||
expected_round_trip:
|
||||
name: myNsg
|
||||
type: "Microsoft.Network/networkSecurityGroups"
|
||||
properties:
|
||||
securityrules:
|
||||
- name: rule1
|
||||
properties:
|
||||
protocol: Tcp
|
||||
access: Allow
|
||||
- name: rule2
|
||||
properties:
|
||||
protocol: "*"
|
||||
access: Deny
|
||||
|
||||
- note: nested sub-resource arrays
|
||||
input:
|
||||
name: myVnet
|
||||
properties:
|
||||
subnets:
|
||||
- name: subnet1
|
||||
properties:
|
||||
addressPrefix: "10.0.0.0/24"
|
||||
ipConfigurations:
|
||||
- name: ipconfig1
|
||||
properties:
|
||||
privateIPAddress: "10.0.0.4"
|
||||
sub_resource_arrays: ["subnets", "subnets.ipConfigurations"]
|
||||
resource_type: "Microsoft.Network/virtualNetworks"
|
||||
expected_normalized:
|
||||
name: myVnet
|
||||
subnets:
|
||||
- name: subnet1
|
||||
addressprefix: "10.0.0.0/24"
|
||||
ipconfigurations:
|
||||
- name: ipconfig1
|
||||
privateipaddress: "10.0.0.4"
|
||||
round_trip: true
|
||||
expected_round_trip:
|
||||
name: myVnet
|
||||
properties:
|
||||
subnets:
|
||||
- name: subnet1
|
||||
properties:
|
||||
addressprefix: "10.0.0.0/24"
|
||||
ipconfigurations:
|
||||
- name: ipconfig1
|
||||
properties:
|
||||
privateipaddress: "10.0.0.4"
|
||||
|
||||
- note: sub-resource element without properties
|
||||
input:
|
||||
name: test
|
||||
properties:
|
||||
items:
|
||||
- name: plain-object
|
||||
enabled: true
|
||||
sub_resource_arrays: ["items"]
|
||||
resource_type: test
|
||||
expected_normalized:
|
||||
name: test
|
||||
items:
|
||||
- name: plain-object
|
||||
enabled: true
|
||||
round_trip: true
|
||||
expected_round_trip:
|
||||
name: test
|
||||
properties:
|
||||
items:
|
||||
- name: plain-object
|
||||
properties:
|
||||
enabled: true
|
||||
|
||||
- note: case-insensitive sub-resource match
|
||||
input:
|
||||
name: test
|
||||
properties:
|
||||
securityRules:
|
||||
- name: r1
|
||||
properties:
|
||||
protocol: Tcp
|
||||
sub_resource_arrays: ["SecurityRules"]
|
||||
resource_type: test
|
||||
expected_normalized:
|
||||
name: test
|
||||
securityrules:
|
||||
- name: r1
|
||||
protocol: Tcp
|
||||
round_trip: true
|
||||
expected_round_trip:
|
||||
name: test
|
||||
properties:
|
||||
securityrules:
|
||||
- name: r1
|
||||
properties:
|
||||
protocol: Tcp
|
||||
@@ -0,0 +1,127 @@
|
||||
# Tests that exercise the public AliasRegistry API (normalize, denormalize,
|
||||
# normalize_and_wrap) rather than the low-level *_with_aliases functions.
|
||||
# This ensures the registry lookup path and public entry points are covered.
|
||||
|
||||
aliases_json: |
|
||||
[
|
||||
{
|
||||
"namespace": "Microsoft.Storage",
|
||||
"resourceTypes": [
|
||||
{
|
||||
"resourceType": "storageAccounts",
|
||||
"aliases": [
|
||||
{
|
||||
"name": "Microsoft.Storage/storageAccounts/supportsHttpsTrafficOnly",
|
||||
"defaultPath": "properties.supportsHttpsTrafficOnly",
|
||||
"paths": []
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.Storage/storageAccounts/isHnsEnabled",
|
||||
"defaultPath": "properties.isHnsEnabled",
|
||||
"paths": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"namespace": "Microsoft.Network",
|
||||
"resourceTypes": [
|
||||
{
|
||||
"resourceType": "networkSecurityGroups",
|
||||
"aliases": [
|
||||
{
|
||||
"name": "Microsoft.Network/networkSecurityGroups/securityRules[*].protocol",
|
||||
"defaultPath": "properties.securityRules[*].properties.protocol",
|
||||
"paths": []
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.Network/networkSecurityGroups/securityRules[*].name",
|
||||
"defaultPath": "properties.securityRules[*].name",
|
||||
"paths": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
cases:
|
||||
- note: registry API normalize
|
||||
use_registry_api: true
|
||||
input:
|
||||
name: myStorage
|
||||
type: "Microsoft.Storage/storageAccounts"
|
||||
location: westus2
|
||||
properties:
|
||||
supportsHttpsTrafficOnly: true
|
||||
isHnsEnabled: false
|
||||
expected_normalized:
|
||||
name: myStorage
|
||||
type: "Microsoft.Storage/storageAccounts"
|
||||
location: westus2
|
||||
supportshttpstrafficonly: true
|
||||
ishnsenabled: false
|
||||
round_trip: true
|
||||
|
||||
- note: registry API denormalize
|
||||
use_registry_api: true
|
||||
input:
|
||||
name: myStorage
|
||||
type: "Microsoft.Storage/storageAccounts"
|
||||
location: westus2
|
||||
supportshttpstrafficonly: true
|
||||
ishnsenabled: false
|
||||
expected_denormalized:
|
||||
name: myStorage
|
||||
type: "Microsoft.Storage/storageAccounts"
|
||||
location: westus2
|
||||
properties:
|
||||
supportsHttpsTrafficOnly: true
|
||||
isHnsEnabled: false
|
||||
reverse_round_trip: true
|
||||
|
||||
- note: registry API normalize_and_wrap (envelope)
|
||||
use_registry_api: true
|
||||
input:
|
||||
name: myNsg
|
||||
type: "Microsoft.Network/networkSecurityGroups"
|
||||
properties:
|
||||
securityRules:
|
||||
- name: rule1
|
||||
properties:
|
||||
protocol: Tcp
|
||||
context:
|
||||
resourceGroup:
|
||||
name: rg1
|
||||
expected_envelope:
|
||||
resource:
|
||||
name: myNsg
|
||||
type: "Microsoft.Network/networkSecurityGroups"
|
||||
securityrules:
|
||||
- name: rule1
|
||||
protocol: Tcp
|
||||
context:
|
||||
resourceGroup:
|
||||
name: rg1
|
||||
parameters: {}
|
||||
|
||||
- note: registry API round-trip sub-resource
|
||||
use_registry_api: true
|
||||
input:
|
||||
name: myNsg
|
||||
type: "Microsoft.Network/networkSecurityGroups"
|
||||
properties:
|
||||
securityRules:
|
||||
- name: rule1
|
||||
properties:
|
||||
protocol: Tcp
|
||||
round_trip: true
|
||||
expected_round_trip:
|
||||
name: myNsg
|
||||
type: "Microsoft.Network/networkSecurityGroups"
|
||||
properties:
|
||||
securityRules:
|
||||
- name: rule1
|
||||
properties:
|
||||
protocol: Tcp
|
||||
@@ -0,0 +1,449 @@
|
||||
aliases_json: |
|
||||
[
|
||||
{
|
||||
"namespace": "Microsoft.Storage",
|
||||
"resourceTypes": [
|
||||
{
|
||||
"resourceType": "storageAccounts",
|
||||
"aliases": [
|
||||
{
|
||||
"name": "Microsoft.Storage/storageAccounts/supportsHttpsTrafficOnly",
|
||||
"defaultPath": "properties.supportsHttpsTrafficOnly",
|
||||
"paths": []
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.Storage/storageAccounts/isHnsEnabled",
|
||||
"defaultPath": "properties.isHnsEnabled",
|
||||
"paths": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"namespace": "Microsoft.Network",
|
||||
"resourceTypes": [
|
||||
{
|
||||
"resourceType": "networkSecurityGroups",
|
||||
"aliases": [
|
||||
{
|
||||
"name": "Microsoft.Network/networkSecurityGroups/securityRules[*].protocol",
|
||||
"defaultPath": "properties.securityRules[*].properties.protocol",
|
||||
"paths": []
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.Network/networkSecurityGroups/securityRules[*].access",
|
||||
"defaultPath": "properties.securityRules[*].properties.access",
|
||||
"paths": []
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.Network/networkSecurityGroups/securityRules[*].name",
|
||||
"defaultPath": "properties.securityRules[*].name",
|
||||
"paths": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
cases:
|
||||
- note: round-trip simple resource
|
||||
input:
|
||||
name: myStorage
|
||||
type: "Microsoft.Storage/storageAccounts"
|
||||
location: westus2
|
||||
sku:
|
||||
name: Standard_LRS
|
||||
properties:
|
||||
supportsHttpsTrafficOnly: true
|
||||
isHnsEnabled: false
|
||||
round_trip: true
|
||||
expected_round_trip:
|
||||
name: myStorage
|
||||
type: "Microsoft.Storage/storageAccounts"
|
||||
location: westus2
|
||||
sku:
|
||||
name: Standard_LRS
|
||||
properties:
|
||||
supportsHttpsTrafficOnly: true
|
||||
isHnsEnabled: false
|
||||
|
||||
- note: round-trip sub-resource
|
||||
input:
|
||||
name: myNsg
|
||||
type: "Microsoft.Network/networkSecurityGroups"
|
||||
properties:
|
||||
securityRules:
|
||||
- name: rule1
|
||||
properties:
|
||||
protocol: Tcp
|
||||
access: Allow
|
||||
round_trip: true
|
||||
expected_round_trip:
|
||||
name: myNsg
|
||||
type: "Microsoft.Network/networkSecurityGroups"
|
||||
properties:
|
||||
securityRules:
|
||||
- name: rule1
|
||||
properties:
|
||||
protocol: Tcp
|
||||
access: Allow
|
||||
|
||||
- note: round-trip versioned array alias (element field remap)
|
||||
aliases_json: |
|
||||
[
|
||||
{
|
||||
"namespace": "Microsoft.Network",
|
||||
"resourceTypes": [
|
||||
{
|
||||
"resourceType": "firewallPolicies",
|
||||
"aliases": [
|
||||
{
|
||||
"name": "Microsoft.Network/firewallPolicies/rules[*].priority",
|
||||
"defaultPath": "properties.rules[*].properties.priority",
|
||||
"paths": [
|
||||
{
|
||||
"path": "properties.rules[*].properties.prio",
|
||||
"apiVersions": ["2021-01-01"]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.Network/firewallPolicies/rules[*].name",
|
||||
"defaultPath": "properties.rules[*].name",
|
||||
"paths": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
input:
|
||||
name: myPolicy
|
||||
type: "Microsoft.Network/firewallPolicies"
|
||||
properties:
|
||||
rules:
|
||||
- name: rule1
|
||||
properties:
|
||||
prio: 100
|
||||
api_version: "2021-01-01"
|
||||
round_trip: true
|
||||
expected_round_trip:
|
||||
name: myPolicy
|
||||
type: "Microsoft.Network/firewallPolicies"
|
||||
properties:
|
||||
rules:
|
||||
- name: rule1
|
||||
properties:
|
||||
prio: 100
|
||||
|
||||
# Regression: default and versioned paths both produce exactly one element
|
||||
# remap (same count), but the source field differs. A length-only comparison
|
||||
# would silently reuse the default aggregate, producing incorrect results for
|
||||
# the versioned API version.
|
||||
- note: round-trip versioned remap with same count but different source field
|
||||
aliases_json: |
|
||||
[
|
||||
{
|
||||
"namespace": "Microsoft.Network",
|
||||
"resourceTypes": [
|
||||
{
|
||||
"resourceType": "firewallPolicies",
|
||||
"aliases": [
|
||||
{
|
||||
"name": "Microsoft.Network/firewallPolicies/rules[*].priority",
|
||||
"defaultPath": "properties.rules[*].properties.oldA",
|
||||
"paths": [
|
||||
{
|
||||
"path": "properties.rules[*].properties.oldB",
|
||||
"apiVersions": ["2023-06-01"]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.Network/firewallPolicies/rules[*].name",
|
||||
"defaultPath": "properties.rules[*].name",
|
||||
"paths": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
input:
|
||||
name: myPolicy
|
||||
type: "Microsoft.Network/firewallPolicies"
|
||||
properties:
|
||||
rules:
|
||||
- name: rule1
|
||||
properties:
|
||||
oldB: 100
|
||||
api_version: "2023-06-01"
|
||||
round_trip: true
|
||||
expected_round_trip:
|
||||
name: myPolicy
|
||||
type: "Microsoft.Network/firewallPolicies"
|
||||
properties:
|
||||
rules:
|
||||
- name: rule1
|
||||
properties:
|
||||
oldB: 100
|
||||
|
||||
- note: round-trip systemData and extendedLocation
|
||||
input:
|
||||
name: myStorage
|
||||
type: "Microsoft.Storage/storageAccounts"
|
||||
location: westus2
|
||||
identity:
|
||||
type: SystemAssigned
|
||||
principalId: pid-123
|
||||
userAssignedIdentities:
|
||||
/subscriptions/Sub/resourceGroups/Rg/providers/Microsoft.ManagedIdentity/userAssignedIdentities/Uai1: {}
|
||||
systemData:
|
||||
createdBy: user@example.com
|
||||
createdByType: User
|
||||
createdAt: "2023-01-01T00:00:00Z"
|
||||
extendedLocation:
|
||||
name: edge-site-1
|
||||
type: EdgeZone
|
||||
properties:
|
||||
supportsHttpsTrafficOnly: true
|
||||
expected_normalized:
|
||||
name: myStorage
|
||||
type: "Microsoft.Storage/storageAccounts"
|
||||
location: westus2
|
||||
identity:
|
||||
type: SystemAssigned
|
||||
principalid: pid-123
|
||||
userassignedidentities:
|
||||
/subscriptions/Sub/resourceGroups/Rg/providers/Microsoft.ManagedIdentity/userAssignedIdentities/Uai1: {}
|
||||
systemdata:
|
||||
createdby: user@example.com
|
||||
createdbytype: User
|
||||
createdat: "2023-01-01T00:00:00Z"
|
||||
extendedlocation:
|
||||
name: edge-site-1
|
||||
type: EdgeZone
|
||||
supportshttpstrafficonly: true
|
||||
round_trip: true
|
||||
expected_round_trip:
|
||||
name: myStorage
|
||||
type: "Microsoft.Storage/storageAccounts"
|
||||
location: westus2
|
||||
identity:
|
||||
type: SystemAssigned
|
||||
principalId: pid-123
|
||||
userAssignedIdentities:
|
||||
/subscriptions/Sub/resourceGroups/Rg/providers/Microsoft.ManagedIdentity/userAssignedIdentities/Uai1: {}
|
||||
systemData:
|
||||
createdBy: user@example.com
|
||||
createdByType: User
|
||||
createdAt: "2023-01-01T00:00:00Z"
|
||||
extendedLocation:
|
||||
name: edge-site-1
|
||||
type: EdgeZone
|
||||
properties:
|
||||
supportsHttpsTrafficOnly: true
|
||||
|
||||
# Regression: element remap must remove the stale ARM source field from the
|
||||
# normalized output. Without cleanup, both the alias short name AND the
|
||||
# original ARM leaf key survive; casing restoration during denormalization
|
||||
# then produces a duplicate key (e.g. both "prio" and "priority").
|
||||
- note: element remap removes stale ARM source field
|
||||
aliases_json: |
|
||||
[
|
||||
{
|
||||
"namespace": "Microsoft.Network",
|
||||
"resourceTypes": [
|
||||
{
|
||||
"resourceType": "firewallPolicies",
|
||||
"aliases": [
|
||||
{
|
||||
"name": "Microsoft.Network/firewallPolicies/rules[*].priority",
|
||||
"defaultPath": "properties.rules[*].properties.prio",
|
||||
"paths": []
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.Network/firewallPolicies/rules[*].name",
|
||||
"defaultPath": "properties.rules[*].name",
|
||||
"paths": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
input:
|
||||
name: myPolicy
|
||||
type: "Microsoft.Network/firewallPolicies"
|
||||
properties:
|
||||
rules:
|
||||
- name: rule1
|
||||
properties:
|
||||
prio: 42
|
||||
# Normalize: "prio" (ARM leaf) → "priority" (alias short name).
|
||||
# The stale "prio" key must be removed from the normalized element.
|
||||
expected_normalized:
|
||||
name: myPolicy
|
||||
type: "Microsoft.Network/firewallPolicies"
|
||||
rules:
|
||||
- name: rule1
|
||||
priority: 42
|
||||
# Round-trip: normalize → denormalize should restore ARM structure.
|
||||
round_trip: true
|
||||
expected_round_trip:
|
||||
name: myPolicy
|
||||
type: "Microsoft.Network/firewallPolicies"
|
||||
properties:
|
||||
rules:
|
||||
- name: rule1
|
||||
properties:
|
||||
prio: 42
|
||||
|
||||
# Regression: same stale-field-cleanup scenario but with a dotted ARM leaf
|
||||
# path (e.g. "config.value" rather than a single segment "prio").
|
||||
# The remove_element_field helper must navigate to the parent object and
|
||||
# remove the leaf key via remove_at_dotted_path.
|
||||
- note: element remap removes stale dotted ARM source field
|
||||
aliases_json: |
|
||||
[
|
||||
{
|
||||
"namespace": "Microsoft.Test",
|
||||
"resourceTypes": [
|
||||
{
|
||||
"resourceType": "widgets",
|
||||
"aliases": [
|
||||
{
|
||||
"name": "Microsoft.Test/widgets/items[*].rating",
|
||||
"defaultPath": "properties.items[*].properties.config.score",
|
||||
"paths": []
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.Test/widgets/items[*].name",
|
||||
"defaultPath": "properties.items[*].name",
|
||||
"paths": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
input:
|
||||
type: "Microsoft.Test/widgets"
|
||||
properties:
|
||||
items:
|
||||
- name: w1
|
||||
properties:
|
||||
config:
|
||||
score: 5
|
||||
# Normalize: "config.score" (dotted ARM leaf) → "rating" (alias short name).
|
||||
# The stale "config.score" path must be removed; only "rating" should remain.
|
||||
expected_normalized:
|
||||
type: "Microsoft.Test/widgets"
|
||||
items:
|
||||
- name: w1
|
||||
config: {}
|
||||
rating: 5
|
||||
# Round-trip: normalize → denormalize should restore ARM structure.
|
||||
round_trip: true
|
||||
expected_round_trip:
|
||||
type: "Microsoft.Test/widgets"
|
||||
properties:
|
||||
items:
|
||||
- name: w1
|
||||
properties:
|
||||
config:
|
||||
score: 5
|
||||
|
||||
# Regression: array base rename must move (not clone) the value so that
|
||||
# the stale ARM base key does not survive in the normalized output.
|
||||
# Without the removal, denormalization produces a duplicate key.
|
||||
- note: array base rename removes stale ARM base key
|
||||
aliases_json: |
|
||||
[
|
||||
{
|
||||
"namespace": "Microsoft.Test",
|
||||
"resourceTypes": [
|
||||
{
|
||||
"resourceType": "widgets",
|
||||
"aliases": [
|
||||
{
|
||||
"name": "Microsoft.Test/widgets/items[*].label",
|
||||
"defaultPath": "properties.entries[*].label",
|
||||
"paths": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
input:
|
||||
type: "Microsoft.Test/widgets"
|
||||
properties:
|
||||
entries:
|
||||
- label: hello
|
||||
# Normalize: ARM base "entries" → alias base "items".
|
||||
# The stale "entries" key must be removed; only "items" should remain.
|
||||
expected_normalized:
|
||||
type: "Microsoft.Test/widgets"
|
||||
items:
|
||||
- label: hello
|
||||
# Round-trip: normalize → denormalize should restore ARM structure.
|
||||
round_trip: true
|
||||
expected_round_trip:
|
||||
type: "Microsoft.Test/widgets"
|
||||
properties:
|
||||
entries:
|
||||
- label: hello
|
||||
|
||||
# Regression: denormalize reverse element remap with a dotted ARM target
|
||||
# must preserve restored casing (e.g. "Config.Score") rather than
|
||||
# re-lowercasing it.
|
||||
- note: round-trip dotted element remap preserves ARM casing
|
||||
aliases_json: |
|
||||
[
|
||||
{
|
||||
"namespace": "Microsoft.Test",
|
||||
"resourceTypes": [
|
||||
{
|
||||
"resourceType": "widgets",
|
||||
"aliases": [
|
||||
{
|
||||
"name": "Microsoft.Test/widgets/items[*].rating",
|
||||
"defaultPath": "properties.items[*].properties.Config.Score",
|
||||
"paths": []
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.Test/widgets/items[*].name",
|
||||
"defaultPath": "properties.items[*].name",
|
||||
"paths": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
input:
|
||||
type: "Microsoft.Test/widgets"
|
||||
properties:
|
||||
items:
|
||||
- name: w1
|
||||
properties:
|
||||
Config:
|
||||
Score: 9
|
||||
expected_normalized:
|
||||
type: "Microsoft.Test/widgets"
|
||||
items:
|
||||
- name: w1
|
||||
config: {}
|
||||
rating: 9
|
||||
round_trip: true
|
||||
expected_round_trip:
|
||||
type: "Microsoft.Test/widgets"
|
||||
properties:
|
||||
items:
|
||||
- name: w1
|
||||
properties:
|
||||
Config:
|
||||
Score: 9
|
||||
@@ -0,0 +1,322 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
//! YAML-driven normalization / denormalization tests.
|
||||
//!
|
||||
//! Each YAML file contains a `cases` array. Every case specifies an `input`
|
||||
//! JSON value plus optional `aliases` and `api_version`. The runner then
|
||||
//! checks whichever of the following fields are present:
|
||||
//!
|
||||
//! * `expected_normalized` – result of normalizing `input`
|
||||
//! * `expected_denormalized` – result of denormalizing `input`
|
||||
//! * `round_trip` – normalize then denormalize; compare with `expected_round_trip`
|
||||
//! * `reverse_round_trip` – denormalize then normalize; compare with
|
||||
//! `expected_reverse_round_trip`
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
use serde::Deserialize;
|
||||
use test_generator::test_resources;
|
||||
|
||||
use regorus::languages::azure_policy::aliases::normalizer;
|
||||
use regorus::languages::azure_policy::aliases::types::ResolvedAliases;
|
||||
use regorus::languages::azure_policy::aliases::{denormalizer, AliasRegistry};
|
||||
use regorus::Value;
|
||||
|
||||
// ── YAML schema ──────────────────────────────────────────────────────────
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct YamlTest {
|
||||
#[serde(default)]
|
||||
aliases_json: Option<String>,
|
||||
#[serde(default)]
|
||||
aliases_file: Option<String>,
|
||||
#[serde(default)]
|
||||
data_manifest_json: Option<String>,
|
||||
#[serde(default)]
|
||||
data_manifest_file: Option<String>,
|
||||
cases: Vec<TestCase>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct TestCase {
|
||||
note: String,
|
||||
input: serde_json::Value,
|
||||
#[serde(default)]
|
||||
api_version: Option<String>,
|
||||
#[serde(default)]
|
||||
context: Option<serde_json::Value>,
|
||||
#[serde(default)]
|
||||
parameters: Option<serde_json::Value>,
|
||||
#[serde(default)]
|
||||
expected_normalized: Option<serde_json::Value>,
|
||||
#[serde(default)]
|
||||
expected_denormalized: Option<serde_json::Value>,
|
||||
#[serde(default)]
|
||||
expected_envelope: Option<serde_json::Value>,
|
||||
#[serde(default)]
|
||||
round_trip: bool,
|
||||
#[serde(default)]
|
||||
expected_round_trip: Option<serde_json::Value>,
|
||||
#[serde(default)]
|
||||
reverse_round_trip: bool,
|
||||
#[serde(default)]
|
||||
expected_reverse_round_trip: Option<serde_json::Value>,
|
||||
#[serde(default)]
|
||||
aliases_json: Option<String>,
|
||||
#[serde(default)]
|
||||
sub_resource_arrays: Option<Vec<String>>,
|
||||
#[serde(default)]
|
||||
resource_type: Option<String>,
|
||||
#[serde(default)]
|
||||
use_registry_api: bool,
|
||||
}
|
||||
|
||||
// ── Helpers ──────────────────────────────────────────────────────────────
|
||||
|
||||
/// Convert a `serde_json::Value` to `regorus::Value`.
|
||||
fn to_regorus(v: &serde_json::Value) -> Value {
|
||||
Value::from(v.clone())
|
||||
}
|
||||
|
||||
fn load_registry(yaml_file: &str, test: &YamlTest) -> Result<Option<AliasRegistry>> {
|
||||
let mut reg = AliasRegistry::new();
|
||||
let mut loaded = false;
|
||||
|
||||
if let Some(ref inline) = test.aliases_json {
|
||||
reg.load_from_json(inline)?;
|
||||
loaded = true;
|
||||
}
|
||||
if let Some(ref relpath) = test.aliases_file {
|
||||
let base = Path::new(yaml_file).parent().unwrap_or(Path::new("."));
|
||||
let path = base.join(relpath);
|
||||
let json = std::fs::read_to_string(&path)?;
|
||||
reg.load_from_json(&json)?;
|
||||
loaded = true;
|
||||
}
|
||||
if let Some(ref inline) = test.data_manifest_json {
|
||||
reg.load_data_policy_manifest_json(inline)?;
|
||||
loaded = true;
|
||||
}
|
||||
if let Some(ref relpath) = test.data_manifest_file {
|
||||
let base = Path::new(yaml_file).parent().unwrap_or(Path::new("."));
|
||||
let path = base.join(relpath);
|
||||
let json = std::fs::read_to_string(&path)?;
|
||||
reg.load_data_policy_manifest_json(&json)?;
|
||||
loaded = true;
|
||||
}
|
||||
|
||||
Ok(if loaded { Some(reg) } else { None })
|
||||
}
|
||||
|
||||
fn case_override_registry(case: &TestCase) -> Result<Option<AliasRegistry>> {
|
||||
if let Some(ref inline) = case.aliases_json {
|
||||
let mut reg = AliasRegistry::new();
|
||||
reg.load_from_json(inline)?;
|
||||
return Ok(Some(reg));
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn resolve_aliases(
|
||||
registry: Option<&AliasRegistry>,
|
||||
case: &TestCase,
|
||||
input: &serde_json::Value,
|
||||
) -> Option<ResolvedAliases> {
|
||||
let resource_type = case
|
||||
.resource_type
|
||||
.clone()
|
||||
.or_else(|| input.get("type").and_then(|v| v.as_str()).map(String::from));
|
||||
|
||||
if let (Some(reg), Some(rt)) = (registry, resource_type.as_deref()) {
|
||||
if let Some(resolved) = reg.get(rt) {
|
||||
let mut r = resolved.clone();
|
||||
if let Some(ref subs) = case.sub_resource_arrays {
|
||||
r.sub_resource_arrays = subs.iter().map(|s| s.to_ascii_lowercase()).collect();
|
||||
}
|
||||
return Some(r);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(ref subs) = case.sub_resource_arrays {
|
||||
return Some(ResolvedAliases {
|
||||
resource_type: resource_type.unwrap_or_default(),
|
||||
entries: Default::default(),
|
||||
sub_resource_arrays: subs.iter().map(|s| s.to_ascii_lowercase()).collect(),
|
||||
default_aggregates: Default::default(),
|
||||
versioned_aggregates: Default::default(),
|
||||
});
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn pretty_regorus(v: &Value) -> String {
|
||||
v.to_json_str().unwrap_or_else(|_| format!("{v:?}"))
|
||||
}
|
||||
|
||||
// ── Runner ───────────────────────────────────────────────────────────────
|
||||
|
||||
fn run_yaml_test(file: &str) -> Result<()> {
|
||||
let yaml_str = std::fs::read_to_string(file)?;
|
||||
let test: YamlTest = serde_yaml::from_str(&yaml_str)?;
|
||||
let file_registry = load_registry(file, &test)?;
|
||||
|
||||
for case in &test.cases {
|
||||
print!(" case: {} … ", case.note);
|
||||
|
||||
let case_override = case_override_registry(case)?;
|
||||
let registry = case_override.as_ref().or(file_registry.as_ref());
|
||||
let resolved = resolve_aliases(registry, case, &case.input);
|
||||
let api_ver = case.api_version.as_deref();
|
||||
let input = to_regorus(&case.input);
|
||||
|
||||
// ── normalize ────────────────────────────────────────────────
|
||||
if let Some(ref expected) = case.expected_normalized {
|
||||
let expected = to_regorus(expected);
|
||||
let actual = if case.use_registry_api {
|
||||
normalizer::normalize(&input, registry, api_ver)
|
||||
} else {
|
||||
normalizer::normalize_with_aliases(&input, resolved.as_ref(), api_ver)
|
||||
};
|
||||
if actual != expected {
|
||||
bail!(
|
||||
"normalize mismatch in '{}':\nexpected:\n{}\nactual:\n{}",
|
||||
case.note,
|
||||
pretty_regorus(&expected),
|
||||
pretty_regorus(&actual),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ── denormalize ──────────────────────────────────────────────
|
||||
if let Some(ref expected) = case.expected_denormalized {
|
||||
let expected = to_regorus(expected);
|
||||
let actual = if case.use_registry_api {
|
||||
denormalizer::denormalize(&input, registry, api_ver)
|
||||
} else {
|
||||
denormalizer::denormalize_with_aliases(&input, resolved.as_ref(), api_ver)
|
||||
};
|
||||
if actual != expected {
|
||||
bail!(
|
||||
"denormalize mismatch in '{}':\nexpected:\n{}\nactual:\n{}",
|
||||
case.note,
|
||||
pretty_regorus(&expected),
|
||||
pretty_regorus(&actual),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ── envelope ─────────────────────────────────────────────────
|
||||
if let Some(ref expected) = case.expected_envelope {
|
||||
let expected = to_regorus(expected);
|
||||
let actual = if case.use_registry_api {
|
||||
if let Some(reg) = registry {
|
||||
reg.normalize_and_wrap(
|
||||
&input,
|
||||
api_ver,
|
||||
case.context.as_ref().map(to_regorus),
|
||||
case.parameters.as_ref().map(to_regorus),
|
||||
)
|
||||
} else {
|
||||
let norm = normalizer::normalize(&input, None, api_ver);
|
||||
normalizer::build_input_envelope(
|
||||
norm,
|
||||
case.context.as_ref().map(to_regorus),
|
||||
case.parameters.as_ref().map(to_regorus),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
let norm = normalizer::normalize_with_aliases(&input, resolved.as_ref(), api_ver);
|
||||
normalizer::build_input_envelope(
|
||||
norm,
|
||||
case.context.as_ref().map(to_regorus),
|
||||
case.parameters.as_ref().map(to_regorus),
|
||||
)
|
||||
};
|
||||
if actual != expected {
|
||||
bail!(
|
||||
"envelope mismatch in '{}':\nexpected:\n{}\nactual:\n{}",
|
||||
case.note,
|
||||
pretty_regorus(&expected),
|
||||
pretty_regorus(&actual),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ── round-trip ───────────────────────────────────────────────
|
||||
if case.round_trip {
|
||||
let (normalized, denormalized) = if case.use_registry_api {
|
||||
let n = normalizer::normalize(&input, registry, api_ver);
|
||||
let d = denormalizer::denormalize(&n, registry, api_ver);
|
||||
(n, d)
|
||||
} else {
|
||||
let n = normalizer::normalize_with_aliases(&input, resolved.as_ref(), api_ver);
|
||||
let d = denormalizer::denormalize_with_aliases(&n, resolved.as_ref(), api_ver);
|
||||
(n, d)
|
||||
};
|
||||
let _ = normalized;
|
||||
if let Some(ref expected) = case.expected_round_trip {
|
||||
let expected = to_regorus(expected);
|
||||
if denormalized != expected {
|
||||
bail!(
|
||||
"round-trip mismatch in '{}':\nexpected:\n{}\nactual:\n{}",
|
||||
case.note,
|
||||
pretty_regorus(&expected),
|
||||
pretty_regorus(&denormalized),
|
||||
);
|
||||
}
|
||||
} else if denormalized != input {
|
||||
bail!(
|
||||
"round-trip mismatch in '{}' (expected original input):\ninput:\n{}\nresult:\n{}",
|
||||
case.note,
|
||||
pretty_regorus(&input),
|
||||
pretty_regorus(&denormalized),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ── reverse round-trip ─────────────────────────────────────
|
||||
if case.reverse_round_trip {
|
||||
let (denormalized, renormalized) = if case.use_registry_api {
|
||||
let d = denormalizer::denormalize(&input, registry, api_ver);
|
||||
let n = normalizer::normalize(&d, registry, api_ver);
|
||||
(d, n)
|
||||
} else {
|
||||
let d = denormalizer::denormalize_with_aliases(&input, resolved.as_ref(), api_ver);
|
||||
let n = normalizer::normalize_with_aliases(&d, resolved.as_ref(), api_ver);
|
||||
(d, n)
|
||||
};
|
||||
let _ = denormalized;
|
||||
if let Some(ref expected) = case.expected_reverse_round_trip {
|
||||
let expected = to_regorus(expected);
|
||||
if renormalized != expected {
|
||||
bail!(
|
||||
"reverse round-trip mismatch in '{}':\nexpected:\n{}\nactual:\n{}",
|
||||
case.note,
|
||||
pretty_regorus(&expected),
|
||||
pretty_regorus(&renormalized),
|
||||
);
|
||||
}
|
||||
} else if renormalized != input {
|
||||
bail!(
|
||||
"reverse round-trip mismatch in '{}' (expected original input):\ninput:\n{}\nresult:\n{}",
|
||||
case.note,
|
||||
pretty_regorus(&input),
|
||||
pretty_regorus(&renormalized),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
println!("ok");
|
||||
}
|
||||
|
||||
println!(" {} cases passed in {file}", test.cases.len());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test_resources("tests/azure_policy/normalization/cases/**/*.yaml")]
|
||||
fn run(path: &str) {
|
||||
run_yaml_test(path).unwrap()
|
||||
}
|
||||
@@ -12,5 +12,8 @@ mod lexer;
|
||||
mod parser;
|
||||
mod value;
|
||||
|
||||
#[cfg(feature = "azure_policy")]
|
||||
mod azure_policy;
|
||||
|
||||
#[cfg(feature = "rvm")]
|
||||
mod rvm;
|
||||
|
||||
Reference in New Issue
Block a user