From d208dfd7e80dbb8687282cc72d08c047a0cc3de4 Mon Sep 17 00:00:00 2001 From: Grzegorz Ziolkowski Date: Fri, 29 May 2026 18:42:39 +0200 Subject: [PATCH] Abilities API: Add coverage for top-level `required` in input validation Document how a top-level `required` keyword is treated by WP_Ability::validate_input(). For a non-object root type it is inert: validation gates solely on `type`, and `required: false` does not make a `null` value acceptable. For an object type, a draft-04 `required` array of property names is honored and enforces property presence. Also remove the inert top-level `required => true` flags that were set on simple-type schemas throughout the test file, since they had no effect on validation. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../phpunit/tests/abilities-api/wpAbility.php | 122 ++++++++++++++---- 1 file changed, 94 insertions(+), 28 deletions(-) diff --git a/tests/phpunit/tests/abilities-api/wpAbility.php b/tests/phpunit/tests/abilities-api/wpAbility.php index c19efc7f1ee56..a2a9353b93d67 100644 --- a/tests/phpunit/tests/abilities-api/wpAbility.php +++ b/tests/phpunit/tests/abilities-api/wpAbility.php @@ -25,7 +25,6 @@ public function set_up(): void { 'output_schema' => array( 'type' => 'number', 'description' => 'The result of performing a math operation.', - 'required' => true, ), 'execute_callback' => static function (): int { return 0; @@ -274,7 +273,6 @@ public function data_execute_input() { array( 'type' => array( 'null', 'integer' ), 'description' => 'The null or integer to convert to integer.', - 'required' => true, ), static function ( $input ): int { return null === $input ? 0 : (int) $input; @@ -286,7 +284,6 @@ static function ( $input ): int { array( 'type' => 'boolean', 'description' => 'The boolean to convert to integer.', - 'required' => true, ), static function ( bool $input ): int { return $input ? 1 : 0; @@ -298,7 +295,6 @@ static function ( bool $input ): int { array( 'type' => 'integer', 'description' => 'The integer to add 5 to.', - 'required' => true, ), static function ( int $input ): int { return 5 + $input; @@ -310,7 +306,6 @@ static function ( int $input ): int { array( 'type' => 'number', 'description' => 'The floating number to round.', - 'required' => true, ), static function ( float $input ): int { return (int) round( $input ); @@ -322,7 +317,6 @@ static function ( float $input ): int { array( 'type' => 'string', 'description' => 'The string to measure the length of.', - 'required' => true, ), static function ( string $input ): int { return strlen( $input ); @@ -361,7 +355,6 @@ static function ( array $input ): int { array( 'type' => 'array', 'description' => 'An array containing two numbers to add.', - 'required' => true, 'minItems' => 2, 'maxItems' => 2, 'items' => array( @@ -403,6 +396,100 @@ public function test_execute_input( $input_schema, $execute_callback, $input, $r $this->assertSame( $result, $ability->execute( $input ) ); } + /** + * Data provider for top-level `required` validation behavior. + * + * Each schema variant is paired with both a valid and an invalid input so the + * inert behavior of a top-level `required` boolean — and the meaningful + * behavior of a draft-04 `required` array on an object — are sealed. + * + * @return array Data sets. + */ + public function data_validate_input_top_level_required() { + $required_true = array( + 'type' => 'string', + 'required' => true, + ); + $required_false = array( + 'type' => 'string', + 'required' => false, + ); + $required_unset = array( + 'type' => 'string', + ); + $object_required = array( + 'type' => 'object', + 'properties' => array( + 'a' => array( 'type' => 'integer' ), + 'b' => array( 'type' => 'integer' ), + ), + 'required' => array( 'a', 'b' ), + ); + + return array( + // A top-level `required: true` is inert: only `type` is enforced. + 'required true: valid input' => array( $required_true, 'hello', true ), + 'required true: invalid input' => array( $required_true, 123, false ), + + // A top-level `required: false` is equally inert and does not permit null. + 'required false: valid input' => array( $required_false, 'hello', true ), + 'required false: invalid input' => array( $required_false, 123, false ), + 'required false: null still invalid' => array( $required_false, null, false ), + + // Omitting `required` behaves identically to setting it. + 'required unset: valid input' => array( $required_unset, 'hello', true ), + 'required unset: invalid input' => array( $required_unset, 123, false ), + + // A draft-04 `required` array on an object type IS honored. + 'object required array: valid input' => array( + $object_required, + array( + 'a' => 1, + 'b' => 2, + ), + true, + ), + 'object required array: invalid input' => array( $object_required, array( 'a' => 1 ), false ), + ); + } + + /** + * Tests how a top-level `required` keyword is handled during input validation. + * + * For a non-object root type, a top-level `required` flag is inert: validation + * gates solely on `type`, so the outcome is identical whether `required` is + * `true`, `false`, or omitted — and `required: false` notably does not make a + * `null` value acceptable. For an object root type, a draft-04 `required` array + * of property names is honored and enforces the presence of those properties. + * + * @ticket 64955 + * + * @covers WP_Ability::validate_input + * + * @dataProvider data_validate_input_top_level_required + * + * @param array $input_schema The input schema under test. + * @param mixed $input The input value to validate. + * @param bool $is_valid Whether the input is expected to pass validation. + */ + public function test_validate_input_top_level_required( $input_schema, $input, $is_valid ) { + $ability = new WP_Ability( + self::$test_ability_name, + array_merge( + self::$test_ability_properties, + array( 'input_schema' => $input_schema ) + ) + ); + + $result = $ability->validate_input( $input ); + + if ( $is_valid ) { + $this->assertTrue( $result, 'Expected the input to pass validation.' ); + } else { + $this->assertWPError( $result, 'Expected the input to fail validation.' ); + } + } + /** * A static method to be used as a callback in tests. * @@ -466,7 +553,6 @@ public function test_execute_with_different_callbacks( $execute_callback ) { 'input_schema' => array( 'type' => 'string', 'description' => 'Test input string.', - 'required' => true, ), 'execute_callback' => $execute_callback, ) @@ -561,7 +647,6 @@ public function test_before_execute_ability_action() { 'input_schema' => array( 'type' => 'integer', 'description' => 'Test input parameter.', - 'required' => true, ), 'execute_callback' => static function ( int $input ): int { return $input * 2; @@ -645,7 +730,6 @@ public function test_after_execute_ability_action() { 'input_schema' => array( 'type' => 'integer', 'description' => 'Test input parameter.', - 'required' => true, ), 'execute_callback' => static function ( int $input ): int { return $input * 3; @@ -813,7 +897,6 @@ public function test_after_action_not_fired_on_output_validation_error() { 'output_schema' => array( 'type' => 'string', 'description' => 'Expected string output.', - 'required' => true, ), 'execute_callback' => static function (): int { return 42; @@ -856,12 +939,10 @@ public function test_normalize_input_filter_can_transform_input() { 'input_schema' => array( 'type' => 'string', 'description' => 'Test input string.', - 'required' => true, ), 'output_schema' => array( 'type' => 'integer', 'description' => 'Result integer.', - 'required' => true, ), 'execute_callback' => static function ( string $input ): int { return strlen( $input ); @@ -898,7 +979,6 @@ public function test_normalize_input_filter_wp_error_halts_execution() { 'input_schema' => array( 'type' => 'string', 'description' => 'Test input string.', - 'required' => true, ), 'execute_callback' => static function ( string $input ) { return strlen( $input ); @@ -934,12 +1014,10 @@ public function test_permission_result_filter_can_grant_permission() { 'input_schema' => array( 'type' => 'integer', 'description' => 'Test input integer.', - 'required' => true, ), 'output_schema' => array( 'type' => 'integer', 'description' => 'Result integer.', - 'required' => true, ), 'execute_callback' => static function ( int $input ): int { return $input; @@ -1105,7 +1183,6 @@ public function test_pre_execute_ability_filter_short_circuits_pipeline() { 'input_schema' => array( 'type' => 'integer', 'description' => 'Test input integer.', - 'required' => true, ), 'execute_callback' => static function (): int { return 1; @@ -1272,7 +1349,6 @@ public function test_execute_result_filter_can_transform_result() { 'input_schema' => array( 'type' => 'integer', 'description' => 'Test input integer.', - 'required' => true, ), 'execute_callback' => static function ( int $input ): int { return $input * 2; @@ -1416,7 +1492,6 @@ public function test_validate_input_filter_receives_all_parameters() { 'input_schema' => array( 'type' => 'string', 'description' => 'Test input string.', - 'required' => true, ), 'execute_callback' => static function ( string $input ): int { return strlen( $input ); @@ -1454,12 +1529,10 @@ public function test_validate_input_filter_overrides_validation_failure() { 'input_schema' => array( 'type' => 'integer', 'description' => 'Test input integer.', - 'required' => true, ), 'output_schema' => array( 'type' => 'integer', 'description' => 'Result integer.', - 'required' => true, ), 'execute_callback' => static function () { return 99; @@ -1496,7 +1569,6 @@ public function test_validate_input_filter_receives_error_on_invalid_input() { 'input_schema' => array( 'type' => 'integer', 'description' => 'Test input integer.', - 'required' => true, ), 'execute_callback' => static function ( int $input ): int { return $input * 2; @@ -1534,7 +1606,6 @@ public function test_validate_input_filter_replaces_error_with_custom() { 'input_schema' => array( 'type' => 'integer', 'description' => 'Test input integer.', - 'required' => true, ), 'execute_callback' => static function ( int $input ): int { return $input * 2; @@ -1572,7 +1643,6 @@ public function test_validate_output_filter_receives_all_parameters() { 'output_schema' => array( 'type' => 'integer', 'description' => 'The result integer.', - 'required' => true, ), 'execute_callback' => static function (): int { return 42; @@ -1610,7 +1680,6 @@ public function test_validate_output_filter_overrides_validation_failure() { 'output_schema' => array( 'type' => 'string', 'description' => 'The result string.', - 'required' => true, ), 'execute_callback' => static function (): int { return 42; @@ -1647,7 +1716,6 @@ public function test_validate_output_filter_receives_error_on_invalid_output() { 'output_schema' => array( 'type' => 'string', 'description' => 'The result string.', - 'required' => true, ), 'execute_callback' => static function (): int { return 42; @@ -1685,7 +1753,6 @@ public function test_validate_output_filter_replaces_error_with_custom() { 'output_schema' => array( 'type' => 'string', 'description' => 'The result string.', - 'required' => true, ), 'execute_callback' => static function (): int { return 42; @@ -1805,7 +1872,6 @@ public function test_ability_invoked_action_fires_on_validation_failure() { 'input_schema' => array( 'type' => 'integer', 'description' => 'Int input.', - 'required' => true, ), 'execute_callback' => static function ( int $input ): int { return $input;