diff --git a/includes/abilities-api.php b/includes/abilities-api.php index 05e4bb3..73ba658 100644 --- a/includes/abilities-api.php +++ b/includes/abilities-api.php @@ -254,9 +254,16 @@ * @type array $meta { * Optional. Additional metadata for the ability. * - * @type array $annotations Optional. Annotation metadata for the ability. Provides - * additional semantic information about the ability's - * characteristics and behavior. + * @type array $annotations { + * Optional. Semantic annotations describing the ability's behavioral characteristics. + * These annotations are hints for tooling and documentation. + * + * @type bool|null $readonly Optional. If true, the ability does not modify its environment. + * @type bool|null $destructive Optional. If true, the ability may perform destructive updates to its environment. + * If false, the ability performs only additive updates. + * @type bool|null $idempotent Optional. If true, calling the ability repeatedly with the same arguments + * will have no additional effect on its environment. + * } * @type bool $show_in_rest Optional. Whether to expose this ability in the REST API. * When true, the ability can be invoked via HTTP requests. * Default false. @@ -269,7 +276,7 @@ * @return WP_Ability|null The registered ability instance on success, `null` on failure. */ function wp_register_ability( string $name, array $args ): ?WP_Ability { - if ( ! did_action( 'wp_abilities_api_init' ) ) { + if ( ! doing_action( 'wp_abilities_api_init' ) ) { _doing_it_wrong( __FUNCTION__, sprintf( @@ -458,7 +465,7 @@ function wp_get_abilities(): array { * @return WP_Ability_Category|null The registered ability category instance on success, `null` on failure. */ function wp_register_ability_category( string $slug, array $args ): ?WP_Ability_Category { - if ( ! did_action( 'wp_abilities_api_categories_init' ) ) { + if ( ! doing_action( 'wp_abilities_api_categories_init' ) ) { _doing_it_wrong( __FUNCTION__, sprintf( diff --git a/includes/abilities-api/class-wp-abilities-registry.php b/includes/abilities-api/class-wp-abilities-registry.php index 398d28c..ecd6dc2 100644 --- a/includes/abilities-api/class-wp-abilities-registry.php +++ b/includes/abilities-api/class-wp-abilities-registry.php @@ -61,7 +61,16 @@ final class WP_Abilities_Registry { * @type array $meta { * Optional. Additional metadata for the ability. * - * @type array $annotations Optional. Annotation metadata for the ability. + * @type array $annotations { + * Optional. Semantic annotations describing the ability's behavioral characteristics. + * These annotations are hints for tooling and documentation. + * + * @type bool|null $readonly Optional. If true, the ability does not modify its environment. + * @type bool|null $destructive Optional. If true, the ability may perform destructive updates to its environment. + * If false, the ability performs only additive updates. + * @type bool|null $idempotent Optional. If true, calling the ability repeatedly with the same arguments + * will have no additional effect on its environment. + * } * @type bool $show_in_rest Optional. Whether to expose this ability in the REST API. Default false. * } * @type string $ability_class Optional. Custom class to instantiate instead of WP_Ability. diff --git a/includes/abilities-api/class-wp-ability-categories-registry.php b/includes/abilities-api/class-wp-ability-categories-registry.php index 00e6ad8..58bc432 100644 --- a/includes/abilities-api/class-wp-ability-categories-registry.php +++ b/includes/abilities-api/class-wp-ability-categories-registry.php @@ -206,7 +206,7 @@ public static function get_instance(): ?self { __METHOD__, sprintf( // translators: %s: init action. - __( 'Ability API should not be initialized before the %s action has fired' ), + __( 'Ability API should not be initialized before the %s action has fired.' ), 'init' ), '6.9.0' diff --git a/includes/abilities-api/class-wp-ability.php b/includes/abilities-api/class-wp-ability.php index b890d92..d116080 100644 --- a/includes/abilities-api/class-wp-ability.php +++ b/includes/abilities-api/class-wp-ability.php @@ -33,7 +33,7 @@ class WP_Ability { * They are not guaranteed to provide a faithful description of ability behavior. * * @since 6.9.0 - * @var array + * @var array */ protected static $default_annotations = array( // If true, the ability does not modify its environment. @@ -150,7 +150,16 @@ class WP_Ability { * @type array $meta { * Optional. Additional metadata for the ability. * - * @type array $annotations Optional. Annotation metadata for the ability. + * @type array $annotations { + * Optional. Semantic annotations describing the ability's behavioral characteristics. + * These annotations are hints for tooling and documentation. + * + * @type bool|null $readonly Optional. If true, the ability does not modify its environment. + * @type bool|null $destructive Optional. If true, the ability may perform destructive updates to its environment. + * If false, the ability performs only additive updates. + * @type bool|null $idempotent Optional. If true, calling the ability repeatedly with the same arguments + * will have no additional effect on its environment. + * } * @type bool $show_in_rest Optional. Whether to expose this ability in the REST API. Default false. * } * } @@ -205,7 +214,16 @@ public function __construct( string $name, array $args ) { * @type array $meta { * Optional. Additional metadata for the ability. * - * @type array $annotations Optional. Annotation metadata for the ability. + * @type array $annotations { + * Optional. Semantic annotations describing the ability's behavioral characteristics. + * These annotations are hints for tooling and documentation. + * + * @type bool|null $readonly Optional. If true, the ability does not modify its environment. + * @type bool|null $destructive Optional. If true, the ability may perform destructive updates to its environment. + * If false, the ability performs only additive updates. + * @type bool|null $idempotent Optional. If true, calling the ability repeatedly with the same arguments + * will have no additional effect on its environment. + * } * @type bool $show_in_rest Optional. Whether to expose this ability in the REST API. Default false. * } * } @@ -224,7 +242,16 @@ public function __construct( string $name, array $args ) { * @type array $meta { * Additional metadata for the ability. * - * @type array $annotations Optional. Annotation metadata for the ability. + * @type array $annotations { + * Semantic annotations describing the ability's behavioral characteristics. + * These annotations are hints for tooling and documentation. + * + * @type bool|null $readonly If true, the ability does not modify its environment. + * @type bool|null $destructive If true, the ability may perform destructive updates to its environment. + * If false, the ability performs only additive updates. + * @type bool|null $idempotent If true, calling the ability repeatedly with the same arguments + * will have no additional effect on its environment. + * } * @type bool $show_in_rest Whether to expose this ability in the REST API. Default false. * } * } @@ -498,6 +525,14 @@ protected function invoke_callback( callable $callback, $input = null ) { * @return bool|WP_Error Whether the ability has the necessary permission. */ public function check_permissions( $input = null ) { + if ( ! is_callable( $this->permission_callback ) ) { + return new WP_Error( + 'ability_invalid_permission_callback', + /* translators: %s ability name. */ + sprintf( __( 'Ability "%s" does not have a valid permission callback.' ), esc_html( $this->name ) ) + ); + } + return $this->invoke_callback( $this->permission_callback, $input ); } diff --git a/tests/unit/abilities-api/wpAbilitiesRegistry.php b/tests/unit/abilities-api/wpAbilitiesRegistry.php index 95fa29a..badd81c 100644 --- a/tests/unit/abilities-api/wpAbilitiesRegistry.php +++ b/tests/unit/abilities-api/wpAbilitiesRegistry.php @@ -29,8 +29,9 @@ public function set_up(): void { remove_all_filters( 'wp_register_ability_args' ); - // Fire the init hook to allow test ability category registration. - do_action( 'wp_abilities_api_categories_init' ); + // Simulates the Abilities API init hook to allow test ability category registration. + global $wp_current_filter; + $wp_current_filter[] = 'wp_abilities_api_categories_init'; wp_register_ability_category( 'math', array( @@ -38,6 +39,7 @@ public function set_up(): void { 'description' => 'Mathematical operations and calculations.', ) ); + array_pop( $wp_current_filter ); self::$test_ability_args = array( 'label' => 'Add numbers', diff --git a/tests/unit/abilities-api/wpAbility.php b/tests/unit/abilities-api/wpAbility.php index 910ae7d..73a5fbf 100644 --- a/tests/unit/abilities-api/wpAbility.php +++ b/tests/unit/abilities-api/wpAbility.php @@ -18,16 +18,6 @@ class Tests_Abilities_API_WpAbility extends WP_UnitTestCase { public function set_up(): void { parent::set_up(); - // Fire the init hook to allow test ability category registration. - do_action( 'wp_abilities_api_categories_init' ); - wp_register_ability_category( - 'math', - array( - 'label' => 'Math', - 'description' => 'Mathematical operations and calculations.', - ) - ); - self::$test_ability_properties = array( 'label' => 'Calculator', 'description' => 'Calculates the result of math operations.', @@ -56,9 +46,6 @@ public function set_up(): void { * Tear down after each test. */ public function tear_down(): void { - // Clean up registered test ability category. - wp_unregister_ability_category( 'math' ); - parent::tear_down(); } diff --git a/tests/unit/abilities-api/wpCoreAbilities.php b/tests/unit/abilities-api/wpCoreAbilities.php deleted file mode 100644 index d69dc24..0000000 --- a/tests/unit/abilities-api/wpCoreAbilities.php +++ /dev/null @@ -1,180 +0,0 @@ -is_registered( 'core/get-site-info' ) ) { - $registry->unregister( 'core/get-site-info' ); - } - if ( $registry->is_registered( 'core/get-user-info' ) ) { - $registry->unregister( 'core/get-user-info' ); - } - if ( $registry->is_registered( 'core/get-environment-info' ) ) { - $registry->unregister( 'core/get-environment-info' ); - } - - // Unregister categories if they exist. - $category_registry = WP_Ability_Categories_Registry::get_instance(); - if ( $category_registry->is_registered( 'site' ) ) { - wp_unregister_ability_category( 'site' ); - } - if ( $category_registry->is_registered( 'user' ) ) { - wp_unregister_ability_category( 'user' ); - } - - // Register core abilities category during the proper hook. - $callback = static function (): void { - wp_register_core_ability_categories(); - }; - add_action( 'wp_abilities_api_categories_init', $callback ); - do_action( 'wp_abilities_api_categories_init', WP_Ability_Categories_Registry::get_instance() ); - remove_action( 'wp_abilities_api_categories_init', $callback ); - - // Fire the init action if it hasn't been fired yet. - if ( ! did_action( 'wp_abilities_api_init' ) ) { - do_action( 'wp_abilities_api_init' ); - } - - // Register core abilities for testing. - wp_register_core_abilities(); - } - - /** - * Tests that the `core/get-site-info` ability is registered with the expected schema. - */ - public function test_core_get_bloginfo_ability_is_registered(): void { - $ability = wp_get_ability( 'core/get-site-info' ); - - $this->assertInstanceOf( WP_Ability::class, $ability ); - $this->assertTrue( $ability->get_meta_item( 'show_in_rest', false ) ); - - $input_schema = $ability->get_input_schema(); - $output_schema = $ability->get_output_schema(); - - $this->assertSame( 'object', $input_schema['type'] ); - $this->assertArrayHasKey( 'default', $input_schema ); - $this->assertSame( array(), $input_schema['default'] ); - - // Input schema should have optional fields array. - $this->assertArrayHasKey( 'fields', $input_schema['properties'] ); - $this->assertSame( 'array', $input_schema['properties']['fields']['type'] ); - $this->assertContains( 'name', $input_schema['properties']['fields']['items']['enum'] ); - - // Output schema should have all fields documented. - $this->assertArrayHasKey( 'name', $output_schema['properties'] ); - $this->assertArrayHasKey( 'url', $output_schema['properties'] ); - $this->assertArrayHasKey( 'version', $output_schema['properties'] ); - } - - /** - * Tests executing the `core/get-site-info` ability returns all fields by default. - */ - public function test_core_get_bloginfo_executes(): void { - // Requires manage_options. - $admin_id = self::factory()->user->create( array( 'role' => 'administrator' ) ); - wp_set_current_user( $admin_id ); - - $ability = wp_get_ability( 'core/get-site-info' ); - - // Test without fields parameter - should return all fields. - $result = $ability->execute(); - - $this->assertIsArray( $result ); - $this->assertArrayHasKey( 'name', $result ); - $this->assertArrayHasKey( 'description', $result ); - $this->assertArrayHasKey( 'url', $result ); - $this->assertArrayHasKey( 'version', $result ); - $this->assertSame( get_bloginfo( 'name' ), $result['name'] ); - - // Test with fields parameter - should return only requested fields. - $result = $ability->execute( - array( - 'fields' => array( 'name', 'url' ), - ) - ); - - $this->assertIsArray( $result ); - $this->assertArrayHasKey( 'name', $result ); - $this->assertArrayHasKey( 'url', $result ); - $this->assertArrayNotHasKey( 'description', $result ); - $this->assertArrayNotHasKey( 'version', $result ); - $this->assertSame( get_bloginfo( 'name' ), $result['name'] ); - $this->assertSame( get_bloginfo( 'url' ), $result['url'] ); - - wp_set_current_user( 0 ); - } - - /** - * Tests that executing the current user info ability requires authentication. - */ - public function test_core_get_current_user_info_requires_authentication(): void { - $ability = wp_get_ability( 'core/get-user-info' ); - - $this->assertFalse( $ability->check_permissions() ); - - $result = $ability->execute(); - $this->assertWPError( $result ); - $this->assertSame( 'ability_invalid_permissions', $result->get_error_code() ); - } - - /** - * Tests executing the current user info ability as an authenticated user. - */ - public function test_core_get_current_user_info_returns_user_data(): void { - $user_id = self::factory()->user->create( - array( - 'role' => 'subscriber', - 'locale' => 'fr_FR', - ) - ); - - wp_set_current_user( $user_id ); - - $ability = wp_get_ability( 'core/get-user-info' ); - - $this->assertTrue( $ability->check_permissions() ); - - $result = $ability->execute(); - $this->assertSame( $user_id, $result['id'] ); - $this->assertSame( 'fr_FR', $result['locale'] ); - $this->assertSame( 'subscriber', $result['roles'][0] ); - $this->assertSame( get_userdata( $user_id )->display_name, $result['display_name'] ); - - wp_set_current_user( 0 ); - } - - /** - * Tests executing the environment info ability. - */ - public function test_core_get_environment_type_executes(): void { - // Requires manage_options. - $admin_id = self::factory()->user->create( array( 'role' => 'administrator' ) ); - wp_set_current_user( $admin_id ); - - $ability = wp_get_ability( 'core/get-environment-info' ); - $environment = wp_get_environment_type(); - $ability_data = $ability->execute(); - - $this->assertIsArray( $ability_data ); - $this->assertArrayHasKey( 'environment', $ability_data ); - $this->assertArrayHasKey( 'php_version', $ability_data ); - $this->assertArrayHasKey( 'db_server_info', $ability_data ); - $this->assertArrayHasKey( 'wp_version', $ability_data ); - $this->assertSame( $environment, $ability_data['environment'] ); - - wp_set_current_user( 0 ); - } - -} diff --git a/tests/unit/abilities-api/wpRegisterAbility.php b/tests/unit/abilities-api/wpRegisterAbility.php index 21b108f..91a8aaf 100644 --- a/tests/unit/abilities-api/wpRegisterAbility.php +++ b/tests/unit/abilities-api/wpRegisterAbility.php @@ -29,10 +29,12 @@ class Test_Abilities_API_WpRegisterAbility extends WP_UnitTestCase { * Set up before each test. */ public function set_up(): void { + global $wp_current_filter; + parent::set_up(); - // Fire the init hook to allow test ability category registration. - do_action( 'wp_abilities_api_categories_init' ); + // Simulate the init hook for ability categories to allow test ability category registration. + $wp_current_filter[] = 'wp_abilities_api_categories_init'; wp_register_ability_category( 'math', array( @@ -86,6 +88,8 @@ public function set_up(): void { * Tear down after each test. */ public function tear_down(): void { + global $wp_current_filter; + foreach ( wp_get_abilities() as $ability ) { if ( ! str_starts_with( $ability->get_name(), 'test/' ) ) { continue; @@ -100,6 +104,15 @@ public function tear_down(): void { parent::tear_down(); } + /** + * Simulates the `wp_abilities_api_init` action. + */ + private function simulate_doing_wp_abilities_init_action() { + global $wp_current_filter; + + $wp_current_filter[] = 'wp_abilities_api_init'; + } + /** * Tests registering an ability with invalid name. * @@ -108,7 +121,7 @@ public function tear_down(): void { * @expectedIncorrectUsage WP_Abilities_Registry::register */ public function test_register_ability_invalid_name(): void { - do_action( 'wp_abilities_api_init' ); + $this->simulate_doing_wp_abilities_init_action(); $result = wp_register_ability( 'invalid_name', array() ); @@ -116,28 +129,17 @@ public function test_register_ability_invalid_name(): void { } /** - * Tests registering an ability when `abilities_api_init` action has not fired. + * Tests registering an ability when `wp_abilities_api_init` action has not fired. * * @ticket 64098 * * @expectedIncorrectUsage wp_register_ability */ public function test_register_ability_no_abilities_api_init_action(): void { - global $wp_actions; - - // Store the original action count. - $original_count = isset( $wp_actions['wp_abilities_api_init'] ) ? $wp_actions['wp_abilities_api_init'] : 0; - - // Reset the action count to simulate it not being fired. - unset( $wp_actions['wp_abilities_api_init'] ); + $this->assertFalse( doing_action( 'wp_abilities_api_init' ) ); $result = wp_register_ability( self::$test_ability_name, self::$test_ability_args ); - // Restore the original action count. - if ( $original_count > 0 ) { - $wp_actions['wp_abilities_api_init'] = $original_count; - } - $this->assertNull( $result ); } @@ -151,14 +153,14 @@ public function test_register_ability_no_abilities_api_init_action(): void { public function test_register_ability_no_init_action(): void { global $wp_actions; - do_action( 'wp_abilities_api_init' ); - // Store the original action count. $original_count = isset( $wp_actions['init'] ) ? $wp_actions['init'] : 0; // Reset the action count to simulate it not being fired. unset( $wp_actions['init'] ); + $this->simulate_doing_wp_abilities_init_action(); + $result = wp_register_ability( self::$test_ability_name, self::$test_ability_args ); // Restore the original action count. @@ -175,7 +177,7 @@ public function test_register_ability_no_init_action(): void { * @ticket 64098 */ public function test_register_valid_ability(): void { - do_action( 'wp_abilities_api_init' ); + $this->simulate_doing_wp_abilities_init_action(); $result = wp_register_ability( self::$test_ability_name, self::$test_ability_args ); @@ -225,7 +227,7 @@ public function test_register_valid_ability(): void { * @ticket 64098 */ public function test_register_ability_no_permissions(): void { - do_action( 'wp_abilities_api_init' ); + $this->simulate_doing_wp_abilities_init_action(); self::$test_ability_args['permission_callback'] = static function (): bool { return false; @@ -260,7 +262,7 @@ public function test_register_ability_no_permissions(): void { * @ticket 64098 */ public function test_register_ability_custom_ability_class(): void { - do_action( 'wp_abilities_api_init' ); + $this->simulate_doing_wp_abilities_init_action(); $result = wp_register_ability( self::$test_ability_name, @@ -302,7 +304,7 @@ public function test_register_ability_custom_ability_class(): void { * @ticket 64098 */ public function test_execute_ability_no_input_schema_match(): void { - do_action( 'wp_abilities_api_init' ); + $this->simulate_doing_wp_abilities_init_action(); $result = wp_register_ability( self::$test_ability_name, self::$test_ability_args ); @@ -331,7 +333,7 @@ public function test_execute_ability_no_input_schema_match(): void { * @ticket 64098 */ public function test_execute_ability_no_output_schema_match(): void { - do_action( 'wp_abilities_api_init' ); + $this->simulate_doing_wp_abilities_init_action(); self::$test_ability_args['execute_callback'] = static function (): bool { return true; @@ -362,7 +364,7 @@ public function test_execute_ability_no_output_schema_match(): void { * @ticket 64098 */ public function test_validate_input_no_input_schema_match(): void { - do_action( 'wp_abilities_api_init' ); + $this->simulate_doing_wp_abilities_init_action(); $result = wp_register_ability( self::$test_ability_name, self::$test_ability_args ); @@ -391,7 +393,7 @@ public function test_validate_input_no_input_schema_match(): void { * @ticket 64098 */ public function test_permission_callback_receives_input(): void { - do_action( 'wp_abilities_api_init' ); + $this->simulate_doing_wp_abilities_init_action(); $received_input = null; self::$test_ability_args['permission_callback'] = static function ( array $input ) use ( &$received_input ): bool { @@ -453,6 +455,8 @@ public function test_unregister_ability_no_init_action(): void { // Reset the action count to simulate it not being fired. unset( $wp_actions['init'] ); + $this->simulate_doing_wp_abilities_init_action(); + $result = wp_unregister_ability( self::$test_ability_name ); // Restore the original action count. @@ -469,7 +473,7 @@ public function test_unregister_ability_no_init_action(): void { * @ticket 64098 */ public function test_unregister_existing_ability() { - do_action( 'wp_abilities_api_init' ); + $this->simulate_doing_wp_abilities_init_action(); wp_register_ability( self::$test_ability_name, self::$test_ability_args ); @@ -497,6 +501,8 @@ public function test_get_ability_no_init_action(): void { // Reset the action count to simulate it not being fired. unset( $wp_actions['init'] ); + $this->simulate_doing_wp_abilities_init_action(); + $result = wp_get_ability( self::$test_ability_name ); // Restore the original action count. @@ -513,6 +519,8 @@ public function test_get_ability_no_init_action(): void { * @ticket 64098 */ public function test_get_existing_ability_using_callback() { + $this->simulate_doing_wp_abilities_init_action(); + $name = self::$test_ability_name; $args = self::$test_ability_args; $callback = static function ( $instance ) use ( $name, $args ) { @@ -556,6 +564,8 @@ public function test_has_ability_no_init_action(): void { // Reset the action count to simulate it not being fired. unset( $wp_actions['init'] ); + $this->simulate_doing_wp_abilities_init_action(); + $result = wp_has_ability( self::$test_ability_name ); // Restore the original action count. @@ -572,7 +582,7 @@ public function test_has_ability_no_init_action(): void { * @ticket 64098 */ public function test_has_registered_ability() { - do_action( 'wp_abilities_api_init' ); + $this->simulate_doing_wp_abilities_init_action(); wp_register_ability( self::$test_ability_name, self::$test_ability_args ); @@ -587,7 +597,7 @@ public function test_has_registered_ability() { * @ticket 64098 */ public function test_has_registered_nonexistent_ability() { - do_action( 'wp_abilities_api_init' ); + $this->simulate_doing_wp_abilities_init_action(); $result = wp_has_ability( 'test/non-existent' ); @@ -610,6 +620,8 @@ public function test_get_abilities_no_init_action(): void { // Reset the action count to simulate it not being fired. unset( $wp_actions['init'] ); + $this->simulate_doing_wp_abilities_init_action(); + $result = wp_get_abilities(); // Restore the original action count. @@ -626,7 +638,7 @@ public function test_get_abilities_no_init_action(): void { * @ticket 64098 */ public function test_get_all_registered_abilities() { - do_action( 'wp_abilities_api_init' ); + $this->simulate_doing_wp_abilities_init_action(); $ability_one_name = 'test/ability-one'; $ability_one_args = self::$test_ability_args; diff --git a/tests/unit/abilities-api/wpRegisterAbilityCategory.php b/tests/unit/abilities-api/wpRegisterAbilityCategory.php index ede55f9..169ea28 100644 --- a/tests/unit/abilities-api/wpRegisterAbilityCategory.php +++ b/tests/unit/abilities-api/wpRegisterAbilityCategory.php @@ -45,13 +45,24 @@ public function tear_down(): void { } /** - * Test registering ability category before `abilities_api_categories_init` hook. + * Simulates the `wp_abilities_api_categories_init` action. + */ + private function simulate_doing_wp_ability_categories_init_action() { + global $wp_current_filter; + + $wp_current_filter[] = 'wp_abilities_api_categories_init'; + } + + /** + * Test registering ability category before `wp_abilities_api_categories_init` hook. * * @ticket 64098 * * @expectedIncorrectUsage wp_register_ability_category */ public function test_register_category_before_init_hook(): void { + $this->assertFalse( doing_action( 'wp_abilities_api_categories_init' ) ); + $result = wp_register_ability_category( self::$test_ability_category_name, self::$test_ability_category_args @@ -70,14 +81,14 @@ public function test_register_category_before_init_hook(): void { public function test_register_ability_category_no_init_action(): void { global $wp_actions; - do_action( 'wp_abilities_api_categories_init' ); - // Store the original action count. $original_count = isset( $wp_actions['init'] ) ? $wp_actions['init'] : 0; // Reset the action count to simulate it not being fired. unset( $wp_actions['init'] ); + $this->simulate_doing_wp_ability_categories_init_action(); + $result = wp_register_ability_category( self::$test_ability_category_name, self::$test_ability_category_args @@ -97,7 +108,7 @@ public function test_register_ability_category_no_init_action(): void { * @ticket 64098 */ public function test_register_valid_category(): void { - do_action( 'wp_abilities_api_categories_init' ); + $this->simulate_doing_wp_ability_categories_init_action(); $result = wp_register_ability_category( self::$test_ability_category_name, @@ -120,14 +131,14 @@ public function test_register_valid_category(): void { public function test_unregister_ability_category_no_init_action(): void { global $wp_actions; - do_action( 'wp_abilities_api_categories_init' ); - // Store the original action count. $original_count = isset( $wp_actions['init'] ) ? $wp_actions['init'] : 0; // Reset the action count to simulate it not being fired. unset( $wp_actions['init'] ); + $this->simulate_doing_wp_ability_categories_init_action(); + $result = wp_unregister_ability_category( self::$test_ability_category_name ); // Restore the original action count. @@ -146,7 +157,7 @@ public function test_unregister_ability_category_no_init_action(): void { * @expectedIncorrectUsage WP_Ability_Categories_Registry::unregister */ public function test_unregister_nonexistent_category(): void { - do_action( 'wp_abilities_api_categories_init' ); + $this->simulate_doing_wp_ability_categories_init_action(); $result = wp_unregister_ability_category( 'test-nonexistent' ); @@ -159,7 +170,7 @@ public function test_unregister_nonexistent_category(): void { * @ticket 64098 */ public function test_unregister_existing_category(): void { - do_action( 'wp_abilities_api_categories_init' ); + $this->simulate_doing_wp_ability_categories_init_action(); wp_register_ability_category( self::$test_ability_category_name, @@ -182,14 +193,14 @@ public function test_unregister_existing_category(): void { public function test_has_ability_category_no_init_action(): void { global $wp_actions; - do_action( 'wp_abilities_api_categories_init' ); - // Store the original action count. $original_count = isset( $wp_actions['init'] ) ? $wp_actions['init'] : 0; // Reset the action count to simulate it not being fired. unset( $wp_actions['init'] ); + $this->simulate_doing_wp_ability_categories_init_action(); + $result = wp_has_ability_category( self::$test_ability_category_name ); // Restore the original action count. @@ -206,7 +217,7 @@ public function test_has_ability_category_no_init_action(): void { * @ticket 64098 */ public function test_has_registered_nonexistent_ability_category(): void { - do_action( 'wp_abilities_api_categories_init' ); + $this->simulate_doing_wp_ability_categories_init_action(); $result = wp_has_ability_category( 'test/non-existent' ); @@ -219,7 +230,7 @@ public function test_has_registered_nonexistent_ability_category(): void { * @ticket 64098 */ public function test_has_registered_ability_category(): void { - do_action( 'wp_abilities_api_categories_init' ); + $this->simulate_doing_wp_ability_categories_init_action(); $category_slug = self::$test_ability_category_name; @@ -243,14 +254,14 @@ public function test_has_registered_ability_category(): void { public function test_get_ability_category_no_init_action(): void { global $wp_actions; - do_action( 'wp_abilities_api_categories_init' ); - // Store the original action count. $original_count = isset( $wp_actions['init'] ) ? $wp_actions['init'] : 0; // Reset the action count to simulate it not being fired. unset( $wp_actions['init'] ); + $this->simulate_doing_wp_ability_categories_init_action(); + $result = wp_get_ability_category( self::$test_ability_category_name ); // Restore the original action count. @@ -269,7 +280,7 @@ public function test_get_ability_category_no_init_action(): void { * @expectedIncorrectUsage WP_Ability_Categories_Registry::get_registered */ public function test_get_nonexistent_category(): void { - do_action( 'wp_abilities_api_categories_init' ); + $this->simulate_doing_wp_ability_categories_init_action(); $result = wp_get_ability_category( 'test-nonexistent' ); @@ -316,14 +327,14 @@ public function test_get_existing_category_using_callback(): void { public function test_get_ability_categories_no_init_action(): void { global $wp_actions; - do_action( 'wp_abilities_api_categories_init' ); - // Store the original action count. $original_count = isset( $wp_actions['init'] ) ? $wp_actions['init'] : 0; // Reset the action count to simulate it not being fired. unset( $wp_actions['init'] ); + $this->simulate_doing_wp_ability_categories_init_action(); + $result = wp_get_ability_categories( self::$test_ability_category_name ); // Restore the original action count. @@ -340,7 +351,7 @@ public function test_get_ability_categories_no_init_action(): void { * @ticket 64098 */ public function test_get_all_categories(): void { - do_action( 'wp_abilities_api_categories_init' ); + $this->simulate_doing_wp_ability_categories_init_action(); wp_register_ability_category( self::$test_ability_category_name, diff --git a/tests/unit/abilities-api/wpRegisterCoreAbilities.php b/tests/unit/abilities-api/wpRegisterCoreAbilities.php new file mode 100644 index 0000000..c89d6da --- /dev/null +++ b/tests/unit/abilities-api/wpRegisterCoreAbilities.php @@ -0,0 +1,177 @@ +get_name() ); + } + foreach ( wp_get_ability_categories() as $ability_category ) { + wp_unregister_ability_category( $ability_category->get_slug() ); + } + + parent::tear_down_after_class(); + } + + /** + * Tests that the `core/get-site-info` ability is registered with the expected schema. + * @ticket 64146 + */ + public function test_core_get_site_info_ability_is_registered(): void { + $ability = wp_get_ability( 'core/get-site-info' ); + + $this->assertInstanceOf( WP_Ability::class, $ability ); + $this->assertTrue( $ability->get_meta_item( 'show_in_rest', false ) ); + + $input_schema = $ability->get_input_schema(); + $output_schema = $ability->get_output_schema(); + + $this->assertSame( 'object', $input_schema['type'] ); + $this->assertArrayHasKey( 'default', $input_schema ); + $this->assertSame( array(), $input_schema['default'] ); + + // Input schema should have optional fields array. + $this->assertArrayHasKey( 'fields', $input_schema['properties'] ); + $this->assertSame( 'array', $input_schema['properties']['fields']['type'] ); + $this->assertContains( 'name', $input_schema['properties']['fields']['items']['enum'] ); + + // Output schema should have all fields documented. + $this->assertArrayHasKey( 'name', $output_schema['properties'] ); + $this->assertArrayHasKey( 'url', $output_schema['properties'] ); + $this->assertArrayHasKey( 'version', $output_schema['properties'] ); + } + + /** + * Tests executing the `core/get-site-info` ability returns all fields by default. + * @ticket 64146 + */ + public function test_core_get_site_info_executes(): void { + // Requires manage_options. + $admin_id = self::factory()->user->create( array( 'role' => 'administrator' ) ); + wp_set_current_user( $admin_id ); + + $ability = wp_get_ability( 'core/get-site-info' ); + + // Test without fields parameter - should return all fields. + $result = $ability->execute(); + + $this->assertIsArray( $result ); + $this->assertArrayHasKey( 'name', $result ); + $this->assertArrayHasKey( 'description', $result ); + $this->assertArrayHasKey( 'url', $result ); + $this->assertArrayHasKey( 'version', $result ); + $this->assertSame( get_bloginfo( 'name' ), $result['name'] ); + + // Test with fields parameter - should return only requested fields. + $result = $ability->execute( + array( + 'fields' => array( 'name', 'url' ), + ) + ); + + $this->assertIsArray( $result ); + $this->assertCount( 2, $result ); + $this->assertSame( get_bloginfo( 'name' ), $result['name'] ); + $this->assertSame( get_bloginfo( 'url' ), $result['url'] ); + } + + /** + * Tests that executing the current user info ability requires authentication. + * @ticket 64146 + */ + public function test_core_get_current_user_info_requires_authentication(): void { + $ability = wp_get_ability( 'core/get-user-info' ); + + $this->assertFalse( $ability->check_permissions() ); + + $result = $ability->execute(); + $this->assertWPError( $result ); + $this->assertSame( 'ability_invalid_permissions', $result->get_error_code() ); + } + + /** + * Tests executing the current user info ability as an authenticated user. + * @ticket 64146 + */ + public function test_core_get_current_user_info_returns_user_data(): void { + $user_id = self::factory()->user->create( + array( + 'role' => 'subscriber', + 'locale' => 'fr_FR', + ) + ); + + wp_set_current_user( $user_id ); + + $ability = wp_get_ability( 'core/get-user-info' ); + + $this->assertTrue( $ability->check_permissions() ); + + $result = $ability->execute(); + $this->assertSame( $user_id, $result['id'] ); + $this->assertSame( 'fr_FR', $result['locale'] ); + $this->assertSame( 'subscriber', $result['roles'][0] ); + $this->assertSame( get_userdata( $user_id )->display_name, $result['display_name'] ); + } + + /** + * Tests executing the environment info ability. + * @ticket 64146 + */ + public function test_core_get_environment_info_executes(): void { + // Requires manage_options. + $admin_id = self::factory()->user->create( array( 'role' => 'administrator' ) ); + wp_set_current_user( $admin_id ); + + $ability = wp_get_ability( 'core/get-environment-info' ); + $environment = wp_get_environment_type(); + $ability_data = $ability->execute(); + + $this->assertIsArray( $ability_data ); + $this->assertArrayHasKey( 'environment', $ability_data ); + $this->assertArrayHasKey( 'php_version', $ability_data ); + $this->assertArrayHasKey( 'db_server_info', $ability_data ); + $this->assertArrayHasKey( 'wp_version', $ability_data ); + $this->assertSame( $environment, $ability_data['environment'] ); + } +} diff --git a/tests/unit/rest-api/wpRestAbilitiesV1CategoriesController.php b/tests/unit/rest-api/wpRestAbilitiesV1CategoriesController.php index a6b7f23..2fa665e 100644 --- a/tests/unit/rest-api/wpRestAbilitiesV1CategoriesController.php +++ b/tests/unit/rest-api/wpRestAbilitiesV1CategoriesController.php @@ -62,8 +62,6 @@ public function set_up(): void { do_action( 'rest_api_init' ); - // Initialize the API and register test ability categories. - do_action( 'wp_abilities_api_categories_init' ); $this->register_test_ability_categories(); wp_set_current_user( self::$admin_user_id ); @@ -93,6 +91,10 @@ public function tear_down(): void { * Register test ability categories for testing. */ public function register_test_ability_categories(): void { + // Simulates the init hook to allow test ability categories registration. + global $wp_current_filter; + $wp_current_filter[] = 'wp_abilities_api_categories_init'; + wp_register_ability_category( 'test-data-retrieval', array( @@ -130,6 +132,8 @@ public function register_test_ability_categories(): void { ) ); } + + array_pop( $wp_current_filter ); } /** diff --git a/tests/unit/rest-api/wpRestAbilitiesV1ListController.php b/tests/unit/rest-api/wpRestAbilitiesV1ListController.php index a6e5d92..19e3522 100644 --- a/tests/unit/rest-api/wpRestAbilitiesV1ListController.php +++ b/tests/unit/rest-api/wpRestAbilitiesV1ListController.php @@ -37,8 +37,6 @@ public static function set_up_before_class(): void { ) ); - // Fire the init hook to allow test ability categories registration. - do_action( 'wp_abilities_api_categories_init' ); self::register_test_categories(); } @@ -67,8 +65,6 @@ public function set_up(): void { do_action( 'rest_api_init' ); - // Initialize Abilities API. - do_action( 'wp_abilities_api_init' ); $this->register_test_abilities(); // Set default user for tests @@ -99,6 +95,10 @@ public function tear_down(): void { * Register test categories for testing. */ public static function register_test_categories(): void { + // Simulates the init hook to allow test ability categories registration. + global $wp_current_filter; + $wp_current_filter[] = 'wp_abilities_api_categories_init'; + wp_register_ability_category( 'math', array( @@ -122,6 +122,24 @@ public static function register_test_categories(): void { 'description' => 'General purpose abilities.', ) ); + + array_pop( $wp_current_filter ); + } + + /** + * Helper to register a test ability. + * + * @param string $name Ability name. + * @param array $args Ability arguments. + */ + private function register_test_ability( string $name, array $args ): void { + // Simulates the init hook to allow test abilities registration. + global $wp_current_filter; + $wp_current_filter[] = 'wp_abilities_api_init'; + + wp_register_ability( $name, $args ); + + array_pop( $wp_current_filter ); } /** @@ -129,7 +147,7 @@ public static function register_test_categories(): void { */ private function register_test_abilities(): void { // Register a regular ability. - wp_register_ability( + $this->register_test_ability( 'test/calculator', array( 'label' => 'Calculator', @@ -173,7 +191,7 @@ private function register_test_abilities(): void { ); // Register a read-only ability. - wp_register_ability( + $this->register_test_ability( 'test/system-info', array( 'label' => 'System Info', @@ -220,7 +238,7 @@ private function register_test_abilities(): void { ); // Ability that does not show in REST. - wp_register_ability( + $this->register_test_ability( 'test/not-show-in-rest', array( 'label' => 'Hidden from REST', @@ -235,7 +253,7 @@ private function register_test_abilities(): void { // Register multiple abilities for pagination testing for ( $i = 1; $i <= 60; $i++ ) { - wp_register_ability( + $this->register_test_ability( "test/ability-{$i}", array( 'label' => "Test Ability {$i}", @@ -589,7 +607,7 @@ public function test_get_schema(): void { */ public function test_ability_name_with_valid_special_characters(): void { // Register ability with hyphen (valid). - wp_register_ability( + $this->register_test_ability( 'test-hyphen/ability', array( 'label' => 'Test Hyphen Ability', diff --git a/tests/unit/rest-api/wpRestAbilitiesV1RunController.php b/tests/unit/rest-api/wpRestAbilitiesV1RunController.php index 40372fe..f4340f8 100644 --- a/tests/unit/rest-api/wpRestAbilitiesV1RunController.php +++ b/tests/unit/rest-api/wpRestAbilitiesV1RunController.php @@ -49,8 +49,6 @@ public static function set_up_before_class(): void { ) ); - // Fire the init hook to allow test ability categories registration. - do_action( 'wp_abilities_api_categories_init' ); self::register_test_categories(); } @@ -78,8 +76,6 @@ public function set_up(): void { do_action( 'rest_api_init' ); - // Initialize Abilities API. - do_action( 'wp_abilities_api_init' ); $this->register_test_abilities(); // Set default user for tests @@ -109,6 +105,10 @@ public function tear_down(): void { * Register test categories for testing. */ public static function register_test_categories(): void { + // Simulates the init hook to allow test ability category registration. + global $wp_current_filter; + $wp_current_filter[] = 'wp_abilities_api_categories_init'; + wp_register_ability_category( 'math', array( @@ -132,6 +132,24 @@ public static function register_test_categories(): void { 'description' => 'General purpose abilities.', ) ); + + array_pop( $wp_current_filter ); + } + + /** + * Helper to register a test ability. + * + * @param string $name Ability name. + * @param array $args Ability arguments. + */ + private function register_test_ability( string $name, array $args ): void { + // Simulates the init hook to allow test abilities registration. + global $wp_current_filter; + $wp_current_filter[] = 'wp_abilities_api_init'; + + wp_register_ability( $name, $args ); + + array_pop( $wp_current_filter ); } /** @@ -139,7 +157,7 @@ public static function register_test_categories(): void { */ private function register_test_abilities(): void { // Regular ability (POST only). - wp_register_ability( + $this->register_test_ability( 'test/calculator', array( 'label' => 'Calculator', @@ -176,7 +194,7 @@ private function register_test_abilities(): void { ); // Read-only ability (GET method). - wp_register_ability( + $this->register_test_ability( 'test/user-info', array( 'label' => 'User Info', @@ -222,7 +240,7 @@ private function register_test_abilities(): void { ); // Destructive ability (DELETE method). - wp_register_ability( + $this->register_test_ability( 'test/delete-user', array( 'label' => 'Delete User', @@ -263,7 +281,7 @@ private function register_test_abilities(): void { ); // Ability with contextual permissions - wp_register_ability( + $this->register_test_ability( 'test/restricted', array( 'label' => 'Restricted Action', @@ -294,7 +312,7 @@ private function register_test_abilities(): void { ); // Ability that does not show in REST. - wp_register_ability( + $this->register_test_ability( 'test/not-show-in-rest', array( 'label' => 'Hidden from REST', @@ -308,7 +326,7 @@ private function register_test_abilities(): void { ); // Ability that returns null - wp_register_ability( + $this->register_test_ability( 'test/null-return', array( 'label' => 'Null Return', @@ -325,7 +343,7 @@ private function register_test_abilities(): void { ); // Ability that returns WP_Error - wp_register_ability( + $this->register_test_ability( 'test/error-return', array( 'label' => 'Error Return', @@ -342,7 +360,7 @@ private function register_test_abilities(): void { ); // Ability with invalid output - wp_register_ability( + $this->register_test_ability( 'test/invalid-output', array( 'label' => 'Invalid Output', @@ -362,7 +380,7 @@ private function register_test_abilities(): void { ); // Read-only ability for query params testing. - wp_register_ability( + $this->register_test_ability( 'test/query-params', array( 'label' => 'Query Params Test', @@ -463,7 +481,7 @@ public function test_execute_destructive_ability_delete(): void { * @ticket 64098 */ public function test_regular_ability_requires_post(): void { - wp_register_ability( + $this->register_test_ability( 'test/open-tool', array( 'label' => 'Open Tool', @@ -793,7 +811,7 @@ public function test_post_request_with_non_array_input(): void { */ public function test_output_validation_failure_returns_error(): void { // Register ability with strict output schema. - wp_register_ability( + $this->register_test_ability( 'test/strict-output', array( 'label' => 'Strict Output', @@ -842,7 +860,7 @@ public function test_output_validation_failure_returns_error(): void { */ public function test_input_validation_failure_returns_error(): void { // Register ability with strict input schema. - wp_register_ability( + $this->register_test_ability( 'test/strict-input', array( 'label' => 'Strict Input', @@ -891,7 +909,7 @@ public function test_input_validation_failure_returns_error(): void { */ public function test_ability_without_annotations_defaults_to_post_method(): void { // Register ability without annotations. - wp_register_ability( + $this->register_test_ability( 'test/no-annotations', array( 'label' => 'No Annotations', @@ -926,7 +944,7 @@ public function test_ability_without_annotations_defaults_to_post_method(): void * @ticket 64098 */ public function test_empty_input_handling_get_method(): void { - wp_register_ability( + $this->register_test_ability( 'test/read-only-empty', array( 'label' => 'Read-only Empty', @@ -958,7 +976,7 @@ public function test_empty_input_handling_get_method(): void { * @ticket 64098 */ public function test_empty_input_handling_get_method_with_normalized_input(): void { - wp_register_ability( + $this->register_test_ability( 'test/read-only-empty-array', array( 'label' => 'Read-only Empty Array', @@ -994,7 +1012,7 @@ public function test_empty_input_handling_get_method_with_normalized_input(): vo * @ticket 64098 */ public function test_empty_input_handling_post_method(): void { - wp_register_ability( + $this->register_test_ability( 'test/regular-empty', array( 'label' => 'Regular Empty', @@ -1065,7 +1083,7 @@ public function test_malformed_json_post_body( string $json ): void { */ public function test_php_type_strings_in_input(): void { // Register ability that accepts any input - wp_register_ability( + $this->register_test_ability( 'test/echo', array( 'label' => 'Echo', @@ -1114,7 +1132,7 @@ public function test_php_type_strings_in_input(): void { */ public function test_mixed_encoding_in_input(): void { // Register ability that accepts any input - wp_register_ability( + $this->register_test_ability( 'test/echo-encoding', array( 'label' => 'Echo Encoding', @@ -1183,7 +1201,7 @@ public function data_invalid_http_methods_provider(): array { */ public function test_invalid_http_methods( string $method ): void { // Register an ability with no permission requirements for this test - wp_register_ability( + $this->register_test_ability( 'test/method-test', array( 'label' => 'Method Test',