Files
jspg/src/tests.rs

1090 lines
41 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);
// 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);
}