@@ -25,7 +25,6 @@ public function set_up(): void {
2525 'output_schema ' => array (
2626 'type ' => 'number ' ,
2727 'description ' => 'The result of performing a math operation. ' ,
28- 'required ' => true ,
2928 ),
3029 'execute_callback ' => static function (): int {
3130 return 0 ;
@@ -274,7 +273,6 @@ public function data_execute_input() {
274273 array (
275274 'type ' => array ( 'null ' , 'integer ' ),
276275 'description ' => 'The null or integer to convert to integer. ' ,
277- 'required ' => true ,
278276 ),
279277 static function ( $ input ): int {
280278 return null === $ input ? 0 : (int ) $ input ;
@@ -286,7 +284,6 @@ static function ( $input ): int {
286284 array (
287285 'type ' => 'boolean ' ,
288286 'description ' => 'The boolean to convert to integer. ' ,
289- 'required ' => true ,
290287 ),
291288 static function ( bool $ input ): int {
292289 return $ input ? 1 : 0 ;
@@ -298,7 +295,6 @@ static function ( bool $input ): int {
298295 array (
299296 'type ' => 'integer ' ,
300297 'description ' => 'The integer to add 5 to. ' ,
301- 'required ' => true ,
302298 ),
303299 static function ( int $ input ): int {
304300 return 5 + $ input ;
@@ -310,7 +306,6 @@ static function ( int $input ): int {
310306 array (
311307 'type ' => 'number ' ,
312308 'description ' => 'The floating number to round. ' ,
313- 'required ' => true ,
314309 ),
315310 static function ( float $ input ): int {
316311 return (int ) round ( $ input );
@@ -322,7 +317,6 @@ static function ( float $input ): int {
322317 array (
323318 'type ' => 'string ' ,
324319 'description ' => 'The string to measure the length of. ' ,
325- 'required ' => true ,
326320 ),
327321 static function ( string $ input ): int {
328322 return strlen ( $ input );
@@ -361,7 +355,6 @@ static function ( array $input ): int {
361355 array (
362356 'type ' => 'array ' ,
363357 'description ' => 'An array containing two numbers to add. ' ,
364- 'required ' => true ,
365358 'minItems ' => 2 ,
366359 'maxItems ' => 2 ,
367360 'items ' => array (
@@ -403,6 +396,100 @@ public function test_execute_input( $input_schema, $execute_callback, $input, $r
403396 $ this ->assertSame ( $ result , $ ability ->execute ( $ input ) );
404397 }
405398
399+ /**
400+ * Data provider for top-level `required` validation behavior.
401+ *
402+ * Each schema variant is paired with both a valid and an invalid input so the
403+ * inert behavior of a top-level `required` boolean — and the meaningful
404+ * behavior of a draft-04 `required` array on an object — are sealed.
405+ *
406+ * @return array<string, array{0: array, 1: mixed, 2: bool}> Data sets.
407+ */
408+ public function data_validate_input_top_level_required () {
409+ $ required_true = array (
410+ 'type ' => 'string ' ,
411+ 'required ' => true ,
412+ );
413+ $ required_false = array (
414+ 'type ' => 'string ' ,
415+ 'required ' => false ,
416+ );
417+ $ required_unset = array (
418+ 'type ' => 'string ' ,
419+ );
420+ $ object_required = array (
421+ 'type ' => 'object ' ,
422+ 'properties ' => array (
423+ 'a ' => array ( 'type ' => 'integer ' ),
424+ 'b ' => array ( 'type ' => 'integer ' ),
425+ ),
426+ 'required ' => array ( 'a ' , 'b ' ),
427+ );
428+
429+ return array (
430+ // A top-level `required: true` is inert: only `type` is enforced.
431+ 'required true: valid input ' => array ( $ required_true , 'hello ' , true ),
432+ 'required true: invalid input ' => array ( $ required_true , 123 , false ),
433+
434+ // A top-level `required: false` is equally inert and does not permit null.
435+ 'required false: valid input ' => array ( $ required_false , 'hello ' , true ),
436+ 'required false: invalid input ' => array ( $ required_false , 123 , false ),
437+ 'required false: null still invalid ' => array ( $ required_false , null , false ),
438+
439+ // Omitting `required` behaves identically to setting it.
440+ 'required unset: valid input ' => array ( $ required_unset , 'hello ' , true ),
441+ 'required unset: invalid input ' => array ( $ required_unset , 123 , false ),
442+
443+ // A draft-04 `required` array on an object type IS honored.
444+ 'object required array: valid input ' => array (
445+ $ object_required ,
446+ array (
447+ 'a ' => 1 ,
448+ 'b ' => 2 ,
449+ ),
450+ true ,
451+ ),
452+ 'object required array: invalid input ' => array ( $ object_required , array ( 'a ' => 1 ), false ),
453+ );
454+ }
455+
456+ /**
457+ * Tests how a top-level `required` keyword is handled during input validation.
458+ *
459+ * For a non-object root type, a top-level `required` flag is inert: validation
460+ * gates solely on `type`, so the outcome is identical whether `required` is
461+ * `true`, `false`, or omitted — and `required: false` notably does not make a
462+ * `null` value acceptable. For an object root type, a draft-04 `required` array
463+ * of property names is honored and enforces the presence of those properties.
464+ *
465+ * @ticket 64955
466+ *
467+ * @covers WP_Ability::validate_input
468+ *
469+ * @dataProvider data_validate_input_top_level_required
470+ *
471+ * @param array $input_schema The input schema under test.
472+ * @param mixed $input The input value to validate.
473+ * @param bool $is_valid Whether the input is expected to pass validation.
474+ */
475+ public function test_validate_input_top_level_required ( $ input_schema , $ input , $ is_valid ) {
476+ $ ability = new WP_Ability (
477+ self ::$ test_ability_name ,
478+ array_merge (
479+ self ::$ test_ability_properties ,
480+ array ( 'input_schema ' => $ input_schema )
481+ )
482+ );
483+
484+ $ result = $ ability ->validate_input ( $ input );
485+
486+ if ( $ is_valid ) {
487+ $ this ->assertTrue ( $ result , 'Expected the input to pass validation. ' );
488+ } else {
489+ $ this ->assertWPError ( $ result , 'Expected the input to fail validation. ' );
490+ }
491+ }
492+
406493 /**
407494 * A static method to be used as a callback in tests.
408495 *
@@ -466,7 +553,6 @@ public function test_execute_with_different_callbacks( $execute_callback ) {
466553 'input_schema ' => array (
467554 'type ' => 'string ' ,
468555 'description ' => 'Test input string. ' ,
469- 'required ' => true ,
470556 ),
471557 'execute_callback ' => $ execute_callback ,
472558 )
@@ -561,7 +647,6 @@ public function test_before_execute_ability_action() {
561647 'input_schema ' => array (
562648 'type ' => 'integer ' ,
563649 'description ' => 'Test input parameter. ' ,
564- 'required ' => true ,
565650 ),
566651 'execute_callback ' => static function ( int $ input ): int {
567652 return $ input * 2 ;
@@ -645,7 +730,6 @@ public function test_after_execute_ability_action() {
645730 'input_schema ' => array (
646731 'type ' => 'integer ' ,
647732 'description ' => 'Test input parameter. ' ,
648- 'required ' => true ,
649733 ),
650734 'execute_callback ' => static function ( int $ input ): int {
651735 return $ input * 3 ;
@@ -813,7 +897,6 @@ public function test_after_action_not_fired_on_output_validation_error() {
813897 'output_schema ' => array (
814898 'type ' => 'string ' ,
815899 'description ' => 'Expected string output. ' ,
816- 'required ' => true ,
817900 ),
818901 'execute_callback ' => static function (): int {
819902 return 42 ;
@@ -856,12 +939,10 @@ public function test_normalize_input_filter_can_transform_input() {
856939 'input_schema ' => array (
857940 'type ' => 'string ' ,
858941 'description ' => 'Test input string. ' ,
859- 'required ' => true ,
860942 ),
861943 'output_schema ' => array (
862944 'type ' => 'integer ' ,
863945 'description ' => 'Result integer. ' ,
864- 'required ' => true ,
865946 ),
866947 'execute_callback ' => static function ( string $ input ): int {
867948 return strlen ( $ input );
@@ -898,7 +979,6 @@ public function test_normalize_input_filter_wp_error_halts_execution() {
898979 'input_schema ' => array (
899980 'type ' => 'string ' ,
900981 'description ' => 'Test input string. ' ,
901- 'required ' => true ,
902982 ),
903983 'execute_callback ' => static function ( string $ input ) {
904984 return strlen ( $ input );
@@ -934,12 +1014,10 @@ public function test_permission_result_filter_can_grant_permission() {
9341014 'input_schema ' => array (
9351015 'type ' => 'integer ' ,
9361016 'description ' => 'Test input integer. ' ,
937- 'required ' => true ,
9381017 ),
9391018 'output_schema ' => array (
9401019 'type ' => 'integer ' ,
9411020 'description ' => 'Result integer. ' ,
942- 'required ' => true ,
9431021 ),
9441022 'execute_callback ' => static function ( int $ input ): int {
9451023 return $ input ;
@@ -1105,7 +1183,6 @@ public function test_pre_execute_ability_filter_short_circuits_pipeline() {
11051183 'input_schema ' => array (
11061184 'type ' => 'integer ' ,
11071185 'description ' => 'Test input integer. ' ,
1108- 'required ' => true ,
11091186 ),
11101187 'execute_callback ' => static function (): int {
11111188 return 1 ;
@@ -1272,7 +1349,6 @@ public function test_execute_result_filter_can_transform_result() {
12721349 'input_schema ' => array (
12731350 'type ' => 'integer ' ,
12741351 'description ' => 'Test input integer. ' ,
1275- 'required ' => true ,
12761352 ),
12771353 'execute_callback ' => static function ( int $ input ): int {
12781354 return $ input * 2 ;
@@ -1416,7 +1492,6 @@ public function test_validate_input_filter_receives_all_parameters() {
14161492 'input_schema ' => array (
14171493 'type ' => 'string ' ,
14181494 'description ' => 'Test input string. ' ,
1419- 'required ' => true ,
14201495 ),
14211496 'execute_callback ' => static function ( string $ input ): int {
14221497 return strlen ( $ input );
@@ -1454,12 +1529,10 @@ public function test_validate_input_filter_overrides_validation_failure() {
14541529 'input_schema ' => array (
14551530 'type ' => 'integer ' ,
14561531 'description ' => 'Test input integer. ' ,
1457- 'required ' => true ,
14581532 ),
14591533 'output_schema ' => array (
14601534 'type ' => 'integer ' ,
14611535 'description ' => 'Result integer. ' ,
1462- 'required ' => true ,
14631536 ),
14641537 'execute_callback ' => static function () {
14651538 return 99 ;
@@ -1496,7 +1569,6 @@ public function test_validate_input_filter_receives_error_on_invalid_input() {
14961569 'input_schema ' => array (
14971570 'type ' => 'integer ' ,
14981571 'description ' => 'Test input integer. ' ,
1499- 'required ' => true ,
15001572 ),
15011573 'execute_callback ' => static function ( int $ input ): int {
15021574 return $ input * 2 ;
@@ -1534,7 +1606,6 @@ public function test_validate_input_filter_replaces_error_with_custom() {
15341606 'input_schema ' => array (
15351607 'type ' => 'integer ' ,
15361608 'description ' => 'Test input integer. ' ,
1537- 'required ' => true ,
15381609 ),
15391610 'execute_callback ' => static function ( int $ input ): int {
15401611 return $ input * 2 ;
@@ -1572,7 +1643,6 @@ public function test_validate_output_filter_receives_all_parameters() {
15721643 'output_schema ' => array (
15731644 'type ' => 'integer ' ,
15741645 'description ' => 'The result integer. ' ,
1575- 'required ' => true ,
15761646 ),
15771647 'execute_callback ' => static function (): int {
15781648 return 42 ;
@@ -1610,7 +1680,6 @@ public function test_validate_output_filter_overrides_validation_failure() {
16101680 'output_schema ' => array (
16111681 'type ' => 'string ' ,
16121682 'description ' => 'The result string. ' ,
1613- 'required ' => true ,
16141683 ),
16151684 'execute_callback ' => static function (): int {
16161685 return 42 ;
@@ -1647,7 +1716,6 @@ public function test_validate_output_filter_receives_error_on_invalid_output() {
16471716 'output_schema ' => array (
16481717 'type ' => 'string ' ,
16491718 'description ' => 'The result string. ' ,
1650- 'required ' => true ,
16511719 ),
16521720 'execute_callback ' => static function (): int {
16531721 return 42 ;
@@ -1685,7 +1753,6 @@ public function test_validate_output_filter_replaces_error_with_custom() {
16851753 'output_schema ' => array (
16861754 'type ' => 'string ' ,
16871755 'description ' => 'The result string. ' ,
1688- 'required ' => true ,
16891756 ),
16901757 'execute_callback ' => static function (): int {
16911758 return 42 ;
@@ -1805,7 +1872,6 @@ public function test_ability_invoked_action_fires_on_validation_failure() {
18051872 'input_schema ' => array (
18061873 'type ' => 'integer ' ,
18071874 'description ' => 'Int input. ' ,
1808- 'required ' => true ,
18091875 ),
18101876 'execute_callback ' => static function ( int $ input ): int {
18111877 return $ input ;
0 commit comments