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); // Test 4: Test that unevaluatedProperties: true cascades down refs let cascading_instance = json!({ "strict_branch": { "another_prop": "is_ok" }, "non_strict_branch": { "extra_at_toplevel": "is_ok", // Extra property at this level "some_prop": { "deep_prop": "is_ok", "extra_in_ref": "is_also_ok" // Extra property in the $ref'd schema } } }); let cascading_result = validate_json_schema("nested_unevaluated_test.request", jsonb(cascading_instance)); assert_success(&cascading_result); // Test 5: For good measure, test that the strict branch is still strict let strict_fail_instance = json!({ "strict_branch": { "another_prop": "is_ok", "extra_in_strict": "is_not_ok" } }); let strict_fail_result = validate_json_schema("nested_unevaluated_test.request", jsonb(strict_fail_instance)); assert_error_count(&strict_fail_result, 1); assert_has_error(&strict_fail_result, "ADDITIONAL_PROPERTIES_NOT_ALLOWED", "/strict_branch/extra_in_strict"); } #[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_failure(&result_invalid_job); assert_has_error(&result_invalid_job, "CONST_VIOLATED", "/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_failure(&result_invalid_short); assert_has_error(&result_invalid_short, "CONST_VIOLATED", "/type"); // 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_failure(&result_invalid_punc_root); assert_has_error(&result_invalid_punc_root, "CONST_VIOLATED", "/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_failure(&result_invalid_punc_nested); assert_has_error(&result_invalid_punc_nested, "CONST_VIOLATED", "/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)); assert_failure(&result_invalid_punc_oneof); assert_has_error(&result_invalid_punc_oneof, "CONST_VIOLATED", "/nested_or_super_job/type"); } #[pg_test] fn test_validate_union_type_matching() { let cache_result = union_schemas(); assert_success(&cache_result); // 1. Test valid instance with type 'union_a' let valid_instance_a = json!({ "union_prop": { "id": "123", "type": "union_a", "prop_a": "hello" } }); let result_a = validate_json_schema("union_test.request", jsonb(valid_instance_a)); assert_success(&result_a); // 2. Test valid instance with type 'union_b' let valid_instance_b = json!({ "union_prop": { "id": "456", "type": "union_b", "prop_b": 123 } }); let result_b = validate_json_schema("union_test.request", jsonb(valid_instance_b)); assert_success(&result_b); // 3. Test invalid instance - wrong type const in a valid oneOf branch let invalid_sub_schema = json!({ "union_prop": { "id": "789", "type": "union_b", // Should be union_a "prop_a": "hello" } }); let result_invalid_sub = validate_json_schema("union_test.request", jsonb(invalid_sub_schema)); assert_failure(&result_invalid_sub); // This should fail because the `type` override in `union_a` is `const: "union_a"` assert_has_error(&result_invalid_sub, "CONST_VIOLATED", "/union_prop/type"); // 4. Test invalid instance - base type, should fail due to override let invalid_base_type = json!({ "union_prop": { "id": "101", "type": "union_base", // This is the base type, but the override should be enforced "prop_a": "world" } }); let result_invalid_base = validate_json_schema("union_test.request", jsonb(invalid_base_type)); assert_failure(&result_invalid_base); assert_has_error(&result_invalid_base, "CONST_VIOLATED", "/union_prop/type"); } #[pg_test] fn test_validate_nullable_union() { let cache_result = nullable_union_schemas(); assert_success(&cache_result); // 1. Test valid instance with object type 'thing_a' let valid_object_a = json!({ "nullable_prop": { "id": "123", "type": "thing_a", "prop_a": "hello" } }); let result_obj_a = validate_json_schema("nullable_union_test.request", jsonb(valid_object_a)); assert_success(&result_obj_a); // 2. Test valid instance with object type 'thing_b' let valid_object_b = json!({ "nullable_prop": { "id": "456", "type": "thing_b", "prop_b": "goodbye" } }); let result_obj_b = validate_json_schema("nullable_union_test.request", jsonb(valid_object_b)); assert_success(&result_obj_b); // 3. Test valid instance with null let valid_null = json!({ "nullable_prop": null }); let result_null = validate_json_schema("nullable_union_test.request", jsonb(valid_null)); assert_success(&result_null); // 4. Test invalid instance - base type, should fail due to override let invalid_base_type = json!({ "nullable_prop": { "id": "789", "type": "thing_base", "prop_a": "should fail" } }); let result_invalid_base = validate_json_schema("nullable_union_test.request", jsonb(invalid_base_type)); assert_failure(&result_invalid_base); assert_has_error(&result_invalid_base, "CONST_VIOLATED", "/nullable_prop/type"); // 5. Test invalid instance (e.g., a string) let invalid_string = json!({ "nullable_prop": "not_an_object_or_null" }); let result_invalid = validate_json_schema("nullable_union_test.request", jsonb(invalid_string)); assert_failure(&result_invalid); assert_has_error(&result_invalid, "TYPE_MISMATCH", "/nullable_prop"); } #[pg_test] fn test_validate_type_hierarchy() { clear_json_schemas(); let cache_result = hierarchy_schemas(); assert_success(&cache_result); // 1. Test success case: validating a derived type (person) against a base schema (organization) let person_instance = json!({ "id": "person-id", "type": "person", "name": "person-name", "password": "person-password", "first_name": "person-first-name" }); let result_success = validate_json_schema("organization", jsonb(person_instance.clone())); assert_success(&result_success); // 2. Test success case: validating a base type (organization) against its own schema let org_instance = json!({ "id": "org-id", "type": "organization", "name": "org-name" }); let result_org_success = validate_json_schema("organization", jsonb(org_instance)); assert_success(&result_org_success); // 3. Test failure case: validating an ancestor type (entity) against a derived schema (organization) let entity_instance = json!({ "id": "entity-id", "type": "entity" }); let result_fail_ancestor = validate_json_schema("organization", jsonb(entity_instance)); assert_failure(&result_fail_ancestor); assert_has_error(&result_fail_ancestor, "ENUM_VIOLATED", "/type"); // 4. Test failure case: validating a completely unrelated type let unrelated_instance = json!({ "id": "job-id", "type": "job", "name": "job-name" }); let result_fail_unrelated = validate_json_schema("organization", jsonb(unrelated_instance)); assert_failure(&result_fail_unrelated); assert_has_error(&result_fail_unrelated, "ENUM_VIOLATED", "/type"); // 5. Test that the punc using the schema also works let punc_success = validate_json_schema("test_org_punc.request", jsonb(person_instance.clone())); assert_success(&punc_success); }