mirror of
https://github.com/microsoft/regorus.git
synced 2026-08-05 02:16:11 +00:00
Lints are added (deny) at crate level. In each offending file, the failing lints are explicitly allowed. Each file will be fixed in subsequent PRs. Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>
1339 lines
47 KiB
Rust
1339 lines
47 KiB
Rust
// Copyright (c) Microsoft Corporation.
|
|
// Licensed under the MIT License.
|
|
|
|
#![allow(
|
|
clippy::unwrap_used,
|
|
clippy::expect_used,
|
|
clippy::panic,
|
|
clippy::shadow_unrelated,
|
|
clippy::pattern_type_mismatch,
|
|
clippy::assertions_on_result_states,
|
|
clippy::as_conversions
|
|
)] // test cases use unwrap/expect and panic to assert error flows
|
|
|
|
use crate::{
|
|
schema::{error::ValidationError, validate::SchemaValidator, Schema},
|
|
*,
|
|
};
|
|
use serde_json::json;
|
|
|
|
// Helper function to create a schema for Azure Resource types
|
|
fn create_resource_schema() -> Rc<Schema> {
|
|
let schema_json = json!({
|
|
"enum": ["Microsoft.Compute/virtualMachines", "Microsoft.Storage/storageAccounts", "Microsoft.Network/virtualNetworks"],
|
|
"description": "Azure Resource types"
|
|
});
|
|
|
|
let schema = Schema::from_serde_json_value(schema_json).unwrap();
|
|
Rc::new(schema)
|
|
}
|
|
|
|
// Helper function to create a virtual machine resource schema
|
|
fn create_vm_resource_schema() -> Rc<Schema> {
|
|
let schema_json = json!({
|
|
"type": "object",
|
|
"properties": {
|
|
"type": {
|
|
"const": "Microsoft.Compute/virtualMachines"
|
|
},
|
|
"apiVersion": {
|
|
"enum": ["2021-03-01", "2021-07-01", "2022-03-01"]
|
|
},
|
|
"name": {
|
|
"type": "string",
|
|
"pattern": "^[a-zA-Z0-9-._]{1,64}$"
|
|
},
|
|
"location": {
|
|
"type": "string",
|
|
"description": "Azure region where the VM will be deployed"
|
|
},
|
|
"properties": {
|
|
"type": "object",
|
|
"properties": {
|
|
"hardwareProfile": {
|
|
"type": "object",
|
|
"properties": {
|
|
"vmSize": {
|
|
"enum": ["Standard_B1s", "Standard_B2s", "Standard_D2s_v3", "Standard_D4s_v3"]
|
|
}
|
|
},
|
|
"required": ["vmSize"]
|
|
},
|
|
"osProfile": {
|
|
"type": "object",
|
|
"properties": {
|
|
"computerName": {
|
|
"type": "string"
|
|
},
|
|
"adminUsername": {
|
|
"type": "string"
|
|
}
|
|
},
|
|
"required": ["computerName", "adminUsername"]
|
|
}
|
|
},
|
|
"required": ["hardwareProfile", "osProfile"]
|
|
}
|
|
},
|
|
"required": ["type", "apiVersion", "name", "location", "properties"],
|
|
"description": "Schema for Azure Virtual Machine resources"
|
|
});
|
|
|
|
let schema = Schema::from_serde_json_value(schema_json).unwrap();
|
|
Rc::new(schema)
|
|
}
|
|
|
|
// Helper function to create a storage account resource schema
|
|
fn create_storage_resource_schema() -> Rc<Schema> {
|
|
let schema_json = json!({
|
|
"type": "object",
|
|
"properties": {
|
|
"type": {
|
|
"const": "Microsoft.Storage/storageAccounts"
|
|
},
|
|
"apiVersion": {
|
|
"enum": ["2021-04-01", "2021-06-01", "2022-05-01"]
|
|
},
|
|
"name": {
|
|
"type": "string",
|
|
"pattern": "^[a-z0-9]{3,24}$"
|
|
},
|
|
"location": {
|
|
"type": "string",
|
|
"description": "Azure region for the storage account"
|
|
},
|
|
"sku": {
|
|
"type": "object",
|
|
"properties": {
|
|
"name": {
|
|
"enum": ["Standard_LRS", "Standard_GRS", "Standard_RAGRS", "Premium_LRS"]
|
|
}
|
|
},
|
|
"required": ["name"]
|
|
},
|
|
"kind": {
|
|
"enum": ["Storage", "StorageV2", "BlobStorage", "FileStorage", "BlockBlobStorage"]
|
|
},
|
|
"properties": {
|
|
"type": "object",
|
|
"properties": {
|
|
"accessTier": {
|
|
"enum": ["Hot", "Cool"]
|
|
},
|
|
"encryption": {
|
|
"type": "object",
|
|
"properties": {
|
|
"services": {
|
|
"type": "object"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"required": ["type", "apiVersion", "name", "location", "sku", "kind"],
|
|
"description": "Schema for Azure Storage Account resources"
|
|
});
|
|
|
|
let schema = Schema::from_serde_json_value(schema_json).unwrap();
|
|
Rc::new(schema)
|
|
}
|
|
|
|
// Helper function to create a network resource schema
|
|
fn create_network_resource_schema() -> Rc<Schema> {
|
|
let schema_json = json!({
|
|
"type": "object",
|
|
"properties": {
|
|
"type": {
|
|
"const": "Microsoft.Network/virtualNetworks"
|
|
},
|
|
"apiVersion": {
|
|
"enum": ["2020-11-01", "2021-02-01", "2021-05-01"]
|
|
},
|
|
"name": {
|
|
"type": "string",
|
|
"pattern": "^[a-zA-Z0-9-._]{2,64}$"
|
|
},
|
|
"location": {
|
|
"type": "string",
|
|
"description": "Azure region for the virtual network"
|
|
},
|
|
"properties": {
|
|
"type": "object",
|
|
"properties": {
|
|
"addressSpace": {
|
|
"type": "object",
|
|
"properties": {
|
|
"addressPrefixes": {
|
|
"type": "array",
|
|
"items": {
|
|
"type": "string",
|
|
"pattern": "^(?:[0-9]{1,3}\\.){3}[0-9]{1,3}/[0-9]{1,2}$"
|
|
}
|
|
}
|
|
},
|
|
"required": ["addressPrefixes"]
|
|
},
|
|
"subnets": {
|
|
"type": "array",
|
|
"items": {
|
|
"type": "object",
|
|
"properties": {
|
|
"name": {
|
|
"type": "string"
|
|
},
|
|
"properties": {
|
|
"type": "object",
|
|
"properties": {
|
|
"addressPrefix": {
|
|
"type": "string",
|
|
"pattern": "^(?:[0-9]{1,3}\\.){3}[0-9]{1,3}/[0-9]{1,2}$"
|
|
}
|
|
},
|
|
"required": ["addressPrefix"]
|
|
}
|
|
},
|
|
"required": ["name", "properties"]
|
|
}
|
|
}
|
|
},
|
|
"required": ["addressSpace"]
|
|
}
|
|
},
|
|
"required": ["type", "apiVersion", "name", "location", "properties"],
|
|
"description": "Schema for Azure Virtual Network resources"
|
|
});
|
|
|
|
let schema = Schema::from_serde_json_value(schema_json).unwrap();
|
|
Rc::new(schema)
|
|
}
|
|
|
|
// Schema validation tests for Azure Resource schemas
|
|
|
|
#[test]
|
|
fn test_validate_vm_resource_valid() {
|
|
let schema = create_vm_resource_schema();
|
|
|
|
let valid_vm_data = json!({
|
|
"type": "Microsoft.Compute/virtualMachines",
|
|
"apiVersion": "2021-03-01",
|
|
"name": "my-vm-01",
|
|
"location": "eastus",
|
|
"properties": {
|
|
"hardwareProfile": {
|
|
"vmSize": "Standard_B2s"
|
|
},
|
|
"osProfile": {
|
|
"computerName": "my-computer",
|
|
"adminUsername": "azureuser"
|
|
}
|
|
}
|
|
});
|
|
|
|
let value = Value::from(valid_vm_data);
|
|
let result = SchemaValidator::validate(&value, &schema);
|
|
assert!(result.is_ok());
|
|
}
|
|
|
|
#[test]
|
|
fn test_validate_vm_resource_missing_required() {
|
|
let schema = create_vm_resource_schema();
|
|
|
|
let invalid_vm_data = json!({
|
|
"type": "Microsoft.Compute/virtualMachines",
|
|
"apiVersion": "2021-03-01",
|
|
"name": "my-vm-01"
|
|
// Missing location and properties
|
|
});
|
|
|
|
let value = Value::from(invalid_vm_data);
|
|
let result = SchemaValidator::validate(&value, &schema);
|
|
assert!(result.is_err());
|
|
|
|
match result.unwrap_err() {
|
|
ValidationError::MissingRequiredProperty { property, .. } => {
|
|
// Should be missing either location or properties
|
|
assert!(property == "location".into() || property == "properties".into());
|
|
}
|
|
other => panic!("Expected MissingRequiredProperty error, got: {:?}", other),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_validate_vm_resource_invalid_vm_size() {
|
|
let schema = create_vm_resource_schema();
|
|
|
|
let invalid_vm_data = json!({
|
|
"type": "Microsoft.Compute/virtualMachines",
|
|
"apiVersion": "2021-03-01",
|
|
"name": "my-vm-01",
|
|
"location": "eastus",
|
|
"properties": {
|
|
"hardwareProfile": {
|
|
"vmSize": "InvalidSize"
|
|
},
|
|
"osProfile": {
|
|
"computerName": "my-computer",
|
|
"adminUsername": "azureuser"
|
|
}
|
|
}
|
|
});
|
|
|
|
let value = Value::from(invalid_vm_data);
|
|
let result = SchemaValidator::validate(&value, &schema);
|
|
assert!(result.is_err());
|
|
|
|
match result.unwrap_err() {
|
|
ValidationError::PropertyValidationFailed {
|
|
property, error, ..
|
|
} => {
|
|
assert_eq!(property, "properties".into());
|
|
match error.as_ref() {
|
|
ValidationError::PropertyValidationFailed {
|
|
property: inner_prop,
|
|
error: inner_error,
|
|
..
|
|
} => {
|
|
assert_eq!(*inner_prop, "hardwareProfile".into());
|
|
match inner_error.as_ref() {
|
|
ValidationError::PropertyValidationFailed {
|
|
property: vm_size_prop,
|
|
error: vm_size_error,
|
|
..
|
|
} => {
|
|
assert_eq!(*vm_size_prop, "vmSize".into());
|
|
match vm_size_error.as_ref() {
|
|
ValidationError::NotInEnum { .. } => {
|
|
// Expected deeply nested error structure
|
|
}
|
|
other => {
|
|
panic!("Expected NotInEnum error for vmSize, got: {:?}", other)
|
|
}
|
|
}
|
|
}
|
|
other => panic!(
|
|
"Expected PropertyValidationFailed for vmSize, got: {:?}",
|
|
other
|
|
),
|
|
}
|
|
}
|
|
other => panic!(
|
|
"Expected PropertyValidationFailed for hardwareProfile, got: {:?}",
|
|
other
|
|
),
|
|
}
|
|
}
|
|
other => panic!("Expected PropertyValidationFailed error, got: {:?}", other),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_validate_storage_resource_valid() {
|
|
let schema = create_storage_resource_schema();
|
|
|
|
let valid_storage_data = json!({
|
|
"type": "Microsoft.Storage/storageAccounts",
|
|
"apiVersion": "2021-04-01",
|
|
"name": "mystorageaccount001",
|
|
"location": "westus2",
|
|
"sku": {
|
|
"name": "Standard_LRS"
|
|
},
|
|
"kind": "StorageV2",
|
|
"properties": {
|
|
"accessTier": "Hot",
|
|
"encryption": {
|
|
"services": {}
|
|
}
|
|
}
|
|
});
|
|
|
|
let value = Value::from(valid_storage_data);
|
|
let result = SchemaValidator::validate(&value, &schema);
|
|
assert!(result.is_ok());
|
|
}
|
|
|
|
#[test]
|
|
fn test_validate_storage_resource_invalid_name() {
|
|
let schema = create_storage_resource_schema();
|
|
|
|
let invalid_storage_data = json!({
|
|
"type": "Microsoft.Storage/storageAccounts",
|
|
"apiVersion": "2021-04-01",
|
|
"name": "Invalid-Storage-Name-With-Caps-And-Dashes",
|
|
"location": "westus2",
|
|
"sku": {
|
|
"name": "Standard_LRS"
|
|
},
|
|
"kind": "StorageV2"
|
|
});
|
|
|
|
let value = Value::from(invalid_storage_data);
|
|
let result = SchemaValidator::validate(&value, &schema);
|
|
assert!(result.is_err());
|
|
|
|
match result.unwrap_err() {
|
|
ValidationError::PropertyValidationFailed {
|
|
property, error, ..
|
|
} => {
|
|
assert_eq!(property, "name".into());
|
|
match error.as_ref() {
|
|
ValidationError::PatternMismatch { .. } => {
|
|
// Expected pattern mismatch for storage account name
|
|
}
|
|
other => panic!("Expected PatternMismatch error for name, got: {:?}", other),
|
|
}
|
|
}
|
|
other => panic!("Expected PropertyValidationFailed error, got: {:?}", other),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_validate_storage_resource_invalid_sku() {
|
|
let schema = create_storage_resource_schema();
|
|
|
|
let invalid_storage_data = json!({
|
|
"type": "Microsoft.Storage/storageAccounts",
|
|
"apiVersion": "2021-04-01",
|
|
"name": "mystorageaccount001",
|
|
"location": "westus2",
|
|
"sku": {
|
|
"name": "Invalid_SKU"
|
|
},
|
|
"kind": "StorageV2"
|
|
});
|
|
|
|
let value = Value::from(invalid_storage_data);
|
|
let result = SchemaValidator::validate(&value, &schema);
|
|
assert!(result.is_err());
|
|
|
|
match result.unwrap_err() {
|
|
ValidationError::PropertyValidationFailed {
|
|
property, error, ..
|
|
} => {
|
|
assert_eq!(property, "sku".into());
|
|
match error.as_ref() {
|
|
ValidationError::PropertyValidationFailed {
|
|
property: sku_prop,
|
|
error: sku_error,
|
|
..
|
|
} => {
|
|
assert_eq!(*sku_prop, "name".into());
|
|
match sku_error.as_ref() {
|
|
ValidationError::NotInEnum { .. } => {
|
|
// Expected enum validation error
|
|
}
|
|
other => panic!("Expected NotInEnum error for sku name, got: {:?}", other),
|
|
}
|
|
}
|
|
other => panic!(
|
|
"Expected PropertyValidationFailed for sku name, got: {:?}",
|
|
other
|
|
),
|
|
}
|
|
}
|
|
other => panic!("Expected PropertyValidationFailed error, got: {:?}", other),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_validate_network_resource_valid() {
|
|
let schema = create_network_resource_schema();
|
|
|
|
let valid_network_data = json!({
|
|
"type": "Microsoft.Network/virtualNetworks",
|
|
"apiVersion": "2021-02-01",
|
|
"name": "my-vnet",
|
|
"location": "eastus",
|
|
"properties": {
|
|
"addressSpace": {
|
|
"addressPrefixes": ["10.0.0.0/16"]
|
|
},
|
|
"subnets": [
|
|
{
|
|
"name": "default",
|
|
"properties": {
|
|
"addressPrefix": "10.0.1.0/24"
|
|
}
|
|
}
|
|
]
|
|
}
|
|
});
|
|
|
|
let value = Value::from(valid_network_data);
|
|
let result = SchemaValidator::validate(&value, &schema);
|
|
assert!(result.is_ok());
|
|
}
|
|
|
|
#[test]
|
|
fn test_validate_network_resource_invalid_address_prefix() {
|
|
let schema = create_network_resource_schema();
|
|
|
|
let invalid_network_data = json!({
|
|
"type": "Microsoft.Network/virtualNetworks",
|
|
"apiVersion": "2021-02-01",
|
|
"name": "my-vnet",
|
|
"location": "eastus",
|
|
"properties": {
|
|
"addressSpace": {
|
|
"addressPrefixes": ["invalid-cidr"]
|
|
}
|
|
}
|
|
});
|
|
|
|
let value = Value::from(invalid_network_data);
|
|
let result = SchemaValidator::validate(&value, &schema);
|
|
assert!(result.is_err());
|
|
|
|
match result.unwrap_err() {
|
|
ValidationError::PropertyValidationFailed {
|
|
property, error, ..
|
|
} => {
|
|
assert_eq!(property, "properties".into());
|
|
match error.as_ref() {
|
|
ValidationError::PropertyValidationFailed {
|
|
property: addr_prop,
|
|
..
|
|
} => {
|
|
assert_eq!(*addr_prop, "addressSpace".into());
|
|
// Continue checking nested structure for array validation
|
|
}
|
|
other => panic!(
|
|
"Expected PropertyValidationFailed for addressSpace, got: {:?}",
|
|
other
|
|
),
|
|
}
|
|
}
|
|
other => panic!("Expected PropertyValidationFailed error, got: {:?}", other),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_validate_basic_resource_enum() {
|
|
let schema = create_resource_schema();
|
|
|
|
// Test all valid resource types
|
|
let valid_types = [
|
|
"Microsoft.Compute/virtualMachines",
|
|
"Microsoft.Storage/storageAccounts",
|
|
"Microsoft.Network/virtualNetworks",
|
|
];
|
|
|
|
for resource_type in valid_types {
|
|
let value = Value::from(resource_type);
|
|
let result = SchemaValidator::validate(&value, &schema);
|
|
assert!(
|
|
result.is_ok(),
|
|
"Resource type '{resource_type}' should be valid"
|
|
);
|
|
}
|
|
|
|
// Test invalid resource type
|
|
let invalid_value = Value::from("Microsoft.Invalid/resourceType");
|
|
let result = SchemaValidator::validate(&invalid_value, &schema);
|
|
assert!(result.is_err());
|
|
|
|
match result.unwrap_err() {
|
|
ValidationError::NotInEnum { .. } => {
|
|
// Expected error type
|
|
}
|
|
other => panic!("Expected NotInEnum error, got: {:?}", other),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_validate_complex_arm_template() {
|
|
let complex_schema_json = json!({
|
|
"type": "object",
|
|
"properties": {
|
|
"$schema": {
|
|
"type": "string"
|
|
},
|
|
"contentVersion": {
|
|
"type": "string"
|
|
},
|
|
"parameters": {
|
|
"type": "object",
|
|
"additionalProperties": { "type": "any" }
|
|
},
|
|
"variables": {
|
|
"type": "object",
|
|
"additionalProperties": { "type": "any" }
|
|
},
|
|
"resources": {
|
|
"type": "array",
|
|
"items": {
|
|
"type": "object",
|
|
"properties": {
|
|
"type": {
|
|
"type": "string"
|
|
},
|
|
"apiVersion": {
|
|
"type": "string"
|
|
},
|
|
"name": {
|
|
"type": "string"
|
|
},
|
|
"location": {
|
|
"type": "string"
|
|
},
|
|
"properties": {
|
|
"type": "object",
|
|
"additionalProperties": { "type": "any" }
|
|
},
|
|
"tags": {
|
|
"type": "object",
|
|
"additionalProperties": { "type": "string" }
|
|
}
|
|
},
|
|
"required": ["type", "apiVersion", "name"],
|
|
"additionalProperties": { "type": "any" }
|
|
}
|
|
},
|
|
"outputs": {
|
|
"type": "object",
|
|
"additionalProperties": { "type": "any" }
|
|
}
|
|
},
|
|
"required": ["resources"],
|
|
"additionalProperties": { "type": "any" }
|
|
});
|
|
|
|
let schema = Schema::from_serde_json_value(complex_schema_json).unwrap();
|
|
|
|
let valid_template_data = json!({
|
|
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
|
|
"contentVersion": "1.0.0.0",
|
|
"parameters": {
|
|
"vmName": {
|
|
"type": "string",
|
|
"defaultValue": "myVM"
|
|
}
|
|
},
|
|
"variables": {
|
|
"storageAccountName": "[concat('storage', uniqueString(resourceGroup().id))]"
|
|
},
|
|
"resources": [
|
|
{
|
|
"type": "Microsoft.Compute/virtualMachines",
|
|
"apiVersion": "2021-03-01",
|
|
"name": "[parameters('vmName')]",
|
|
"location": "[resourceGroup().location]",
|
|
"properties": {
|
|
"hardwareProfile": {
|
|
"vmSize": "Standard_B1s"
|
|
}
|
|
},
|
|
"tags": {
|
|
"environment": "dev",
|
|
"project": "test"
|
|
}
|
|
}
|
|
],
|
|
"outputs": {
|
|
"vmId": {
|
|
"type": "string",
|
|
"value": "[resourceId('Microsoft.Compute/virtualMachines', parameters('vmName'))]"
|
|
}
|
|
}
|
|
});
|
|
|
|
let value = Value::from(valid_template_data);
|
|
let result = SchemaValidator::validate(&value, &schema);
|
|
assert!(result.is_ok());
|
|
}
|
|
|
|
#[test]
|
|
fn test_validate_resource_type_mismatch() {
|
|
let schema = create_vm_resource_schema();
|
|
|
|
// Pass a non-object value to object schema
|
|
let invalid_data = Value::from("not an object");
|
|
let result = SchemaValidator::validate(&invalid_data, &schema);
|
|
assert!(result.is_err());
|
|
|
|
match result.unwrap_err() {
|
|
ValidationError::TypeMismatch {
|
|
expected, actual, ..
|
|
} => {
|
|
assert_eq!(expected, "object".into());
|
|
assert_eq!(actual, "string".into());
|
|
}
|
|
other => panic!("Expected TypeMismatch error, got: {:?}", other),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_complex_nested_azure_template_validation() {
|
|
// Create a complex ARM template schema with deeply nested properties
|
|
let complex_schema = json!({
|
|
"type": "object",
|
|
"properties": {
|
|
"$schema": { "type": "string" },
|
|
"contentVersion": { "type": "string", "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+\\.[0-9]+$" },
|
|
"metadata": {
|
|
"type": "object",
|
|
"properties": {
|
|
"description": { "type": "string" },
|
|
"author": { "type": "string" },
|
|
"tags": {
|
|
"type": "object",
|
|
"additionalProperties": { "type": "string" }
|
|
}
|
|
}
|
|
},
|
|
"parameters": {
|
|
"type": "object",
|
|
"additionalProperties": {
|
|
"type": "object",
|
|
"properties": {
|
|
"type": { "enum": ["string", "int", "bool", "array", "object"] },
|
|
"defaultValue": { "type": "any" },
|
|
"allowedValues": { "type": "array", "items": { "type": "any" } },
|
|
"minValue": { "type": "number" },
|
|
"maxValue": { "type": "number" },
|
|
"minLength": { "type": "integer" },
|
|
"maxLength": { "type": "integer" },
|
|
"metadata": {
|
|
"type": "object",
|
|
"properties": {
|
|
"description": { "type": "string" },
|
|
"strongType": { "type": "string" }
|
|
}
|
|
}
|
|
},
|
|
"required": ["type"]
|
|
}
|
|
},
|
|
"variables": { "type": "object" },
|
|
"resources": {
|
|
"type": "array",
|
|
"items": {
|
|
"type": "object",
|
|
"properties": {
|
|
"type": { "type": "string" },
|
|
"apiVersion": { "type": "string" },
|
|
"name": { "type": "string" },
|
|
"location": { "type": "string" },
|
|
"dependsOn": {
|
|
"type": "array",
|
|
"items": { "type": "string" }
|
|
},
|
|
"condition": { "type": "boolean" },
|
|
"copy": {
|
|
"type": "object",
|
|
"properties": {
|
|
"name": { "type": "string" },
|
|
"count": { "type": "integer", "minimum": 1, "maximum": 800 },
|
|
"mode": { "enum": ["Parallel", "Serial"] },
|
|
"batchSize": { "type": "integer", "minimum": 1 }
|
|
},
|
|
"required": ["name", "count"]
|
|
},
|
|
"properties": { "type": "object" },
|
|
"tags": {
|
|
"type": "object",
|
|
"additionalProperties": { "type": "string" }
|
|
}
|
|
},
|
|
"required": ["type", "apiVersion", "name"]
|
|
}
|
|
},
|
|
"outputs": {
|
|
"type": "object",
|
|
"additionalProperties": {
|
|
"type": "object",
|
|
"properties": {
|
|
"type": { "enum": ["string", "int", "bool", "array", "object"] },
|
|
"value": { "type": "any" },
|
|
"condition": { "type": "boolean" },
|
|
"copy": {
|
|
"type": "object",
|
|
"properties": {
|
|
"count": { "type": "integer", "minimum": 1 },
|
|
"input": { "type": "any" }
|
|
},
|
|
"required": ["count", "input"]
|
|
}
|
|
},
|
|
"required": ["type", "value"]
|
|
}
|
|
}
|
|
},
|
|
"required": ["$schema", "contentVersion", "resources"]
|
|
});
|
|
|
|
let schema = Schema::from_serde_json_value(complex_schema).unwrap();
|
|
|
|
// Valid complex template with nested copy loops and conditions
|
|
let valid_template = json!({
|
|
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
|
|
"contentVersion": "1.2.3.4",
|
|
"metadata": {
|
|
"description": "Complex deployment with copy loops and conditions",
|
|
"author": "Azure DevOps Team",
|
|
"tags": {
|
|
"environment": "production",
|
|
"cost-center": "engineering",
|
|
"department": "cloud-infrastructure"
|
|
}
|
|
},
|
|
"parameters": {
|
|
"vmCount": {
|
|
"type": "int",
|
|
"defaultValue": 3,
|
|
"minValue": 1,
|
|
"maxValue": 10,
|
|
"metadata": {
|
|
"description": "Number of VMs to deploy",
|
|
"strongType": "Microsoft.Compute/SKUs"
|
|
}
|
|
},
|
|
"environment": {
|
|
"type": "string",
|
|
"defaultValue": "dev",
|
|
"allowedValues": ["dev", "test", "staging", "prod"],
|
|
"metadata": {
|
|
"description": "Environment name for resource naming"
|
|
}
|
|
},
|
|
"enableMonitoring": {
|
|
"type": "bool",
|
|
"defaultValue": true,
|
|
"metadata": {
|
|
"description": "Whether to enable monitoring extensions"
|
|
}
|
|
}
|
|
},
|
|
"variables": {
|
|
"vmPrefix": "[concat(parameters('environment'), '-vm-')]",
|
|
"storageAccountName": "[concat('storage', uniqueString(resourceGroup().id))]"
|
|
},
|
|
"resources": [
|
|
{
|
|
"type": "Microsoft.Storage/storageAccounts",
|
|
"apiVersion": "2021-04-01",
|
|
"name": "[variables('storageAccountName')]",
|
|
"location": "eastus",
|
|
"properties": {
|
|
"accountType": "Standard_LRS"
|
|
},
|
|
"tags": {
|
|
"purpose": "vm-diagnostics",
|
|
"environment": "[parameters('environment')]"
|
|
}
|
|
},
|
|
{
|
|
"type": "Microsoft.Compute/virtualMachines",
|
|
"apiVersion": "2021-03-01",
|
|
"name": "[concat(variables('vmPrefix'), copyIndex(1))]",
|
|
"location": "eastus",
|
|
"condition": true,
|
|
"dependsOn": [
|
|
"[resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName'))]"
|
|
],
|
|
"copy": {
|
|
"name": "vmLoop",
|
|
"count": 5,
|
|
"mode": "Parallel",
|
|
"batchSize": 2
|
|
},
|
|
"properties": {
|
|
"hardwareProfile": {
|
|
"vmSize": "Standard_B2s"
|
|
}
|
|
},
|
|
"tags": {
|
|
"environment": "[parameters('environment')]",
|
|
"vm-index": "[string(copyIndex())]"
|
|
}
|
|
}
|
|
],
|
|
"outputs": {
|
|
"storageAccountId": {
|
|
"type": "string",
|
|
"value": "[resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName'))]"
|
|
},
|
|
"vmIds": {
|
|
"type": "array",
|
|
"copy": {
|
|
"count": 5,
|
|
"input": "[resourceId('Microsoft.Compute/virtualMachines', concat(variables('vmPrefix'), copyIndex(1)))]"
|
|
},
|
|
"value" : "[resourceId('Microsoft.Compute/virtualMachines', concat(variables('vmPrefix'), copyIndex(1)))]"
|
|
}
|
|
}
|
|
});
|
|
|
|
let value = Value::from(valid_template);
|
|
let result = SchemaValidator::validate(&value, &schema);
|
|
|
|
assert!(
|
|
result.is_ok(),
|
|
"Complex valid template should pass validation"
|
|
);
|
|
|
|
// Test invalid template with constraint violations
|
|
let invalid_template = json!({
|
|
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
|
|
"contentVersion": "invalid-version", // Should match pattern
|
|
"resources": [
|
|
{
|
|
"type": "Microsoft.Compute/virtualMachines",
|
|
"apiVersion": "2021-03-01",
|
|
"name": "test-vm",
|
|
"copy": {
|
|
"name": "vmLoop",
|
|
"count": 1000 // Exceeds maximum of 800
|
|
}
|
|
}
|
|
]
|
|
});
|
|
|
|
let value = Value::from(invalid_template);
|
|
let result = SchemaValidator::validate(&value, &schema);
|
|
assert!(
|
|
result.is_err(),
|
|
"Template with constraint violations should fail"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_deep_nesting_and_recursive_structures() {
|
|
// Schema with moderate nesting (5 levels) to avoid macro recursion limits
|
|
let deep_schema = json!({
|
|
"type": "object",
|
|
"properties": {
|
|
"level1": {
|
|
"type": "object",
|
|
"properties": {
|
|
"level2": {
|
|
"type": "object",
|
|
"properties": {
|
|
"level3": {
|
|
"type": "object",
|
|
"properties": {
|
|
"level4": {
|
|
"type": "object",
|
|
"properties": {
|
|
"level5": {
|
|
"type": "object",
|
|
"properties": {
|
|
"deepValue": {
|
|
"type": "string",
|
|
"pattern": "^deep-[0-9]+$"
|
|
},
|
|
"recursiveArray": {
|
|
"type": "array",
|
|
"items": {
|
|
"type": "object",
|
|
"properties": {
|
|
"nested": {
|
|
"type": "object",
|
|
"properties": {
|
|
"value": { "type": "number" },
|
|
"metadata": {
|
|
"type": "object",
|
|
"additionalProperties": { "type": "string" }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"required": ["deepValue"]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
"required": ["level1"]
|
|
});
|
|
|
|
let schema = Schema::from_serde_json_value(deep_schema).unwrap();
|
|
|
|
// Valid deeply nested structure
|
|
let valid_deep_data = json!({
|
|
"level1": {
|
|
"level2": {
|
|
"level3": {
|
|
"level4": {
|
|
"level5": {
|
|
"deepValue": "deep-12345",
|
|
"recursiveArray": [
|
|
{
|
|
"nested": {
|
|
"value": 42.5,
|
|
"metadata": {
|
|
"type": "numeric",
|
|
"unit": "percentage",
|
|
"source": "sensor-1"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"nested": {
|
|
"value": 15.3,
|
|
"metadata": {
|
|
"type": "numeric",
|
|
"unit": "temperature",
|
|
"source": "sensor-2"
|
|
}
|
|
}
|
|
}
|
|
]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
let value = Value::from(valid_deep_data);
|
|
let result = SchemaValidator::validate(&value, &schema);
|
|
assert!(result.is_ok(), "Valid deeply nested structure should pass");
|
|
|
|
// Invalid - missing required deepValue
|
|
let invalid_deep_data = json!({
|
|
"level1": {
|
|
"level2": {
|
|
"level3": {
|
|
"level4": {
|
|
"level5": {
|
|
// Missing required "deepValue"
|
|
"recursiveArray": []
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
let value = Value::from(invalid_deep_data);
|
|
let result = SchemaValidator::validate(&value, &schema);
|
|
assert!(
|
|
result.is_err(),
|
|
"Structure missing required deep field should fail"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_unicode_and_internationalization_validation() {
|
|
// Schema supporting international characters and Unicode
|
|
let unicode_schema = json!({
|
|
"type": "object",
|
|
"properties": {
|
|
"names": {
|
|
"type": "object",
|
|
"properties": {
|
|
"chinese": { "type": "string", "pattern": "^[\\u4e00-\\u9fff]+$" },
|
|
"russian": { "type": "string", "pattern": "^[\\u0400-\\u04ff]+$" },
|
|
"japanese": { "type": "string", "pattern": "^[\\u3040-\\u309f\\u30a0-\\u30ff\\u4e00-\\u9fff]+$" },
|
|
"korean": { "type": "string", "pattern": "^[\\uac00-\\ud7af]+$" },
|
|
"hindi": { "type": "string", "pattern": "^[\\u0900-\\u097f]+$" },
|
|
"french": { "type": "string", "pattern": "^[a-zA-ZàâäéèêëïîôöùûüÿñæœÀÂÄÉÈÊËÏÎÔÖÙÛÜŸÑÆŒ\\s-]+$" }
|
|
}
|
|
},
|
|
"descriptions": {
|
|
"type": "object",
|
|
"additionalProperties": {
|
|
"type": "string",
|
|
"minLength": 1,
|
|
"maxLength": 1000
|
|
}
|
|
},
|
|
"metadata": {
|
|
"type": "object",
|
|
"properties": {
|
|
"encoding": { "enum": ["UTF-8", "UTF-16", "UTF-32"] },
|
|
"locale": { "type": "string", "pattern": "^[a-z]{2}-[A-Z]{2}$" },
|
|
"timezone": { "type": "string" }
|
|
}
|
|
}
|
|
},
|
|
"required": ["names", "metadata"]
|
|
});
|
|
|
|
let schema = Schema::from_serde_json_value(unicode_schema).unwrap();
|
|
|
|
// Valid international data
|
|
let valid_unicode_data = json!({
|
|
"names": {
|
|
"chinese": "你好世界",
|
|
"russian": "привет",
|
|
"japanese": "こんにちは世界",
|
|
"korean": "안녕하세요",
|
|
"hindi": "नमस्ते",
|
|
"french": "Bonjour le Monde"
|
|
},
|
|
"descriptions": {
|
|
"en-US": "Hello World application for international users",
|
|
"zh-CN": "面向国际用户的你好世界应用程序",
|
|
"ru-RU": "Приложение Hello World для международных пользователей",
|
|
"ja-JP": "国際ユーザー向けのHello Worldアプリケーション",
|
|
"ko-KR": "국제 사용자를 위한 Hello World 애플리케이션",
|
|
"hi-IN": "अंतर्राष्ट्रीय उपयोगकर्ताओं के लिए हैलो वर्ल्ड एप्लिकेशन",
|
|
"fr-FR": "Application Hello World pour les utilisateurs internationaux"
|
|
},
|
|
"metadata": {
|
|
"encoding": "UTF-8",
|
|
"locale": "en-US",
|
|
"timezone": "UTC"
|
|
}
|
|
});
|
|
|
|
let value = Value::from(valid_unicode_data);
|
|
let result = SchemaValidator::validate(&value, &schema);
|
|
assert!(result.is_ok(), "Valid Unicode data should pass validation");
|
|
|
|
// Invalid - non-matching Unicode patterns
|
|
let invalid_unicode_data = json!({
|
|
"names": {
|
|
"chinese": "Hello", // Should be Chinese characters
|
|
"russian": "Goodbye", // Should be Russian characters
|
|
"japanese": "Test", // Should be Japanese characters
|
|
"korean": "Invalid", // Should be Korean characters
|
|
"hindi": "Wrong", // Should be Hindi characters
|
|
"french": "123456" // Should be French text
|
|
},
|
|
"metadata": {
|
|
"encoding": "UTF-8",
|
|
"locale": "invalid-locale", // Should match pattern
|
|
"timezone": "UTC"
|
|
}
|
|
});
|
|
|
|
let value = Value::from(invalid_unicode_data);
|
|
let result = SchemaValidator::validate(&value, &schema);
|
|
assert!(
|
|
result.is_err(),
|
|
"Invalid Unicode patterns should fail validation"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_edge_cases_and_boundary_conditions() {
|
|
// Schema with strict boundary conditions
|
|
let boundary_schema = json!({
|
|
"type": "object",
|
|
"properties": {
|
|
"strings": {
|
|
"type": "object",
|
|
"properties": {
|
|
"empty": { "type": "string", "minLength": 0, "maxLength": 0 },
|
|
"single": { "type": "string", "minLength": 1, "maxLength": 1 },
|
|
"exact_length": { "type": "string", "minLength": 10, "maxLength": 10 },
|
|
"very_long": { "type": "string", "maxLength": 10000 }
|
|
}
|
|
},
|
|
"numbers": {
|
|
"type": "object",
|
|
"properties": {
|
|
"zero": { "type": "number", "minimum": 0, "maximum": 0 },
|
|
"negative": { "type": "number", "minimum": -1000, "maximum": -1 },
|
|
"positive": { "type": "number", "minimum": 1, "maximum": 1000 },
|
|
"float_precision": { "type": "number" }
|
|
}
|
|
},
|
|
"arrays": {
|
|
"type": "object",
|
|
"properties": {
|
|
"empty": { "type": "array", "minItems": 0, "maxItems": 0, "items": { "type": "string" } },
|
|
"single_item": { "type": "array", "minItems": 1, "maxItems": 1, "items": { "type": "string" } },
|
|
"exact_size": { "type": "array", "minItems": 5, "maxItems": 5, "items": { "type": "integer" } },
|
|
"large_array": { "type": "array", "maxItems": 1000, "items": { "type": "boolean" } }
|
|
}
|
|
},
|
|
"objects": {
|
|
"type": "object",
|
|
"properties": {
|
|
"empty": { "type": "object", "additionalProperties": false },
|
|
"single_prop": {
|
|
"type": "object",
|
|
"properties": { "only": { "type": "string" } },
|
|
"additionalProperties": false,
|
|
"required": ["only"]
|
|
}
|
|
}
|
|
},
|
|
"nulls_and_optionals": {
|
|
"type": "object",
|
|
"properties": {
|
|
"nullable": { "type": "string" },
|
|
"optional": { "type": "string" },
|
|
"required_null": { "type": "null" }
|
|
},
|
|
"required": ["required_null"]
|
|
}
|
|
},
|
|
"required": ["strings", "numbers", "arrays", "objects", "nulls_and_optionals"]
|
|
});
|
|
|
|
let schema = Schema::from_serde_json_value(boundary_schema).unwrap();
|
|
|
|
// Valid boundary condition data
|
|
let valid_boundary_data = json!({
|
|
"strings": {
|
|
"empty": "",
|
|
"single": "a",
|
|
"exact_length": "exactly_10",
|
|
"very_long": "a".repeat(9999)
|
|
},
|
|
"numbers": {
|
|
"zero": 0,
|
|
"negative": -500,
|
|
"positive": 250,
|
|
"float_precision": 123.45
|
|
},
|
|
"arrays": {
|
|
"empty": [],
|
|
"single_item": ["test"],
|
|
"exact_size": [1, 2, 3, 4, 5],
|
|
"large_array": vec![true; 500]
|
|
},
|
|
"objects": {
|
|
"empty": {},
|
|
"single_prop": {
|
|
"only": "value"
|
|
}
|
|
},
|
|
"nulls_and_optionals": {
|
|
"nullable": "string_value",
|
|
"optional": "present",
|
|
"required_null": null
|
|
}
|
|
});
|
|
|
|
let value = Value::from(valid_boundary_data);
|
|
let result = SchemaValidator::validate(&value, &schema);
|
|
assert!(
|
|
result.is_ok(),
|
|
"Valid boundary conditions should pass validation"
|
|
);
|
|
|
|
// Invalid boundary violations
|
|
let invalid_boundary_data = json!({
|
|
"strings": {
|
|
"empty": "not empty", // Should be empty
|
|
"single": "too long", // Should be exactly 1 character
|
|
"exact_length": "wrong", // Should be exactly 10 characters
|
|
"very_long": "a".repeat(10001) // Exceeds maximum length
|
|
},
|
|
"numbers": {
|
|
"zero": 0.1, // Should be exactly 0
|
|
"negative": 1, // Should be negative
|
|
"positive": -1, // Should be positive
|
|
"float_precision": 123.456 // Wrong precision
|
|
},
|
|
"arrays": {
|
|
"empty": ["not empty"], // Should be empty
|
|
"single_item": [], // Should have exactly 1 item
|
|
"exact_size": [1, 2, 3], // Should have exactly 5 items
|
|
"large_array": vec![true; 1001] // Exceeds maximum items
|
|
},
|
|
"objects": {
|
|
"empty": { "should_be_empty": true }, // Should have no properties
|
|
"single_prop": {} // Missing required property
|
|
},
|
|
"nulls_and_optionals": {
|
|
"nullable": "should allow null or string",
|
|
"required_null": "should be null" // Should be null
|
|
}
|
|
});
|
|
|
|
let value = Value::from(invalid_boundary_data);
|
|
let result = SchemaValidator::validate(&value, &schema);
|
|
assert!(
|
|
result.is_err(),
|
|
"Boundary violations should fail validation"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_concurrent_schema_validation_stress() {
|
|
use std::sync::Arc;
|
|
use std::thread;
|
|
|
|
// Create a complex schema for concurrent testing
|
|
let concurrent_schema = json!({
|
|
"type": "object",
|
|
"properties": {
|
|
"id": { "type": "string", "pattern": "^[a-zA-Z0-9-]{8,64}$" },
|
|
"timestamp": { "type": "string" },
|
|
"data": {
|
|
"type": "object",
|
|
"properties": {
|
|
"values": {
|
|
"type": "array",
|
|
"items": { "type": "number" },
|
|
"minItems": 1,
|
|
"maxItems": 100
|
|
},
|
|
"metadata": {
|
|
"type": "object",
|
|
"additionalProperties": { "type": "string" }
|
|
}
|
|
},
|
|
"required": ["values"]
|
|
}
|
|
},
|
|
"required": ["id", "timestamp", "data"]
|
|
});
|
|
|
|
let schema = Arc::new(Schema::from_serde_json_value(concurrent_schema).unwrap());
|
|
|
|
// Spawn multiple threads to validate concurrently
|
|
let mut handles = vec![];
|
|
|
|
for thread_id in 0..10 {
|
|
let schema_clone = Arc::clone(&schema);
|
|
|
|
let handle = thread::spawn(move || {
|
|
let mut results = Vec::new();
|
|
|
|
for i in 0..100 {
|
|
let test_data = json!({
|
|
"id": format!("thread-{}-item-{}", thread_id, i),
|
|
"timestamp": "2023-12-31T23:59:59Z",
|
|
"data": {
|
|
"values": vec![1.0, 2.0, 3.0, (i as f64)],
|
|
"metadata": {
|
|
"thread": thread_id.to_string(),
|
|
"iteration": i.to_string(),
|
|
"test_type": "concurrent"
|
|
}
|
|
}
|
|
});
|
|
|
|
let value = Value::from(test_data);
|
|
let result = SchemaValidator::validate(&value, &schema_clone);
|
|
results.push(result.is_ok());
|
|
}
|
|
|
|
results
|
|
});
|
|
|
|
handles.push(handle);
|
|
}
|
|
|
|
// Wait for all threads and collect results
|
|
let mut all_results = Vec::new();
|
|
for handle in handles {
|
|
let thread_results = handle.join().expect("Thread should complete successfully");
|
|
all_results.extend(thread_results);
|
|
}
|
|
|
|
// All validations should pass
|
|
let successful_validations = all_results.iter().filter(|&&result| result).count();
|
|
assert_eq!(
|
|
successful_validations, 1000,
|
|
"All 1000 concurrent validations should pass"
|
|
);
|
|
}
|