905 lines
35 KiB
Rust
905 lines
35 KiB
Rust
use crate::*;
|
|
use crate::helpers::*;
|
|
use crate::schemas::*;
|
|
use serde_json::json;
|
|
use pgrx::pg_test;
|
|
|
|
#[pg_test]
|
|
fn test_validate_not_cached() {
|
|
clear_json_schemas();
|
|
let instance = json!({ "foo": "bar" });
|
|
let result = validate_json_schema("non_existent_schema", jsonb(instance));
|
|
assert_error_count(&result, 1);
|
|
let error = find_error_with_code(&result, "SCHEMA_NOT_FOUND");
|
|
assert_error_message_contains(error, "Schema 'non_existent_schema' not found");
|
|
}
|
|
|
|
#[pg_test]
|
|
fn test_validate_simple() {
|
|
// Use specific schema setup for this test
|
|
let cache_result = simple_schemas();
|
|
assert_success(&cache_result);
|
|
|
|
// Test the basic validation schema
|
|
let valid_instance = json!({ "name": "Alice", "age": 30 });
|
|
let invalid_instance_type = json!({ "name": "Bob", "age": -5 });
|
|
let invalid_instance_missing = json!({ "name": "Charlie" });
|
|
|
|
let valid_result = validate_json_schema("simple.request", jsonb(valid_instance));
|
|
assert_success(&valid_result);
|
|
|
|
// Invalid type - age is negative
|
|
let invalid_result_type = validate_json_schema("simple.request", jsonb(invalid_instance_type));
|
|
assert_error_count(&invalid_result_type, 1);
|
|
|
|
let error = find_error_with_code_and_path(&invalid_result_type, "MINIMUM_VIOLATED", "/age");
|
|
assert_error_detail(error, "schema", "simple.request");
|
|
assert_error_context(error, &json!(-5));
|
|
assert_error_cause_json(error, &json!({"got": -5, "want": 0}));
|
|
assert_error_message_contains(error, "Value must be at least 0, but got -5");
|
|
|
|
// Missing field
|
|
let invalid_result_missing = validate_json_schema("simple.request", jsonb(invalid_instance_missing));
|
|
assert_error_count(&invalid_result_missing, 1);
|
|
|
|
let missing_error = find_error_with_code_and_path(&invalid_result_missing, "REQUIRED_FIELD_MISSING", "/age");
|
|
assert_error_detail(missing_error, "schema", "simple.request");
|
|
assert_error_cause_json(missing_error, &json!({"want": ["age"]}));
|
|
assert_error_message_contains(missing_error, "Required field 'age' is missing");
|
|
}
|
|
|
|
#[pg_test]
|
|
fn test_cache_invalid() {
|
|
let cache_result = invalid_schemas();
|
|
assert_error_count(&cache_result, 2);
|
|
assert!(has_error_with_code(&cache_result, "ENUM_VIOLATED"),
|
|
"Should have ENUM_VIOLATED errors");
|
|
}
|
|
|
|
#[pg_test]
|
|
fn test_validate_errors() {
|
|
let cache_result = errors_schemas();
|
|
assert_success(&cache_result);
|
|
|
|
let invalid_instance = json!({
|
|
"address": {
|
|
"street": 123, // Wrong type
|
|
"city": "Supercalifragilisticexpialidocious" // Too long (maxLength: 10)
|
|
}
|
|
});
|
|
|
|
let result = validate_json_schema("detailed_errors_test.request", jsonb(invalid_instance));
|
|
|
|
// Expect 2 errors: one for type mismatch, one for maxLength violation
|
|
assert_error_count(&result, 2);
|
|
assert_has_error(&result, "TYPE_MISMATCH", "/address/street");
|
|
assert_has_error(&result, "MAX_LENGTH_VIOLATED", "/address/city");
|
|
}
|
|
|
|
#[pg_test]
|
|
fn test_validate_oneof() {
|
|
let cache_result = oneof_schemas();
|
|
assert_success(&cache_result);
|
|
|
|
// --- Test case 1: Fails string maxLength (in branch 0) AND missing number_prop (in branch 1) ---
|
|
let invalid_string_instance = json!({ "string_prop": "toolongstring" });
|
|
let result_invalid_string = validate_json_schema("oneof_test.request", jsonb(invalid_string_instance));
|
|
assert_error_count(&result_invalid_string, 2);
|
|
assert_has_error(&result_invalid_string, "MAX_LENGTH_VIOLATED", "/string_prop");
|
|
assert_has_error(&result_invalid_string, "REQUIRED_FIELD_MISSING", "/number_prop");
|
|
|
|
// --- Test case 2: Fails number minimum (in branch 1) AND missing string_prop (in branch 0) ---
|
|
let invalid_number_instance = json!({ "number_prop": 5 });
|
|
let result_invalid_number = validate_json_schema("oneof_test.request", jsonb(invalid_number_instance));
|
|
assert_error_count(&result_invalid_number, 2);
|
|
assert_has_error(&result_invalid_number, "MINIMUM_VIOLATED", "/number_prop");
|
|
assert_has_error(&result_invalid_number, "REQUIRED_FIELD_MISSING", "/string_prop");
|
|
|
|
// --- Test case 3: Fails type check (not object) for both branches ---
|
|
// Input: boolean, expected object for both branches
|
|
let invalid_bool_instance = json!(true); // Not an object
|
|
let result_invalid_bool = validate_json_schema("oneof_test.request", jsonb(invalid_bool_instance));
|
|
// Expect only 1 leaf error after filtering, as both original errors have instance_path ""
|
|
assert_error_count(&result_invalid_bool, 1);
|
|
let error = find_error_with_code_and_path(&result_invalid_bool, "TYPE_MISMATCH", "");
|
|
assert_error_detail(error, "schema", "oneof_test.request");
|
|
|
|
// --- Test case 4: Fails missing required for both branches ---
|
|
// Input: empty object, expected string_prop (branch 0) OR number_prop (branch 1)
|
|
let invalid_empty_obj = json!({});
|
|
let result_empty_obj = validate_json_schema("oneof_test.request", jsonb(invalid_empty_obj));
|
|
// Now we expect 2 errors because required fields are split into individual errors
|
|
assert_error_count(&result_empty_obj, 2);
|
|
assert_has_error(&result_empty_obj, "REQUIRED_FIELD_MISSING", "/string_prop");
|
|
assert_has_error(&result_empty_obj, "REQUIRED_FIELD_MISSING", "/number_prop");
|
|
}
|
|
|
|
#[pg_test]
|
|
fn test_validate_root_types() {
|
|
let cache_result = root_types_schemas();
|
|
assert_success(&cache_result);
|
|
|
|
// Test 1: Validate null against array schema (using array_test from comprehensive setup)
|
|
let null_instance = json!(null);
|
|
let null_result = validate_json_schema("array_test.request", jsonb(null_instance));
|
|
assert_error_count(&null_result, 1);
|
|
let null_error = find_error_with_code_and_path(&null_result, "TYPE_MISMATCH", "");
|
|
assert_error_detail(null_error, "schema", "array_test.request");
|
|
assert_error_context(null_error, &json!(null));
|
|
assert_error_cause_json(null_error, &json!({"got": "null", "want": ["array"]}));
|
|
assert_error_message_contains(null_error, "Expected array but got null");
|
|
|
|
// Test 2: Validate object against array schema
|
|
let object_instance = json!({"id": "not-an-array"});
|
|
let object_result = validate_json_schema("array_test.request", jsonb(object_instance.clone()));
|
|
assert_error_count(&object_result, 1);
|
|
let object_error = find_error_with_code_and_path(&object_result, "TYPE_MISMATCH", "");
|
|
assert_error_detail(object_error, "schema", "array_test.request");
|
|
assert_error_context(object_error, &object_instance);
|
|
assert_error_cause_json(object_error, &json!({"got": "object", "want": ["array"]}));
|
|
assert_error_message_contains(object_error, "Expected array but got object");
|
|
|
|
// Test 3: Valid empty array
|
|
let valid_empty = json!([]);
|
|
let valid_result = validate_json_schema("array_test.request", jsonb(valid_empty));
|
|
assert_success(&valid_result);
|
|
|
|
// Test 4: String at root when object expected (using object_test)
|
|
let string_instance = json!("not an object");
|
|
let string_result = validate_json_schema("object_test.request", jsonb(string_instance));
|
|
assert_error_count(&string_result, 1);
|
|
let string_error = find_error_with_code_and_path(&string_result, "TYPE_MISMATCH", "");
|
|
assert_error_detail(string_error, "schema", "object_test.request");
|
|
assert_error_context(string_error, &json!("not an object"));
|
|
assert_error_cause_json(string_error, &json!({"got": "string", "want": ["object"]}));
|
|
assert_error_message_contains(string_error, "Expected object but got string");
|
|
}
|
|
|
|
#[pg_test]
|
|
fn test_validate_strict() {
|
|
let cache_result = strict_schemas();
|
|
assert_success(&cache_result);
|
|
|
|
// Test 1: Basic strict validation - extra properties should fail
|
|
let valid_basic = json!({ "name": "John" });
|
|
let invalid_basic = json!({ "name": "John", "extra": "not allowed" });
|
|
|
|
let result_basic_valid = validate_json_schema("basic_strict_test.request", jsonb(valid_basic));
|
|
assert_success(&result_basic_valid);
|
|
|
|
let result_basic_invalid = validate_json_schema("basic_strict_test.request", jsonb(invalid_basic.clone()));
|
|
assert_error_count(&result_basic_invalid, 1);
|
|
assert_has_error(&result_basic_invalid, "ADDITIONAL_PROPERTIES_NOT_ALLOWED", "/extra");
|
|
|
|
// Test 2: Non-strict validation - extra properties should pass
|
|
let result_non_strict = validate_json_schema("non_strict_test.request", jsonb(invalid_basic.clone()));
|
|
assert_success(&result_non_strict);
|
|
|
|
// Test 3: Nested objects and arrays - test recursive strict validation
|
|
let valid_nested = json!({
|
|
"user": { "name": "Alice" },
|
|
"items": [{ "id": "123" }]
|
|
});
|
|
let invalid_nested = json!({
|
|
"user": { "name": "Alice", "extra": "not allowed" }, // Extra in nested object
|
|
"items": [{ "id": "123", "extra": "not allowed" }] // Extra in array item
|
|
});
|
|
|
|
let result_nested_valid = validate_json_schema("nested_strict_test.request", jsonb(valid_nested));
|
|
assert_success(&result_nested_valid);
|
|
|
|
let result_nested_invalid = validate_json_schema("nested_strict_test.request", jsonb(invalid_nested));
|
|
assert_error_count(&result_nested_invalid, 2);
|
|
assert_has_error(&result_nested_invalid, "ADDITIONAL_PROPERTIES_NOT_ALLOWED", "/user/extra");
|
|
assert_has_error(&result_nested_invalid, "ADDITIONAL_PROPERTIES_NOT_ALLOWED", "/items/0/extra");
|
|
|
|
// Test 4: Schema with unevaluatedProperties already set - should allow extras
|
|
let result_already_unevaluated = validate_json_schema("already_unevaluated_test.request", jsonb(invalid_basic.clone()));
|
|
assert_success(&result_already_unevaluated);
|
|
|
|
// Test 5: Schema with additionalProperties already set - should follow that setting
|
|
let result_already_additional = validate_json_schema("already_additional_test.request", jsonb(invalid_basic));
|
|
assert_error_count(&result_already_additional, 1);
|
|
assert_has_error(&result_already_additional, "ADDITIONAL_PROPERTIES_NOT_ALLOWED", "/extra");
|
|
|
|
// Test 6: Conditional schemas - properties in if/then/else should not be restricted
|
|
let valid_conditional = json!({
|
|
"creating": true,
|
|
"name": "Test" // Required when creating=true
|
|
});
|
|
let invalid_conditional = json!({
|
|
"creating": true,
|
|
"name": "Test",
|
|
"extra": "not allowed" // Extra property at root level
|
|
});
|
|
|
|
let result_conditional_valid = validate_json_schema("conditional_strict_test.request", jsonb(valid_conditional));
|
|
assert_success(&result_conditional_valid);
|
|
|
|
let result_conditional_invalid = validate_json_schema("conditional_strict_test.request", jsonb(invalid_conditional));
|
|
assert_error_count(&result_conditional_invalid, 1);
|
|
assert_has_error(&result_conditional_invalid, "ADDITIONAL_PROPERTIES_NOT_ALLOWED", "/extra");
|
|
}
|
|
|
|
#[pg_test]
|
|
fn test_validate_required() {
|
|
let cache_result = required_schemas();
|
|
assert_success(&cache_result);
|
|
|
|
// Test 1: Missing all required fields (using basic_validation_test which requires name and age)
|
|
let empty_instance = json!({});
|
|
let result = validate_json_schema("basic_validation_test.request", jsonb(empty_instance));
|
|
|
|
// Should get 2 separate errors, one for each missing field
|
|
assert_error_count(&result, 2);
|
|
|
|
let name_error = find_error_with_code_and_path(&result, "REQUIRED_FIELD_MISSING", "/name");
|
|
assert_error_message_contains(name_error, "Required field 'name' is missing");
|
|
|
|
let age_error = find_error_with_code_and_path(&result, "REQUIRED_FIELD_MISSING", "/age");
|
|
assert_error_message_contains(age_error, "Required field 'age' is missing");
|
|
|
|
// Test 2: Missing only some required fields
|
|
let partial_instance = json!({
|
|
"name": "Alice"
|
|
});
|
|
let partial_result = validate_json_schema("basic_validation_test.request", jsonb(partial_instance));
|
|
|
|
// Should get 1 error for the missing field
|
|
assert_error_count(&partial_result, 1);
|
|
assert_has_error(&partial_result, "REQUIRED_FIELD_MISSING", "/age");
|
|
}
|
|
|
|
#[pg_test]
|
|
fn test_validate_dependencies() {
|
|
let cache_result = dependencies_schemas();
|
|
assert_success(&cache_result);
|
|
|
|
// Test 1: Has creating=true but missing both dependent fields
|
|
let missing_both = json!({
|
|
"creating": true,
|
|
"description": "Some description"
|
|
});
|
|
let result = validate_json_schema("dependency_split_test.request", jsonb(missing_both));
|
|
|
|
// Should get 2 separate errors, one for each missing dependent field
|
|
assert_error_count(&result, 2);
|
|
|
|
let name_dep_error = find_error_with_code_and_path(&result, "DEPENDENCY_FAILED", "/name");
|
|
assert_error_message_contains(name_dep_error, "Field 'name' is required when 'creating' is present");
|
|
|
|
let kind_dep_error = find_error_with_code_and_path(&result, "DEPENDENCY_FAILED", "/kind");
|
|
assert_error_message_contains(kind_dep_error, "Field 'kind' is required when 'creating' is present");
|
|
|
|
// Test 2: Has creating=true with only one dependent field
|
|
let missing_one = json!({
|
|
"creating": true,
|
|
"name": "My Account"
|
|
});
|
|
let result_one = validate_json_schema("dependency_split_test.request", jsonb(missing_one));
|
|
|
|
// Should get 1 error for the missing kind field
|
|
assert_error_count(&result_one, 1);
|
|
let kind_error = find_error_with_code_and_path(&result_one, "DEPENDENCY_FAILED", "/kind");
|
|
assert_error_message_contains(kind_error, "Field 'kind' is required when 'creating' is present");
|
|
|
|
// Test 3: Has no creating field - no dependency errors
|
|
let no_creating = json!({
|
|
"description": "No creating field"
|
|
});
|
|
let result_no_creating = validate_json_schema("dependency_split_test.request", jsonb(no_creating));
|
|
assert_success(&result_no_creating);
|
|
|
|
// Test 4: Has creating=false - dependencies still apply because field exists!
|
|
let creating_false = json!({
|
|
"creating": false,
|
|
"description": "Creating is false"
|
|
});
|
|
let result_false = validate_json_schema("dependency_split_test.request", jsonb(creating_false));
|
|
// Dependencies are triggered by field existence, not value, so this should fail
|
|
assert_error_count(&result_false, 2);
|
|
assert_has_error(&result_false, "DEPENDENCY_FAILED", "/name");
|
|
assert_has_error(&result_false, "DEPENDENCY_FAILED", "/kind");
|
|
}
|
|
|
|
#[pg_test]
|
|
fn test_validate_nested_req_deps() {
|
|
let cache_result = nested_req_deps_schemas();
|
|
assert_success(&cache_result);
|
|
|
|
// Test with array items that have dependency violations
|
|
let instance = json!({
|
|
"items": [
|
|
{
|
|
"id": "item1",
|
|
"creating": true
|
|
// Missing name and kind
|
|
},
|
|
{
|
|
"id": "item2",
|
|
"creating": true,
|
|
"name": "Item 2"
|
|
// Missing kind
|
|
}
|
|
]
|
|
});
|
|
|
|
let result = validate_json_schema("nested_dep_test.request", jsonb(instance));
|
|
|
|
// Should get 3 errors total: 2 for first item, 1 for second item
|
|
assert_error_count(&result, 3);
|
|
|
|
// Check paths are correct for array items
|
|
assert_has_error(&result, "DEPENDENCY_FAILED", "/items/0/name");
|
|
assert_has_error(&result, "DEPENDENCY_FAILED", "/items/0/kind");
|
|
assert_has_error(&result, "DEPENDENCY_FAILED", "/items/1/kind");
|
|
}
|
|
|
|
#[pg_test]
|
|
fn test_validate_additional_properties() {
|
|
let cache_result = additional_properties_schemas();
|
|
assert_success(&cache_result);
|
|
|
|
// Test 1: Multiple additional properties not allowed
|
|
let instance_many_extras = json!({
|
|
"name": "Alice",
|
|
"age": 30,
|
|
"extra1": "not allowed",
|
|
"extra2": 42,
|
|
"extra3": true
|
|
});
|
|
|
|
let result = validate_json_schema("additional_props_test.request", jsonb(instance_many_extras));
|
|
|
|
// Should get 3 separate errors, one for each additional property
|
|
assert_error_count(&result, 3);
|
|
|
|
let extra1_error = find_error_with_code_and_path(&result, "ADDITIONAL_PROPERTIES_NOT_ALLOWED", "/extra1");
|
|
assert_error_message_contains(extra1_error, "Property 'extra1' is not allowed");
|
|
|
|
let extra2_error = find_error_with_code_and_path(&result, "ADDITIONAL_PROPERTIES_NOT_ALLOWED", "/extra2");
|
|
assert_error_message_contains(extra2_error, "Property 'extra2' is not allowed");
|
|
|
|
let extra3_error = find_error_with_code_and_path(&result, "ADDITIONAL_PROPERTIES_NOT_ALLOWED", "/extra3");
|
|
assert_error_message_contains(extra3_error, "Property 'extra3' is not allowed");
|
|
|
|
// Test 2: Single additional property
|
|
let instance_one_extra = json!({
|
|
"name": "Bob",
|
|
"age": 25,
|
|
"unauthorized": "field"
|
|
});
|
|
|
|
let result_one = validate_json_schema("additional_props_test.request", jsonb(instance_one_extra));
|
|
|
|
// Should get 1 error for the additional property
|
|
assert_error_count(&result_one, 1);
|
|
let unauthorized_error = find_error_with_code_and_path(&result_one, "ADDITIONAL_PROPERTIES_NOT_ALLOWED", "/unauthorized");
|
|
assert_error_message_contains(unauthorized_error, "Property 'unauthorized' is not allowed");
|
|
|
|
// Test 3: Nested objects with additional properties (already in comprehensive setup)
|
|
|
|
let nested_instance = json!({
|
|
"user": {
|
|
"name": "Charlie",
|
|
"role": "admin",
|
|
"level": 5
|
|
}
|
|
});
|
|
|
|
let nested_result = validate_json_schema("nested_additional_props_test.request", jsonb(nested_instance));
|
|
|
|
// Should get 2 errors for the nested additional properties
|
|
assert_error_count(&nested_result, 2);
|
|
assert_has_error(&nested_result, "ADDITIONAL_PROPERTIES_NOT_ALLOWED", "/user/role");
|
|
assert_has_error(&nested_result, "ADDITIONAL_PROPERTIES_NOT_ALLOWED", "/user/level");
|
|
}
|
|
|
|
#[pg_test]
|
|
fn test_validate_unevaluated_properties() {
|
|
let cache_result = unevaluated_properties_schemas();
|
|
assert_success(&cache_result);
|
|
|
|
// Test 1: Multiple unevaluated properties
|
|
let instance_uneval = json!({
|
|
"name": "Alice",
|
|
"age": 30,
|
|
"attr_color": "blue", // This is OK - matches pattern
|
|
"extra1": "not evaluated", // These should fail
|
|
"extra2": 42,
|
|
"extra3": true
|
|
});
|
|
|
|
let result = validate_json_schema("simple_unevaluated_test.request", jsonb(instance_uneval));
|
|
|
|
// Should get 3 separate ADDITIONAL_PROPERTIES_NOT_ALLOWED errors, one for each unevaluated property
|
|
assert_error_count(&result, 3);
|
|
|
|
// Verify all errors are ADDITIONAL_PROPERTIES_NOT_ALLOWED and check paths
|
|
assert_has_error(&result, "ADDITIONAL_PROPERTIES_NOT_ALLOWED", "/extra1");
|
|
assert_has_error(&result, "ADDITIONAL_PROPERTIES_NOT_ALLOWED", "/extra2");
|
|
assert_has_error(&result, "ADDITIONAL_PROPERTIES_NOT_ALLOWED", "/extra3");
|
|
|
|
// Verify error messages
|
|
let extra1_error = find_error_with_code_and_path(&result, "ADDITIONAL_PROPERTIES_NOT_ALLOWED", "/extra1");
|
|
assert_error_message_contains(extra1_error, "Property 'extra1' is not allowed");
|
|
|
|
// Test 2: Complex schema with allOf and unevaluatedProperties (already in comprehensive setup)
|
|
|
|
// firstName and lastName are evaluated by allOf schemas, age by main schema
|
|
let complex_instance = json!({
|
|
"firstName": "John",
|
|
"lastName": "Doe",
|
|
"age": 25,
|
|
"nickname": "JD", // Not evaluated by any schema
|
|
"title": "Mr" // Not evaluated by any schema
|
|
});
|
|
|
|
let complex_result = validate_json_schema("conditional_unevaluated_test.request", jsonb(complex_instance));
|
|
|
|
// Should get 2 ADDITIONAL_PROPERTIES_NOT_ALLOWED errors for unevaluated properties
|
|
assert_error_count(&complex_result, 2);
|
|
assert_has_error(&complex_result, "ADDITIONAL_PROPERTIES_NOT_ALLOWED", "/nickname");
|
|
assert_has_error(&complex_result, "ADDITIONAL_PROPERTIES_NOT_ALLOWED", "/title");
|
|
|
|
// Test 3: Valid instance with all properties evaluated
|
|
let valid_instance = json!({
|
|
"name": "Bob",
|
|
"age": 40,
|
|
"attr_style": "modern",
|
|
"attr_theme": "dark"
|
|
});
|
|
|
|
let valid_result = validate_json_schema("simple_unevaluated_test.request", jsonb(valid_instance));
|
|
assert_success(&valid_result);
|
|
}
|
|
|
|
#[pg_test]
|
|
fn test_validate_format_normal() {
|
|
let cache_result = format_schemas();
|
|
assert_success(&cache_result);
|
|
|
|
// A non-empty but invalid string should still fail
|
|
let instance = json!({
|
|
"date_time": "not-a-date"
|
|
});
|
|
|
|
let result = validate_json_schema("format_test.request", jsonb(instance));
|
|
assert_error_count(&result, 1);
|
|
let error = find_error_with_code(&result, "FORMAT_INVALID");
|
|
assert_error_message_contains(error, "not-a-date");
|
|
}
|
|
|
|
#[pg_test]
|
|
fn test_validate_format_empty_string() {
|
|
let cache_result = format_schemas();
|
|
assert_success(&cache_result);
|
|
|
|
// Test with empty strings for all formatted fields
|
|
let instance = json!({
|
|
"uuid": "",
|
|
"date_time": "",
|
|
"email": ""
|
|
});
|
|
|
|
let result = validate_json_schema("format_test.request", jsonb(instance));
|
|
|
|
// This is the test that should fail before the change and pass after
|
|
assert_success(&result);
|
|
}
|
|
|
|
#[pg_test]
|
|
fn test_validate_format_empty_string_with_ref() {
|
|
let cache_result = format_with_ref_schemas();
|
|
assert_success(&cache_result);
|
|
|
|
// Test that an optional field with a format constraint passes validation
|
|
// when the value is an empty string, even when the schema is referenced by a punc.
|
|
let instance = json!({
|
|
"id": "123e4567-e89b-12d3-a456-426614174000",
|
|
"type": "job",
|
|
"worker_id": "" // Optional field with format, but empty string
|
|
});
|
|
|
|
let result = validate_json_schema("save_job.request", jsonb(instance));
|
|
|
|
// This should succeed because empty strings are ignored for format validation.
|
|
assert_success(&result);
|
|
}
|
|
|
|
#[pg_test]
|
|
fn test_validate_property_merging() {
|
|
let cache_result = property_merging_schemas();
|
|
assert_success(&cache_result);
|
|
|
|
// Test that person schema has all properties from the inheritance chain:
|
|
// entity (id, name) + user (password) + person (first_name, last_name)
|
|
|
|
let valid_person_with_all_properties = json!({
|
|
// From entity
|
|
"id": "550e8400-e29b-41d4-a716-446655440000",
|
|
"name": "John Doe",
|
|
"type": "person",
|
|
|
|
// From user
|
|
"password": "securepass123",
|
|
|
|
// From person
|
|
"first_name": "John",
|
|
"last_name": "Doe"
|
|
});
|
|
|
|
let result = validate_json_schema("person", jsonb(valid_person_with_all_properties));
|
|
assert_success(&result);
|
|
|
|
// Test that properties validate according to their schema definitions across the chain
|
|
let invalid_mixed_properties = json!({
|
|
"id": "550e8400-e29b-41d4-a716-446655440000",
|
|
"name": "John Doe",
|
|
"type": "person",
|
|
"password": "short", // Too short from user schema
|
|
"first_name": "", // Empty string violates person schema minLength
|
|
"last_name": "Doe"
|
|
});
|
|
|
|
let result_invalid = validate_json_schema("person", jsonb(invalid_mixed_properties));
|
|
assert_error_count(&result_invalid, 2);
|
|
assert_has_error(&result_invalid, "MIN_LENGTH_VIOLATED", "/password");
|
|
assert_has_error(&result_invalid, "MIN_LENGTH_VIOLATED", "/first_name");
|
|
}
|
|
|
|
#[pg_test]
|
|
fn test_validate_required_merging() {
|
|
let cache_result = required_merging_schemas();
|
|
assert_success(&cache_result);
|
|
|
|
// Test that required fields are merged from inheritance chain:
|
|
// entity: ["id", "type", "created_by"]
|
|
// user: ["password"] (conditional when type=user)
|
|
// person: ["first_name", "last_name"] (conditional when type=person)
|
|
|
|
let missing_all_required = json!({ "type": "person" });
|
|
|
|
let result = validate_json_schema("person", jsonb(missing_all_required));
|
|
// Should fail for all required fields across inheritance chain
|
|
assert_error_count(&result, 4); // id, created_by, first_name, last_name
|
|
assert_has_error(&result, "REQUIRED_FIELD_MISSING", "/id");
|
|
assert_has_error(&result, "REQUIRED_FIELD_MISSING", "/created_by");
|
|
assert_has_error(&result, "REQUIRED_FIELD_MISSING", "/first_name");
|
|
assert_has_error(&result, "REQUIRED_FIELD_MISSING", "/last_name");
|
|
|
|
// Test conditional requirements work through inheritance
|
|
let with_person_type = json!({
|
|
"id": "550e8400-e29b-41d4-a716-446655440000",
|
|
"type": "person",
|
|
"created_by": "550e8400-e29b-41d4-a716-446655440001"
|
|
// Missing password (required when type=user, which person inherits from)
|
|
// Missing first_name, last_name (required when type=person)
|
|
});
|
|
|
|
let result_conditional = validate_json_schema("person", jsonb(with_person_type));
|
|
assert_error_count(&result_conditional, 2); // first_name, last_name
|
|
assert_has_error(&result_conditional, "REQUIRED_FIELD_MISSING", "/first_name");
|
|
assert_has_error(&result_conditional, "REQUIRED_FIELD_MISSING", "/last_name");
|
|
}
|
|
|
|
#[pg_test]
|
|
fn test_validate_dependencies_merging() {
|
|
let cache_result = dependencies_merging_schemas();
|
|
assert_success(&cache_result);
|
|
|
|
// Test dependencies are merged across inheritance:
|
|
// user: creating -> ["name"]
|
|
// person: creating -> ["first_name", "last_name"]
|
|
|
|
let with_creating_missing_deps = json!({
|
|
"id": "550e8400-e29b-41d4-a716-446655440000",
|
|
"type": "person",
|
|
"created_by": "550e8400-e29b-41d4-a716-446655440001",
|
|
"creating": true,
|
|
"password": "securepass"
|
|
// Missing name (from user dependency)
|
|
// Missing first_name, last_name (from person dependency)
|
|
});
|
|
|
|
let result = validate_json_schema("person", jsonb(with_creating_missing_deps));
|
|
assert_error_count(&result, 3); // name, first_name, last_name
|
|
assert_has_error(&result, "DEPENDENCY_FAILED", "/name");
|
|
assert_has_error(&result, "DEPENDENCY_FAILED", "/first_name");
|
|
assert_has_error(&result, "DEPENDENCY_FAILED", "/last_name");
|
|
|
|
// Test partial dependency satisfaction
|
|
let with_some_deps = json!({
|
|
"id": "550e8400-e29b-41d4-a716-446655440000",
|
|
"type": "person",
|
|
"created_by": "550e8400-e29b-41d4-a716-446655440001",
|
|
"creating": true,
|
|
"password": "securepass",
|
|
"name": "John Doe",
|
|
"first_name": "John"
|
|
// Missing last_name from person dependency
|
|
});
|
|
|
|
let result_partial = validate_json_schema("person", jsonb(with_some_deps));
|
|
assert_error_count(&result_partial, 1);
|
|
assert_has_error(&result_partial, "DEPENDENCY_FAILED", "/last_name");
|
|
}
|
|
|
|
#[pg_test]
|
|
fn test_validate_punc_with_refs() {
|
|
let cache_result = punc_with_refs_schemas();
|
|
assert_success(&cache_result);
|
|
|
|
// Test 1: Public punc is strict - no extra properties allowed at root level
|
|
let public_root_extra = json!({
|
|
"type": "person",
|
|
"id": "550e8400-e29b-41d4-a716-446655440000",
|
|
"name": "John Doe",
|
|
"first_name": "John",
|
|
"last_name": "Doe",
|
|
"extra_field": "not allowed at root", // Should fail in public punc
|
|
"another_extra": 123 // Should also fail in public punc
|
|
});
|
|
|
|
let result_public_root = validate_json_schema("public_ref_test.request", jsonb(public_root_extra));
|
|
assert_error_count(&result_public_root, 2);
|
|
assert_has_error(&result_public_root, "ADDITIONAL_PROPERTIES_NOT_ALLOWED", "/extra_field");
|
|
assert_has_error(&result_public_root, "ADDITIONAL_PROPERTIES_NOT_ALLOWED", "/another_extra");
|
|
|
|
// Test 2: Private punc allows extra properties at root level
|
|
let private_root_extra = json!({
|
|
"type": "person",
|
|
"id": "550e8400-e29b-41d4-a716-446655440000",
|
|
"name": "John Doe",
|
|
"first_name": "John",
|
|
"last_name": "Doe",
|
|
"extra_field": "allowed at root in private punc", // Should pass in private punc
|
|
"another_extra": 123 // Should also pass in private punc
|
|
});
|
|
|
|
let result_private_root = validate_json_schema("private_ref_test.request", jsonb(private_root_extra));
|
|
assert_success(&result_private_root); // Should pass with extra properties at root
|
|
|
|
// Test 3: Valid data with address should pass for both
|
|
let valid_data_with_address = json!({
|
|
"type": "person",
|
|
"id": "550e8400-e29b-41d4-a716-446655440000",
|
|
"name": "John Doe",
|
|
"first_name": "John",
|
|
"last_name": "Doe",
|
|
"address": {
|
|
"street": "123 Main St",
|
|
"city": "Boston"
|
|
}
|
|
});
|
|
|
|
let result_public_valid = validate_json_schema("public_ref_test.request", jsonb(valid_data_with_address.clone()));
|
|
assert_success(&result_public_valid);
|
|
|
|
let result_private_valid = validate_json_schema("private_ref_test.request", jsonb(valid_data_with_address));
|
|
assert_success(&result_private_valid);
|
|
}
|
|
|
|
#[pg_test]
|
|
fn test_validate_enum_schema() {
|
|
let cache_result = enum_schemas();
|
|
assert_success(&cache_result);
|
|
|
|
// Test valid enum value
|
|
let valid_priority = json!({
|
|
"priority": "high"
|
|
});
|
|
|
|
let result = validate_json_schema("enum_ref_test.request", jsonb(valid_priority));
|
|
assert_success(&result);
|
|
|
|
// Test invalid enum value for priority (required field)
|
|
let invalid_priority = json!({
|
|
"priority": "critical" // Invalid - not in task_priority enum
|
|
});
|
|
|
|
let result_priority = validate_json_schema("enum_ref_test.request", jsonb(invalid_priority));
|
|
assert_error_count(&result_priority, 1);
|
|
assert_has_error(&result_priority, "ENUM_VIOLATED", "/priority");
|
|
|
|
// Test missing required enum field
|
|
let missing_priority = json!({});
|
|
|
|
let result_missing = validate_json_schema("enum_ref_test.request", jsonb(missing_priority));
|
|
assert_error_count(&result_missing, 1);
|
|
assert_has_error(&result_missing, "REQUIRED_FIELD_MISSING", "/priority");
|
|
}
|
|
|
|
#[pg_test]
|
|
fn test_validate_punc_local_refs() {
|
|
let cache_result = punc_local_refs_schemas();
|
|
assert_success(&cache_result);
|
|
|
|
// Test 1: Punc request referencing a schema defined locally within the punc
|
|
let valid_local_ref = json!({
|
|
"type": "local_address",
|
|
"street": "123 Main St",
|
|
"city": "Anytown"
|
|
});
|
|
let result_valid_local = validate_json_schema("punc_with_local_ref_test.request", jsonb(valid_local_ref));
|
|
assert_success(&result_valid_local);
|
|
|
|
let invalid_local_ref = json!({
|
|
"type": "local_address",
|
|
"street": "123 Main St" // Missing city
|
|
});
|
|
let result_invalid_local = validate_json_schema("punc_with_local_ref_test.request", jsonb(invalid_local_ref));
|
|
assert_error_count(&result_invalid_local, 1);
|
|
assert_has_error(&result_invalid_local, "REQUIRED_FIELD_MISSING", "/city");
|
|
|
|
// Test 2: Punc with a local schema that references a global type schema
|
|
let valid_global_ref = json!({
|
|
"type": "local_user_with_thing",
|
|
"user_name": "Alice",
|
|
"thing": {
|
|
"type": "global_thing",
|
|
"id": "550e8400-e29b-41d4-a716-446655440000"
|
|
}
|
|
});
|
|
let result_valid_global = validate_json_schema("punc_with_local_ref_to_global_test.request", jsonb(valid_global_ref));
|
|
assert_success(&result_valid_global);
|
|
|
|
let invalid_global_ref = json!({
|
|
"type": "local_user_with_thing",
|
|
"user_name": "Bob",
|
|
"thing": {
|
|
"type": "global_thing",
|
|
"id": "not-a-uuid" // Invalid format for global_thing's id
|
|
}
|
|
});
|
|
let result_invalid_global = validate_json_schema("punc_with_local_ref_to_global_test.request", jsonb(invalid_global_ref));
|
|
assert_error_count(&result_invalid_global, 1);
|
|
assert_has_error(&result_invalid_global, "FORMAT_INVALID", "/thing/id");
|
|
}
|
|
|
|
#[pg_test]
|
|
fn test_validate_title_override() {
|
|
let cache_result = title_override_schemas();
|
|
assert_success(&cache_result);
|
|
|
|
// Test that a schema with an overridden title still inherits validation keywords correctly.
|
|
|
|
// This instance is valid because it provides the 'name' required by the base schema.
|
|
let valid_instance = json!({ "type": "override_with_title", "name": "Test Name" });
|
|
let result_valid = validate_json_schema("override_with_title", jsonb(valid_instance));
|
|
assert_success(&result_valid);
|
|
|
|
// This instance is invalid because it's missing the 'name' required by the base schema.
|
|
// This proves that validation keywords are inherited even when metadata keywords are overridden.
|
|
let invalid_instance = json!({ "type": "override_with_title" });
|
|
let result_invalid = validate_json_schema("override_with_title", jsonb(invalid_instance));
|
|
assert_error_count(&result_invalid, 1);
|
|
assert_has_error(&result_invalid, "REQUIRED_FIELD_MISSING", "/name");
|
|
}
|
|
|
|
#[pg_test]
|
|
fn test_validate_type_matching() {
|
|
let cache_result = type_matching_schemas();
|
|
assert_success(&cache_result);
|
|
|
|
// 1. Test 'job' which extends 'entity'
|
|
let valid_job = json!({
|
|
"type": "job",
|
|
"name": "my job",
|
|
"job_id": "job123"
|
|
});
|
|
let result_valid_job = validate_json_schema("job", jsonb(valid_job));
|
|
assert_success(&result_valid_job);
|
|
|
|
let invalid_job = json!({
|
|
"type": "not_job",
|
|
"name": "my job",
|
|
"job_id": "job123"
|
|
});
|
|
let result_invalid_job = validate_json_schema("job", jsonb(invalid_job));
|
|
assert_error_count(&result_invalid_job, 1);
|
|
assert_has_error(&result_invalid_job, "TYPE_MISMATCH", "/type");
|
|
|
|
// 2. Test 'super_job' which extends 'job'
|
|
let valid_super_job = json!({
|
|
"type": "super_job",
|
|
"name": "my super job",
|
|
"job_id": "job123",
|
|
"manager_id": "mgr1"
|
|
});
|
|
let result_valid_super_job = validate_json_schema("super_job", jsonb(valid_super_job));
|
|
assert_success(&result_valid_super_job);
|
|
|
|
// 3. Test 'super_job.short' which should still expect type 'super_job'
|
|
let valid_short_super_job = json!({
|
|
"type": "super_job",
|
|
"name": "short", // maxLength: 10
|
|
"job_id": "job123",
|
|
"manager_id": "mgr1"
|
|
});
|
|
let result_valid_short = validate_json_schema("super_job.short", jsonb(valid_short_super_job));
|
|
assert_success(&result_valid_short);
|
|
|
|
let invalid_short_super_job = json!({
|
|
"type": "job", // Should be 'super_job'
|
|
"name": "short",
|
|
"job_id": "job123",
|
|
"manager_id": "mgr1"
|
|
});
|
|
let result_invalid_short = validate_json_schema("super_job.short", jsonb(invalid_short_super_job));
|
|
assert_error_count(&result_invalid_short, 1);
|
|
let error = find_error_with_code_and_path(&result_invalid_short, "TYPE_MISMATCH", "/type");
|
|
assert_error_message_contains(error, "Instance type 'job' does not match expected type 'super_job'");
|
|
|
|
// 4. Test punc with root, nested, and oneOf type refs
|
|
let valid_punc_instance = json!({
|
|
"root_job": {
|
|
"type": "job",
|
|
"name": "root job",
|
|
"job_id": "job456"
|
|
},
|
|
"nested_or_super_job": {
|
|
"type": "super_job",
|
|
"name": "nested super job",
|
|
"job_id": "job789",
|
|
"manager_id": "mgr2"
|
|
}
|
|
});
|
|
let result_valid_punc = validate_json_schema("type_test_punc.request", jsonb(valid_punc_instance));
|
|
assert_success(&result_valid_punc);
|
|
|
|
// 5. Test invalid type at punc root ref
|
|
let invalid_punc_root = json!({
|
|
"root_job": {
|
|
"type": "entity", // Should be "job"
|
|
"name": "root job",
|
|
"job_id": "job456"
|
|
},
|
|
"nested_or_super_job": {
|
|
"type": "super_job",
|
|
"name": "nested super job",
|
|
"job_id": "job789",
|
|
"manager_id": "mgr2"
|
|
}
|
|
});
|
|
let result_invalid_punc_root = validate_json_schema("type_test_punc.request", jsonb(invalid_punc_root));
|
|
assert_error_count(&result_invalid_punc_root, 1);
|
|
assert_has_error(&result_invalid_punc_root, "TYPE_MISMATCH", "/root_job/type");
|
|
|
|
// 6. Test invalid type at punc nested ref
|
|
let invalid_punc_nested = json!({
|
|
"root_job": {
|
|
"type": "job",
|
|
"name": "root job",
|
|
"job_id": "job456"
|
|
},
|
|
"nested_or_super_job": {
|
|
"my_job": {
|
|
"type": "entity", // Should be "job"
|
|
"name": "nested job",
|
|
"job_id": "job789"
|
|
}
|
|
}
|
|
});
|
|
let result_invalid_punc_nested = validate_json_schema("type_test_punc.request", jsonb(invalid_punc_nested));
|
|
assert_error_count(&result_invalid_punc_nested, 1);
|
|
assert_has_error(&result_invalid_punc_nested, "TYPE_MISMATCH", "/nested_or_super_job/my_job/type");
|
|
|
|
// 7. Test invalid type at punc oneOf ref
|
|
let invalid_punc_oneof = json!({
|
|
"root_job": {
|
|
"type": "job",
|
|
"name": "root job",
|
|
"job_id": "job456"
|
|
},
|
|
"nested_or_super_job": {
|
|
"type": "job", // Should be "super_job"
|
|
"name": "nested super job",
|
|
"job_id": "job789",
|
|
"manager_id": "mgr2"
|
|
}
|
|
});
|
|
let result_invalid_punc_oneof = validate_json_schema("type_test_punc.request", jsonb(invalid_punc_oneof));
|
|
// This will have multiple errors because the invalid oneOf branch will also fail the other branch's validation
|
|
assert_has_error(&result_invalid_punc_oneof, "TYPE_MISMATCH", "/nested_or_super_job/type");
|
|
} |