From 5f7449248ac94073f226456eb7199e344c7bf1ee Mon Sep 17 00:00:00 2001 From: Grzegorz Ziolkowski Date: Fri, 8 Aug 2025 12:17:35 +0200 Subject: [PATCH 1/6] Add server-side registry --- .editorconfig | 24 ++ phpunit.xml.dist | 20 ++ src/abilities-api.php | 87 +++++ src/class-wp-abilities-registry.php | 285 +++++++++++++++++ src/class-wp-ability.php | 331 +++++++++++++++++++ tests/bootstrap.php | 0 tests/unit/AbilitiesAPITest.php | 269 ++++++++++++++++ tests/unit/WPAbilitiesRegistryTest.php | 425 +++++++++++++++++++++++++ 8 files changed, 1441 insertions(+) create mode 100644 .editorconfig create mode 100644 phpunit.xml.dist create mode 100644 src/abilities-api.php create mode 100644 src/class-wp-abilities-registry.php create mode 100644 src/class-wp-ability.php create mode 100644 tests/bootstrap.php create mode 100644 tests/unit/AbilitiesAPITest.php create mode 100644 tests/unit/WPAbilitiesRegistryTest.php diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..c6f3cf4 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,24 @@ +# This file is for unifying the coding style for different editors and IDEs +# editorconfig.org + +# WordPress Coding Standards +# https://make.wordpress.org/core/handbook/coding-standards/ + +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +indent_style = tab + +[*.yml] +indent_style = space +indent_size = 2 + +[*.md] +trim_trailing_whitespace = false + +[{*.txt,wp-config-sample.php}] +end_of_line = crlf diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..a50c6a6 --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,20 @@ + + + + + tests/unit + + + diff --git a/src/abilities-api.php b/src/abilities-api.php new file mode 100644 index 0000000..dabc211 --- /dev/null +++ b/src/abilities-api.php @@ -0,0 +1,87 @@ +abilities_api_init', + '' . esc_attr( $name ) . '' + ), + '0.1.0' + ); + return null; + } + + return WP_Abilities_Registry::get_instance()->register( $name, $properties ); +} + +/** + * Unregisters an ability using Abilities API. + * + * @see WP_Abilities_Registry::unregister() + * + * @since 0.1.0 + * + * @param string $name The name of the registered ability, with its namespace. + * @return ?WP_Ability The unregistered ability instance on success, null on failure. + */ +function wp_unregister_ability( string $name ): ?WP_Ability { + return WP_Abilities_Registry::get_instance()->unregister( $name ); +} + +/** + * Retrieves a registered ability using Abilities API. + * + * @see WP_Abilities_Registry::get_registered() + * + * @since 0.1.0 + * + * @param string $name The name of the registered ability, with its namespace. + * @return ?WP_Ability The registered ability instance, or null if it is not registered. + */ +function wp_get_ability( string $name ): ?WP_Ability { + return WP_Abilities_Registry::get_instance()->get_registered( $name ); +} + +/** + * Retrieves all registered abilities using Abilities API. + * + * @see WP_Abilities_Registry::get_all_registered() + * + * @since 0.1.0 + * + * @return WP_Ability[] The array of registered abilities. + */ +function wp_get_abilities(): array { + return WP_Abilities_Registry::get_instance()->get_all_registered(); +} diff --git a/src/class-wp-abilities-registry.php b/src/class-wp-abilities-registry.php new file mode 100644 index 0000000..50bcb1e --- /dev/null +++ b/src/class-wp-abilities-registry.php @@ -0,0 +1,285 @@ +get_name(); + } + + if ( ! preg_match( '/^[a-z0-9-]+\/[a-z0-9-]+$/', $name ) ) { + _doing_it_wrong( + __METHOD__, + esc_html__( + 'Ability name must be a string containing a namespace prefix, i.e. "my-plugin/my-ability". It can only contain lowercase alphanumeric characters, dashes and the forward slash.' + ), + '0.1.0' + ); + return null; + } + + if ( $this->is_registered( $name ) ) { + _doing_it_wrong( + __METHOD__, + /* translators: %s: Ability name. */ + esc_html( sprintf( __( 'Ability "%s" is already registered.' ), $name ) ), + '0.1.0' + ); + return null; + } + + // If the ability is already an instance, we can skip the rest of the validation. + if ( null !== $ability ) { + $this->registered_abilities[ $name ] = $ability; + return $ability; + } + + if ( empty( $properties['label'] ) || ! is_string( $properties['label'] ) ) { + _doing_it_wrong( + __METHOD__, + esc_html__( 'The ability properties must contain a `label` string.' ), + '0.1.0' + ); + return null; + } + + if ( empty( $properties['description'] ) || ! is_string( $properties['description'] ) ) { + _doing_it_wrong( + __METHOD__, + esc_html__( 'The ability properties must contain a `description` string.' ), + '0.1.0' + ); + return null; + } + + if ( isset( $properties['input_schema'] ) && ! is_array( $properties['input_schema'] ) ) { + _doing_it_wrong( + __METHOD__, + esc_html__( 'The ability properties should provide a valid `input_schema` definition.' ), + '0.1.0' + ); + return null; + } + + if ( isset( $properties['output_schema'] ) && ! is_array( $properties['output_schema'] ) ) { + _doing_it_wrong( + __METHOD__, + esc_html__( 'The ability properties should provide a valid `output_schema` definition.' ), + '0.1.0' + ); + return null; + } + + if ( empty( $properties['execute_callback'] ) || ! is_callable( $properties['execute_callback'] ) ) { + _doing_it_wrong( + __METHOD__, + esc_html__( 'The ability properties must contain a valid `execute_callback` function.' ), + '0.1.0' + ); + return null; + } + + if ( isset( $properties['permission_callback'] ) && ! is_callable( $properties['permission_callback'] ) ) { + _doing_it_wrong( + __METHOD__, + esc_html__( 'The ability properties should provide a valid `permission_callback` function.' ), + '0.1.0' + ); + return null; + } + + if ( isset( $properties['meta'] ) && ! is_array( $properties['meta'] ) ) { + _doing_it_wrong( + __METHOD__, + esc_html__( 'The ability properties should provide a valid `meta` array.' ), + '0.1.0' + ); + return null; + } + + $ability = new WP_Ability( + $name, + array( + 'label' => $properties['label'], + 'description' => $properties['description'], + 'input_schema' => $properties['input_schema'] ?? [], + 'output_schema' => $properties['output_schema'] ?? [], + 'execute_callback' => $properties['execute_callback'], + 'permission_callback' => $properties['permission_callback'] ?? null, + 'meta' => $properties['meta'] ?? [], + ) + ); + $this->registered_abilities[ $name ] = $ability; + return $ability; + } + + /** + * Unregisters an ability. + * + * Do not use this method directly. Instead, use the `wp_unregister_ability()` function. + * + * @see wp_unregister_ability() + * + * @since 0.1.0 + * + * @param string $name The name of the registered ability, with its namespace. + * @return ?WP_Ability The unregistered ability instance on success, null on failure. + */ + public function unregister( $name ): ?WP_Ability { + if ( ! $this->is_registered( $name ) ) { + _doing_it_wrong( + __METHOD__, + /* translators: %s: Ability name. */ + sprintf( esc_html__( 'Ability "%s" not found.' ), esc_attr( $name ) ), + '0.1.0' + ); + return null; + } + + $unregistered_ability = $this->registered_abilities[ $name ]; + unset( $this->registered_abilities[ $name ] ); + + return $unregistered_ability; + } + + /** + * Retrieves the list of all registered abilities. + * + * Do not use this method directly. Instead, use the `wp_get_abilities()` function. + * + * @see wp_get_abilities() + * + * @since 0.1.0 + * + * @return WP_Ability[] The array of registered abilities. + */ + public function get_all_registered(): array { + return $this->registered_abilities; + } + + /** + * Checks if an ability is registered. + * + * @since 0.1.0 + * + * @param string $name The name of the registered ability, with its namespace. + * @return bool True if the ability is registered, false otherwise. + */ + public function is_registered( $name ): bool { + return isset( $this->registered_abilities[ $name ] ); + } + + /** + * Retrieves a registered ability. + * + * Do not use this method directly. Instead, use the `wp_get_ability()` function. + * + * @see wp_get_ability() + * + * @since 0.1.0 + * + * @param string $name The name of the registered ability, with its namespace. + * @return WP_Ability|null The registered ability instance, or null if it is not registered. + */ + public function get_registered( $name ): ?WP_Ability { + return $this->registered_abilities[ $name ] ?? null; + } + + /** + * Utility method to retrieve the main instance of the registry class. + * + * The instance will be created if it does not exist yet. + * + * @since 0.1.0 + * + * @return WP_Abilities_Registry The main registry instance. + */ + public static function get_instance(): WP_Abilities_Registry { + /* @var WP_Abilities_Registry $wp_abilities */ + global $wp_abilities; + + if ( empty( $wp_abilities ) ) { + $wp_abilities = new self(); + /** + * Fires when preparing abilities registry. + * + * Abilities should be created and register their hooks on this action rather + * than another action to ensure they're only loaded when needed. + * + * @since 0.1.0 + * + * @param WP_Abilities_Registry $instance Abilities registry object. + */ + do_action( 'abilities_api_init', $wp_abilities ); + } + + return $wp_abilities; + } + + /** + * Wakeup magic method. + * + * @since 0.1.0 + */ + public function __wakeup(): void { + if ( empty( $this->registered_abilities ) ) { + return; + } + + foreach ( $this->registered_abilities as $ability ) { + if ( ! $ability instanceof WP_Ability ) { + throw new UnexpectedValueException(); + } + } + } +} diff --git a/src/class-wp-ability.php b/src/class-wp-ability.php new file mode 100644 index 0000000..63ce4f0 --- /dev/null +++ b/src/class-wp-ability.php @@ -0,0 +1,331 @@ +name = $name; + foreach ( $properties as $property_name => $property_value ) { + $this->$property_name = $property_value; + } + } + + /** + * Retrieves the name of the ability, with its namespace. + * Example: `my-plugin/my-ability`. + * + * @since 0.1.0 + * + * @return string The ability name, with its namespace. + */ + public function get_name(): string { + return $this->name; + } + + /** + * Retrieves the human-readable label for the ability. + * + * @since 0.1.0 + * + * @return string The human-readable ability label. + */ + public function get_label(): string { + return $this->label; + } + + /** + * Retrieves the detailed description for the ability. + * + * @since 0.1.0 + * + * @return string The detailed description for the ability. + */ + public function get_description(): string { + return $this->description; + } + + /** + * Retrieves the input schema for the ability. + * + * @since 0.1.0 + * + * @return array The input schema for the ability. + */ + public function get_input_schema(): array { + return $this->input_schema; + } + + /** + * Retrieves the output schema for the ability. + * + * @since 0.1.0 + * + * @return array The output schema for the ability. + */ + public function get_output_schema(): array { + return $this->output_schema; + } + + /** + * Retrieves the metadata for the ability. + * + * @since 0.1.0 + * + * @return array The metadata for the ability. + */ + public function get_meta(): array { + return $this->meta; + } + + /** + * Validates input data against the input schema. + * + * @since 0.1.0 + * + * @param array $input Optional. The input data to validate. + * @return bool Returns true if valid, false if validation fails. + */ + protected function validate_input( array $input = array() ): bool { + $input_schema = $this->get_input_schema(); + if ( empty( $input_schema ) ) { + return true; + } + + $valid_input = rest_validate_value_from_schema( $input, $input_schema ); + if ( is_wp_error( $valid_input ) ) { + _doing_it_wrong( + __METHOD__, + esc_html( + sprintf( + /* translators: %1$s ability name, %2$s error message. */ + __( 'Invalid input provided for ability "%1$s": %2$s.' ), + $this->name, + $valid_input->get_error_message() + ), + ), + '0.1.0' + ); + return false; + } + + return true; + } + + /** + * Checks whether the ability has the necessary permissions. + * If the permission callback is not set, the default behavior is to allow access + * when the input provided passes validation. + * + * @since 0.1.0 + * + * @param array $input Optional. The input data for permission checking. + * @return bool Whether the ability has the necessary permission. + */ + public function has_permission( array $input = array() ): bool { + if ( ! $this->validate_input( $input ) ) { + return false; + } + + if ( ! is_callable( $this->permission_callback ) ) { + return true; + } + + return call_user_func( $this->permission_callback, $input ); + } + + /** + * Executes the ability callback. + * + * @since 0.1.0 + * + * @param array $input The input data for the ability. + * @return mixed|WP_Error The result of the ability execution, or WP_Error on failure. + */ + protected function do_execute( array $input ) { + if ( ! is_callable( $this->execute_callback ) ) { + _doing_it_wrong( + __METHOD__, + esc_html( + /* translators: %s ability name. */ + sprintf( __( 'Ability "%s" does not have a valid execute callback.' ), $this->name ), + ), + '0.1.0' + ); + return null; + } + return call_user_func( $this->execute_callback, $input ); + } + + /** + * Validates output data against the output schema. + * + * @since 0.1.0 + * + * @param mixed $output The output data to validate. + * @return bool Returns true if valid, false if validation fails. + */ + protected function validate_output( $output ): bool { + $output_schema = $this->get_output_schema(); + if ( empty( $output_schema ) ) { + return true; + } + + $valid_output = rest_validate_value_from_schema( $output, $output_schema ); + if ( is_wp_error( $valid_output ) ) { + _doing_it_wrong( + __METHOD__, + esc_html( + sprintf( + /* translators: %1$s ability name, %2$s error message. */ + __( 'Invalid output provided for ability "%1$s": %2$s.' ), + $this->name, + $valid_output->get_error_message() + ), + ), + '0.1.0' + ); + return false; + } + + return true; + } + + /** + * Executes the ability after input validation and running a permission check. + * Before returning the return value, it also validates the output. + * + * @since 0.1.0 + * + * @param array $input Optional. The input data for the ability. + * @return mixed|WP_Error The result of the ability execution, or WP_Error on failure. + */ + public function execute( array $input = array() ) { + if ( ! $this->has_permission( $input ) ) { + _doing_it_wrong( + __METHOD__, + esc_html( + /* translators: %s ability name. */ + sprintf( __( 'Ability "%s" does not have necessary permission.' ), $this->name ) + ), + '0.1.0' + ); + return null; + } + + $result = $this->do_execute( $input ); + if ( is_wp_error( $result ) ) { + return $result; + } + + if ( ! $this->validate_output( $result ) ) { + return null; + } + + return $result; + } + + /** + * Wakeup magic method. + * + * @since 0.1.0 + */ + public function __wakeup(): void { + throw new \LogicException( __CLASS__ . ' should never be unserialized.' ); + } +} diff --git a/tests/bootstrap.php b/tests/bootstrap.php new file mode 100644 index 0000000..e69de29 diff --git a/tests/unit/AbilitiesAPITest.php b/tests/unit/AbilitiesAPITest.php new file mode 100644 index 0000000..3a4e7f8 --- /dev/null +++ b/tests/unit/AbilitiesAPITest.php @@ -0,0 +1,269 @@ + 'Add numbers', + 'description' => 'Calculates the result of adding two numbers.', + 'input_schema' => [ + 'type' => 'object', + 'properties' => [ + 'a' => [ + 'type' => 'number', + 'description' => 'First number.', + 'required' => true, + ], + 'b' => [ + 'type' => 'number', + 'description' => 'Second number.', + 'required' => true, + ], + ], + 'additionalProperties' => false, + ], + 'output_schema' => [ + 'type' => 'number', + 'description' => 'The result of adding the two numbers.', + 'required' => true, + ], + 'execute_callback' => function ( array $input ): int { + return $input['a'] + $input['b']; + }, + 'permission_callback' => function (): bool { + return true; + }, + 'meta' => [ + 'category' => 'math', + ], + ]; + } + + /** + * Tear down after each test. + */ + public function tear_down(): void { + foreach ( wp_get_abilities() as $ability ) { + if ( str_starts_with( $ability->get_name(), 'test/' ) ) { + wp_unregister_ability( $ability->get_name() ); + } + } + + parent::tear_down(); + } + + /** + * Tests registering an ability with invalid name. + * + * @expectedIncorrectUsage wp_register_ability + */ + public function test_register_ability_invalid_name(): void { + do_action( 'abilities_api_init' ); + + $result = wp_register_ability( 'invalid_name', [] ); + + $this->assertNull( $result ); + } + + /** + * Tests registering an ability when `abilities_api_init` hook is not fired. + * + * @expectedIncorrectUsage wp_register_ability + */ + public function test_register_ability_no_abilities_api_init_hook(): void { + $result = wp_register_ability( self::$test_ability_name, self::$test_ability_properties ); + + $this->assertNull( $result ); + } + + /** + * Tests registering a valid ability. + */ + public function test_register_valid_ability(): void { + do_action( 'abilities_api_init' ); + + $result = wp_register_ability( self::$test_ability_name, self::$test_ability_properties ); + + $this->assertInstanceOf( WP_Ability::class, $result ); + $this->assertSame( self::$test_ability_name, $result->get_name() ); + $this->assertSame( self::$test_ability_properties['label'], $result->get_label() ); + $this->assertSame( self::$test_ability_properties['description'], $result->get_description() ); + $this->assertSame( self::$test_ability_properties['input_schema'], $result->get_input_schema() ); + $this->assertSame( self::$test_ability_properties['output_schema'], $result->get_output_schema() ); + $this->assertSame( self::$test_ability_properties['meta'], $result->get_meta() ); + $this->assertTrue( $result->has_permission( [ 'a' => 2, 'b' => 3 ] ) ); + $this->assertSame( 5, $result->execute( [ 'a' => 2, 'b' => 3 ] ) ); + } + + /** + * Tests executing an ability with no permissions. + * + * @expectedIncorrectUsage WP_Ability::execute + */ + public function test_register_ability_no_permissions(): void { + do_action( 'abilities_api_init' ); + + self::$test_ability_properties['permission_callback'] = function (): bool { + return false; + }; + $result = wp_register_ability( self::$test_ability_name, self::$test_ability_properties ); + + $this->assertFalse( $result->has_permission( [ 'a' => 2, 'b' => 3 ] ) ); + $this->assertNull( $result->execute( [ 'a' => 2, 'b' => 3 ] ) ); + } + + /** + * Tests executing an ability with input not matching schema. + * + * @expectedIncorrectUsage WP_Ability::execute + */ + public function test_execute_ability_no_input_schema_match(): void { + do_action( 'abilities_api_init' ); + + $result = wp_register_ability( self::$test_ability_name, self::$test_ability_properties ); + + $this->assertNull( $result->execute( [ 'a' => 2, 'b' => 3, 'unknown' => 1 ] ) ); + } + + /** + * Tests executing an ability with output not matching schema. + * + * @expectedIncorrectUsage WP_Ability::execute + */ + public function test_execute_ability_no_output_schema_match(): void { + do_action( 'abilities_api_init' ); + + self::$test_ability_properties['execute_callback'] = function (): bool { + return true; + }; + $result = wp_register_ability( self::$test_ability_name, self::$test_ability_properties ); + + $this->assertNull( $result->execute( [ 'a' => 2, 'b' => 3 ] ) ); + } + + /** + * Tests permission callback receiving input not matching schema. + * + * @expectedIncorrectUsage WP_Ability::has_permission + */ + public function test_permission_callback_no_input_schema_match(): void { + do_action( 'abilities_api_init' ); + + $result = wp_register_ability( self::$test_ability_name, self::$test_ability_properties ); + + $this->assertFalse( $result->has_permission( [ 'a' => 2, 'b' => 3, 'unknown' => 1 ] ) ); + } + + /** + * Tests permission callback receiving input for contextual permission checks. + */ + public function test_permission_callback_receives_input(): void { + do_action( 'abilities_api_init' ); + + $received_input = null; + self::$test_ability_properties['permission_callback'] = function ( array $input ) use ( &$received_input ): bool { + $received_input = $input; + // Allow only if 'a' is greater than 'b' + return $input['a'] > $input['b']; + }; + $result = wp_register_ability( self::$test_ability_name, self::$test_ability_properties ); + + // Test with a > b (should be allowed) + $this->assertTrue( $result->has_permission( [ 'a' => 5, 'b' => 3 ] ) ); + $this->assertSame( [ 'a' => 5, 'b' => 3 ], $received_input ); + + // Test with a < b (should be denied) + $this->assertFalse( $result->has_permission( [ 'a' => 2, 'b' => 8 ] ) ); + $this->assertSame( [ 'a' => 2, 'b' => 8 ], $received_input ); + } + + /** + * Tests unregistering existing ability. + */ + public function test_unregister_existing_ability() { + do_action( 'abilities_api_init' ); + + wp_register_ability( self::$test_ability_name, self::$test_ability_properties ); + + $result = wp_unregister_ability( self::$test_ability_name ); + + $this->assertEquals( + new WP_Ability( self::$test_ability_name, self::$test_ability_properties ), + $result + ); + } + + /** + * Tests retrieving existing ability. + */ + public function test_get_existing_ability() { + global $wp_abilities; + + $name = self::$test_ability_name; + $properties = self::$test_ability_properties; + $callback = function ( $instance ) use ( $name, $properties ) { + wp_register_ability( $name, $properties ); + }; + + add_action( 'abilities_api_init', $callback ); + + // Temporarily set `$wp_abilities` to null to ensure `wp_get_ability()` triggers `abilities_api_init` action. + $old_wp_abilities = $wp_abilities; + $wp_abilities = null; + + $result = wp_get_ability( $name ); + + $wp_abilities = $old_wp_abilities; + + remove_action( 'abilities_api_init', $callback ); + + $this->assertEquals( + new WP_Ability( $name, $properties ), + $result + ); + } + + /** + * Tests retrieving all registered abilities. + */ + public function test_get_all_registered_abilities() { + do_action( 'abilities_api_init' ); + + $ability_one_name = 'test/ability-one'; + $ability_one_properties = self::$test_ability_properties; + wp_register_ability( $ability_one_name, $ability_one_properties ); + + $ability_two_name = 'test/ability-two'; + $ability_two_properties = self::$test_ability_properties; + wp_register_ability( $ability_two_name, $ability_two_properties ); + + $ability_three_name = 'test/ability-three'; + $ability_three_properties = self::$test_ability_properties; + wp_register_ability( $ability_three_name, $ability_three_properties ); + + $expected = array( + $ability_one_name => new WP_Ability( $ability_one_name, $ability_one_properties ), + $ability_two_name => new WP_Ability( $ability_two_name, $ability_two_properties ), + $ability_three_name => new WP_Ability( $ability_three_name, $ability_three_properties ), + ); + + $result = wp_get_abilities(); + $this->assertEquals( $expected, $result ); + } +} diff --git a/tests/unit/WPAbilitiesRegistryTest.php b/tests/unit/WPAbilitiesRegistryTest.php new file mode 100644 index 0000000..8c365bb --- /dev/null +++ b/tests/unit/WPAbilitiesRegistryTest.php @@ -0,0 +1,425 @@ +registry = new WP_Abilities_Registry(); + + self::$test_ability_properties = [ + 'label' => 'Add numbers', + 'description' => 'Calculates the result of adding two numbers.', + 'input_schema' => [ + 'type' => 'object', + 'properties' => [ + 'a' => [ + 'type' => 'number', + 'description' => 'First number.', + 'required' => true, + ], + 'b' => [ + 'type' => 'number', + 'description' => 'Second number.', + 'required' => true, + ], + ], + 'additionalProperties' => false, + ], + 'output_schema' => [ + 'type' => 'number', + 'description' => 'The result of adding the two numbers.', + 'required' => true, + ], + 'execute_callback' => function ( array $input ): int { + return $input['a'] + $input['b']; + }, + 'permission_callback' => function (): bool { + return true; + }, + 'meta' => [ + 'category' => 'math', + ], + ]; + } + + /** + * Tear down each test method. + */ + public function tear_down(): void { + $this->registry = null; + + parent::tear_down(); + } + + /** + * Should reject ability name without a namespace. + * + * @covers WP_Abilities_Registry::register + * + * @expectedIncorrectUsage WP_Abilities_Registry::register + */ + public function test_register_invalid_name_without_namespace() { + $result = $this->registry->register( 'without-namespace', self::$test_ability_properties ); + $this->assertNull( $result ); + } + + /** + * Should reject ability name with invalid characters. + * + * @covers WP_Abilities_Registry::register + * + * @expectedIncorrectUsage WP_Abilities_Registry::register + */ + public function test_register_invalid_characters_in_name() { + $result = $this->registry->register( 'still/_doing_it_wrong', array() ); + $this->assertNull( $result ); + } + + /** + * Should reject ability name with uppercase characters. + * + * @covers WP_Abilities_Registry::register + * + * @expectedIncorrectUsage WP_Abilities_Registry::register + */ + public function test_register_invalid_uppercase_characters_in_name() { + $result = $this->registry->register( 'Test/AddNumbers', self::$test_ability_properties ); + $this->assertNull( $result ); + } + + /** + * Should reject ability instance with invalid name. + * + * @covers WP_Abilities_Registry::register + * + * @expectedIncorrectUsage WP_Abilities_Registry::register + */ + public function test_register_invalid_name_using_instance() { + $ability = new WP_Ability( 'invalid_name', array() ); + $result = $this->registry->register( $ability ); + $this->assertNull( $result ); + } + + /** + * Should reject ability registration without a label. + * + * @covers WP_Abilities_Registry::register + * + * @expectedIncorrectUsage WP_Abilities_Registry::register + */ + public function test_register_invalid_missing_label() { + // Remove the label from the properties. + unset( self::$test_ability_properties['label'] ); + + $result = $this->registry->register( self::$test_ability_name, self::$test_ability_properties ); + $this->assertNull( $result ); + } + + /** + * Should reject ability registration with invalid label type. + * + * @covers WP_Abilities_Registry::register + * + * @expectedIncorrectUsage WP_Abilities_Registry::register + */ + public function test_register_invalid_label_type() { + self::$test_ability_properties['label'] = false; + + $result = $this->registry->register( self::$test_ability_name, self::$test_ability_properties ); + $this->assertNull( $result ); + } + + /** + * Should reject ability registration without a description. + * + * @covers WP_Abilities_Registry::register + * + * @expectedIncorrectUsage WP_Abilities_Registry::register + */ + public function test_register_invalid_missing_description() { + // Remove the description from the properties. + unset( self::$test_ability_properties['description'] ); + + $result = $this->registry->register( self::$test_ability_name, self::$test_ability_properties ); + $this->assertNull( $result ); + } + + /** + * Should reject ability registration with invalid description type. + * + * @covers WP_Abilities_Registry::register + * + * @expectedIncorrectUsage WP_Abilities_Registry::register + */ + public function test_register_invalid_description_type() { + self::$test_ability_properties['description'] = false; + + $result = $this->registry->register( self::$test_ability_name, self::$test_ability_properties ); + $this->assertNull( $result ); + } + + /** + * Should reject ability registration without an execute callback. + * + * @covers WP_Abilities_Registry::register + * + * @expectedIncorrectUsage WP_Abilities_Registry::register + */ + public function test_register_invalid_missing_execute_callback() { + // Remove the get_value_callback from the properties. + unset( self::$test_ability_properties['execute_callback'] ); + + $result = $this->registry->register( self::$test_ability_name, self::$test_ability_properties ); + $this->assertNull( $result ); + } + + /** + * Should reject ability registration if the execute callback is not a callable. + * + * @covers WP_Abilities_Registry::register + * + * @expectedIncorrectUsage WP_Abilities_Registry::register + */ + public function test_register_incorrect_execute_callback_type() { + self::$test_ability_properties['execute_callback'] = 'not-a-callback'; + + $result = $this->registry->register( self::$test_ability_name, self::$test_ability_properties ); + $this->assertNull( $result ); + } + + /** + * Should reject ability registration if the permission callback is not a callable. + * + * @covers WP_Abilities_Registry::register + * + * @expectedIncorrectUsage WP_Abilities_Registry::register + */ + public function test_register_incorrect_permission_callback_type() { + self::$test_ability_properties['permission_callback'] = 'not-a-callback'; + + $result = $this->registry->register( self::$test_ability_name, self::$test_ability_properties ); + $this->assertNull( $result ); + } + + /** + * Should reject ability registration if the input schema is not an array. + * + * @covers WP_Abilities_Registry::register + * + * @expectedIncorrectUsage WP_Abilities_Registry::register + */ + public function test_register_incorrect_input_schema_type() { + self::$test_ability_properties['input_schema'] = 'not-an-array'; + + $result = $this->registry->register( self::$test_ability_name, self::$test_ability_properties ); + $this->assertNull( $result ); + } + + /** + * Should reject ability registration if the output schema is not an array. + * + * @covers WP_Abilities_Registry::register + * + * @expectedIncorrectUsage WP_Abilities_Registry::register + */ + public function test_register_incorrect_output_schema_type() { + self::$test_ability_properties['output_schema'] = 'not-an-array'; + + $result = $this->registry->register( self::$test_ability_name, self::$test_ability_properties ); + $this->assertNull( $result ); + } + + /** + * Should reject ability registration with invalid meta type. + * + * @covers WP_Abilities_Registry::register + * + * @expectedIncorrectUsage WP_Abilities_Registry::register + */ + public function test_register_invalid_meta_type() { + self::$test_ability_properties['meta'] = false; + + $result = $this->registry->register( self::$test_ability_name, self::$test_ability_properties ); + $this->assertNull( $result ); + } + + /** + * Should reject registration for already registered ability. + * + * @covers WP_Abilities_Registry::register + * + * @expectedIncorrectUsage WP_Abilities_Registry::register + */ + public function test_register_incorrect_already_registered_ability() { + $this->registry->register( self::$test_ability_name, self::$test_ability_properties ); + + $result = $this->registry->register( self::$test_ability_name, self::$test_ability_properties ); + + $this->assertNull( $result ); + } + + /** + * Should reject registration for already registered ability when passing an ability instance. + * + * @covers WP_Abilities_Registry::register + * + * @expectedIncorrectUsage WP_Abilities_Registry::register + */ + public function test_register_incorrect_already_registered_ability_using_instance() { + $ability = $this->registry->register( self::$test_ability_name, self::$test_ability_properties ); + + $result = $this->registry->register( $ability ); + + $this->assertNull( $result ); + } + + /** + * Should successfully register a new ability. + * + * @covers WP_Abilities_Registry::register + */ + public function test_register_new_ability() { + $result = $this->registry->register( self::$test_ability_name, self::$test_ability_properties ); + + $this->assertEquals( + new WP_Ability( self::$test_ability_name, self::$test_ability_properties ), + $result + ); + } + + /** + * Should successfully register a new ability using an instance. + * + * @covers WP_Abilities_Registry::register + * @covers WP_Ability::construct + */ + public function test_register_new_ability_using_instance() { + $ability = new WP_Ability( self::$test_ability_name, self::$test_ability_properties ); + $result = $this->registry->register( $ability ); + + $this->assertSame( $ability, $result ); + } + + /** + * Should return false for ability that's not registered. + * + * @covers WP_Abilities_Registry::is_registered + */ + public function test_is_registered_for_unknown_ability() { + $result = $this->registry->is_registered( 'test/unknown' ); + $this->assertFalse( $result ); + } + + /** + * Should return true if ability is registered. + * + * @covers WP_Abilities_Registry::register + * @covers WP_Abilities_Registry::is_registered + */ + public function test_is_registered_for_known_ability() { + $this->registry->register( 'test/one', self::$test_ability_properties ); + $this->registry->register( 'test/two', self::$test_ability_properties ); + $this->registry->register( 'test/three', self::$test_ability_properties ); + + $result = $this->registry->is_registered( 'test/one' ); + $this->assertTrue( $result ); + } + + /** + * Should not find ability that's not registered. + * + * @covers WP_Abilities_Registry::get_registered + * + * @expectedIncorrectUsage WP_Abilities_Registry::get_registered + */ + public function test_get_registered_rejects_unknown_ability_name() { + $ability = $this->registry->get_registered( 'test/unknown' ); + $this->assertNull( $ability ); + } + + /** + * Should find registered ability by name. + * + * @covers WP_Abilities_Registry::register + * @covers WP_Abilities_Registry::get_registered + */ + public function test_get_registered_for_known_ability() { + $this->registry->register( 'test/one', self::$test_ability_properties ); + $this->registry->register( 'test/two', self::$test_ability_properties ); + $this->registry->register( 'test/three', self::$test_ability_properties ); + + $result = $this->registry->get_registered( 'test/two' ); + $this->assertEquals( 'test/two', $result->get_name() ); + } + + /** + * Unregistering should fail if a ability is not registered. + * + * @covers WP_Abilities_Registry::unregister + * + * @expectedIncorrectUsage WP_Abilities_Registry::unregister + */ + public function test_unregister_not_registered_ability() { + $result = $this->registry->unregister( 'test/unregistered' ); + $this->assertNull( $result ); + } + + /** + * Should unregister ability by name. + * + * @covers WP_Abilities_Registry::register + * @covers WP_Abilities_Registry::unregister + */ + public function test_unregister_for_known_ability() { + $this->registry->register( 'test/one', self::$test_ability_properties ); + $this->registry->register( 'test/two', self::$test_ability_properties ); + $this->registry->register( 'test/three', self::$test_ability_properties ); + + $result = $this->registry->unregister( 'test/three' ); + $this->assertEquals( 'test/three', $result->get_name() ); + + $this->assertFalse( $this->registry->is_registered( 'test/three' ) ); + } + + /** + * Should retrieve all registered abilities. + * + * @covers WP_Abilities_Registry::register + * @covers WP_Abilities_Registry::get_all_registered + */ + public function test_get_all_registered() { + $ability_one_name = 'test/one'; + $this->registry->register( $ability_one_name, self::$test_ability_properties ); + + $ability_two_name = 'test/two'; + $this->registry->register( $ability_two_name, self::$test_ability_properties ); + + $ability_three_name = 'test/three'; + $this->registry->register( $ability_three_name, self::$test_ability_properties ); + + $result = $this->registry->get_all_registered(); + $this->assertCount( 3, $result ); + $this->assertSame( $ability_one_name, $result[ $ability_one_name ]->get_name() ); + $this->assertSame( $ability_two_name, $result[ $ability_two_name ]->get_name() ); + $this->assertSame( $ability_three_name, $result[ $ability_three_name ]->get_name() ); + } +} From 326dea2350196f92edddea52fe8079f15734bf4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Greg=20Zi=C3=B3=C5=82kowski?= Date: Fri, 8 Aug 2025 13:38:16 +0200 Subject: [PATCH 2/6] Update tests/unit/WPAbilitiesRegistryTest.php Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- tests/unit/WPAbilitiesRegistryTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/WPAbilitiesRegistryTest.php b/tests/unit/WPAbilitiesRegistryTest.php index 8c365bb..1c2a196 100644 --- a/tests/unit/WPAbilitiesRegistryTest.php +++ b/tests/unit/WPAbilitiesRegistryTest.php @@ -185,7 +185,7 @@ public function test_register_invalid_description_type() { * @expectedIncorrectUsage WP_Abilities_Registry::register */ public function test_register_invalid_missing_execute_callback() { - // Remove the get_value_callback from the properties. + // Remove the execute_callback from the properties. unset( self::$test_ability_properties['execute_callback'] ); $result = $this->registry->register( self::$test_ability_name, self::$test_ability_properties ); From 152e2634d705f3f3ce5d0f956b46929203173018 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Greg=20Zi=C3=B3=C5=82kowski?= Date: Fri, 8 Aug 2025 13:44:07 +0200 Subject: [PATCH 3/6] Update src/class-wp-abilities-registry.php Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/class-wp-abilities-registry.php | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/class-wp-abilities-registry.php b/src/class-wp-abilities-registry.php index 50bcb1e..6633efb 100644 --- a/src/class-wp-abilities-registry.php +++ b/src/class-wp-abilities-registry.php @@ -232,7 +232,16 @@ public function is_registered( $name ): bool { * @return WP_Ability|null The registered ability instance, or null if it is not registered. */ public function get_registered( $name ): ?WP_Ability { - return $this->registered_abilities[ $name ] ?? null; + if ( ! $this->is_registered( $name ) ) { + _doing_it_wrong( + __METHOD__, + /* translators: %s: Ability name. */ + sprintf( esc_html__( 'Ability "%s" not found.' ), esc_attr( $name ) ), + '0.1.0' + ); + return null; + } + return $this->registered_abilities[ $name ]; } /** From 04333ab75c0a64172e0b00023654fc23f1ee61d8 Mon Sep 17 00:00:00 2001 From: Grzegorz Ziolkowski Date: Fri, 8 Aug 2025 13:48:23 +0200 Subject: [PATCH 4/6] Use type shorthand for null consistently --- src/class-wp-abilities-registry.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/class-wp-abilities-registry.php b/src/class-wp-abilities-registry.php index 6633efb..1cbb4e0 100644 --- a/src/class-wp-abilities-registry.php +++ b/src/class-wp-abilities-registry.php @@ -29,7 +29,7 @@ final class WP_Abilities_Registry { * Container for the main instance of the class. * * @since 0.1.0 - * @var WP_Abilities_Registry|null + * @var ?WP_Abilities_Registry */ private static ?WP_Abilities_Registry $instance = null; @@ -229,7 +229,7 @@ public function is_registered( $name ): bool { * @since 0.1.0 * * @param string $name The name of the registered ability, with its namespace. - * @return WP_Ability|null The registered ability instance, or null if it is not registered. + * @return ?WP_Ability The registered ability instance, or null if it is not registered. */ public function get_registered( $name ): ?WP_Ability { if ( ! $this->is_registered( $name ) ) { From 2d0fab5521c028aa2459d9c91190ae3632dbc30a Mon Sep 17 00:00:00 2001 From: Grzegorz Ziolkowski Date: Fri, 8 Aug 2025 16:01:31 +0200 Subject: [PATCH 5/6] Fix test hints for incorrect usage --- tests/unit/AbilitiesAPITest.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/unit/AbilitiesAPITest.php b/tests/unit/AbilitiesAPITest.php index 3a4e7f8..1386715 100644 --- a/tests/unit/AbilitiesAPITest.php +++ b/tests/unit/AbilitiesAPITest.php @@ -71,7 +71,7 @@ public function tear_down(): void { /** * Tests registering an ability with invalid name. * - * @expectedIncorrectUsage wp_register_ability + * @expectedIncorrectUsage WP_Abilities_Registry::register */ public function test_register_ability_invalid_name(): void { do_action( 'abilities_api_init' ); @@ -131,6 +131,7 @@ public function test_register_ability_no_permissions(): void { /** * Tests executing an ability with input not matching schema. * + * @expectedIncorrectUsage WP_Ability::validate_input * @expectedIncorrectUsage WP_Ability::execute */ public function test_execute_ability_no_input_schema_match(): void { @@ -144,7 +145,7 @@ public function test_execute_ability_no_input_schema_match(): void { /** * Tests executing an ability with output not matching schema. * - * @expectedIncorrectUsage WP_Ability::execute + * @expectedIncorrectUsage WP_Ability::validate_output */ public function test_execute_ability_no_output_schema_match(): void { do_action( 'abilities_api_init' ); @@ -160,7 +161,7 @@ public function test_execute_ability_no_output_schema_match(): void { /** * Tests permission callback receiving input not matching schema. * - * @expectedIncorrectUsage WP_Ability::has_permission + * @expectedIncorrectUsage WP_Ability::validate_input */ public function test_permission_callback_no_input_schema_match(): void { do_action( 'abilities_api_init' ); From 1975276c582ca136185538ef91147cc4010a3dac Mon Sep 17 00:00:00 2001 From: Grzegorz Ziolkowski Date: Mon, 11 Aug 2025 15:49:32 +0200 Subject: [PATCH 6/6] Backport changes for PHP 7.2 and coding standards compatibility with WordPress core --- src/class-wp-abilities-registry.php | 10 +- src/class-wp-ability.php | 18 ++-- tests/unit/AbilitiesAPITest.php | 141 +++++++++++++++++++------ tests/unit/WPAbilitiesRegistryTest.php | 42 ++++---- 4 files changed, 145 insertions(+), 66 deletions(-) diff --git a/src/class-wp-abilities-registry.php b/src/class-wp-abilities-registry.php index 1cbb4e0..4c52c74 100644 --- a/src/class-wp-abilities-registry.php +++ b/src/class-wp-abilities-registry.php @@ -23,7 +23,7 @@ final class WP_Abilities_Registry { * @since 0.1.0 * @var WP_Ability[] */ - private array $registered_abilities = []; + private $registered_abilities = array(); /** * Container for the main instance of the class. @@ -31,7 +31,7 @@ final class WP_Abilities_Registry { * @since 0.1.0 * @var ?WP_Abilities_Registry */ - private static ?WP_Abilities_Registry $instance = null; + private static $instance = null; /** * Registers a new ability. @@ -152,11 +152,11 @@ public function register( $name, array $properties = array() ): ?WP_Ability { array( 'label' => $properties['label'], 'description' => $properties['description'], - 'input_schema' => $properties['input_schema'] ?? [], - 'output_schema' => $properties['output_schema'] ?? [], + 'input_schema' => $properties['input_schema'] ?? array(), + 'output_schema' => $properties['output_schema'] ?? array(), 'execute_callback' => $properties['execute_callback'], 'permission_callback' => $properties['permission_callback'] ?? null, - 'meta' => $properties['meta'] ?? [], + 'meta' => $properties['meta'] ?? array(), ) ); $this->registered_abilities[ $name ] = $ability; diff --git a/src/class-wp-ability.php b/src/class-wp-ability.php index 63ce4f0..50dea61 100644 --- a/src/class-wp-ability.php +++ b/src/class-wp-ability.php @@ -27,7 +27,7 @@ class WP_Ability { * @since 0.1.0 * @var string */ - protected string $name; + protected $name; /** * The human-readable ability label. @@ -35,7 +35,7 @@ class WP_Ability { * @since 0.1.0 * @var string */ - protected string $label; + protected $label; /** * The detailed ability description. @@ -43,7 +43,7 @@ class WP_Ability { * @since 0.1.0 * @var string */ - protected string $description; + protected $description; /** * The optional ability input schema. @@ -51,7 +51,7 @@ class WP_Ability { * @since 0.1.0 * @var array */ - protected array $input_schema = []; + protected $input_schema = array(); /** * The optional ability output schema. @@ -59,7 +59,7 @@ class WP_Ability { * @since 0.1.0 * @var array */ - protected array $output_schema = []; + protected $output_schema = array(); /** * The ability execute callback. @@ -83,7 +83,7 @@ class WP_Ability { * @since 0.1.0 * @var array */ - protected array $meta = []; + protected $meta = array(); /** * Constructor. @@ -197,7 +197,7 @@ protected function validate_input( array $input = array() ): bool { __( 'Invalid input provided for ability "%1$s": %2$s.' ), $this->name, $valid_input->get_error_message() - ), + ) ), '0.1.0' ); @@ -243,7 +243,7 @@ protected function do_execute( array $input ) { __METHOD__, esc_html( /* translators: %s ability name. */ - sprintf( __( 'Ability "%s" does not have a valid execute callback.' ), $this->name ), + sprintf( __( 'Ability "%s" does not have a valid execute callback.' ), $this->name ) ), '0.1.0' ); @@ -276,7 +276,7 @@ protected function validate_output( $output ): bool { __( 'Invalid output provided for ability "%1$s": %2$s.' ), $this->name, $valid_output->get_error_message() - ), + ) ), '0.1.0' ); diff --git a/tests/unit/AbilitiesAPITest.php b/tests/unit/AbilitiesAPITest.php index 1386715..c70d1dd 100644 --- a/tests/unit/AbilitiesAPITest.php +++ b/tests/unit/AbilitiesAPITest.php @@ -11,7 +11,7 @@ class AbilitiesAPITest extends WP_UnitTestCase { public static $test_ability_name = 'test/add-numbers'; - public static $test_ability_properties = []; + public static $test_ability_properties = array(); /** * Set up before each test. @@ -19,40 +19,40 @@ class AbilitiesAPITest extends WP_UnitTestCase { public function set_up(): void { parent::set_up(); - self::$test_ability_properties = [ + self::$test_ability_properties = array( 'label' => 'Add numbers', 'description' => 'Calculates the result of adding two numbers.', - 'input_schema' => [ + 'input_schema' => array( 'type' => 'object', - 'properties' => [ - 'a' => [ + 'properties' => array( + 'a' => array( 'type' => 'number', 'description' => 'First number.', 'required' => true, - ], - 'b' => [ + ), + 'b' => array( 'type' => 'number', 'description' => 'Second number.', 'required' => true, - ], - ], + ), + ), 'additionalProperties' => false, - ], - 'output_schema' => [ + ), + 'output_schema' => array( 'type' => 'number', 'description' => 'The result of adding the two numbers.', 'required' => true, - ], + ), 'execute_callback' => function ( array $input ): int { return $input['a'] + $input['b']; }, 'permission_callback' => function (): bool { return true; }, - 'meta' => [ + 'meta' => array( 'category' => 'math', - ], - ]; + ), + ); } /** @@ -76,7 +76,7 @@ public function tear_down(): void { public function test_register_ability_invalid_name(): void { do_action( 'abilities_api_init' ); - $result = wp_register_ability( 'invalid_name', [] ); + $result = wp_register_ability( 'invalid_name', array() ); $this->assertNull( $result ); } @@ -107,8 +107,23 @@ public function test_register_valid_ability(): void { $this->assertSame( self::$test_ability_properties['input_schema'], $result->get_input_schema() ); $this->assertSame( self::$test_ability_properties['output_schema'], $result->get_output_schema() ); $this->assertSame( self::$test_ability_properties['meta'], $result->get_meta() ); - $this->assertTrue( $result->has_permission( [ 'a' => 2, 'b' => 3 ] ) ); - $this->assertSame( 5, $result->execute( [ 'a' => 2, 'b' => 3 ] ) ); + $this->assertTrue( + $result->has_permission( + array( + 'a' => 2, + 'b' => 3, + ) + ) + ); + $this->assertSame( + 5, + $result->execute( + array( + 'a' => 2, + 'b' => 3, + ) + ) + ); } /** @@ -124,8 +139,22 @@ public function test_register_ability_no_permissions(): void { }; $result = wp_register_ability( self::$test_ability_name, self::$test_ability_properties ); - $this->assertFalse( $result->has_permission( [ 'a' => 2, 'b' => 3 ] ) ); - $this->assertNull( $result->execute( [ 'a' => 2, 'b' => 3 ] ) ); + $this->assertFalse( + $result->has_permission( + array( + 'a' => 2, + 'b' => 3, + ) + ) + ); + $this->assertNull( + $result->execute( + array( + 'a' => 2, + 'b' => 3, + ) + ) + ); } /** @@ -139,7 +168,15 @@ public function test_execute_ability_no_input_schema_match(): void { $result = wp_register_ability( self::$test_ability_name, self::$test_ability_properties ); - $this->assertNull( $result->execute( [ 'a' => 2, 'b' => 3, 'unknown' => 1 ] ) ); + $this->assertNull( + $result->execute( + array( + 'a' => 2, + 'b' => 3, + 'unknown' => 1, + ) + ) + ); } /** @@ -155,7 +192,14 @@ public function test_execute_ability_no_output_schema_match(): void { }; $result = wp_register_ability( self::$test_ability_name, self::$test_ability_properties ); - $this->assertNull( $result->execute( [ 'a' => 2, 'b' => 3 ] ) ); + $this->assertNull( + $result->execute( + array( + 'a' => 2, + 'b' => 3, + ) + ) + ); } /** @@ -168,7 +212,15 @@ public function test_permission_callback_no_input_schema_match(): void { $result = wp_register_ability( self::$test_ability_name, self::$test_ability_properties ); - $this->assertFalse( $result->has_permission( [ 'a' => 2, 'b' => 3, 'unknown' => 1 ] ) ); + $this->assertFalse( + $result->has_permission( + array( + 'a' => 2, + 'b' => 3, + 'unknown' => 1, + ) + ) + ); } /** @@ -177,21 +229,48 @@ public function test_permission_callback_no_input_schema_match(): void { public function test_permission_callback_receives_input(): void { do_action( 'abilities_api_init' ); - $received_input = null; + $received_input = null; self::$test_ability_properties['permission_callback'] = function ( array $input ) use ( &$received_input ): bool { $received_input = $input; // Allow only if 'a' is greater than 'b' return $input['a'] > $input['b']; }; + $result = wp_register_ability( self::$test_ability_name, self::$test_ability_properties ); // Test with a > b (should be allowed) - $this->assertTrue( $result->has_permission( [ 'a' => 5, 'b' => 3 ] ) ); - $this->assertSame( [ 'a' => 5, 'b' => 3 ], $received_input ); + $this->assertTrue( + $result->has_permission( + array( + 'a' => 5, + 'b' => 3, + ) + ) + ); + $this->assertSame( + array( + 'a' => 5, + 'b' => 3, + ), + $received_input + ); // Test with a < b (should be denied) - $this->assertFalse( $result->has_permission( [ 'a' => 2, 'b' => 8 ] ) ); - $this->assertSame( [ 'a' => 2, 'b' => 8 ], $received_input ); + $this->assertFalse( + $result->has_permission( + array( + 'a' => 2, + 'b' => 8, + ) + ) + ); + $this->assertSame( + array( + 'a' => 2, + 'b' => 8, + ), + $received_input + ); } /** @@ -259,9 +338,9 @@ public function test_get_all_registered_abilities() { wp_register_ability( $ability_three_name, $ability_three_properties ); $expected = array( - $ability_one_name => new WP_Ability( $ability_one_name, $ability_one_properties ), - $ability_two_name => new WP_Ability( $ability_two_name, $ability_two_properties ), - $ability_three_name => new WP_Ability( $ability_three_name, $ability_three_properties ), + $ability_one_name => new WP_Ability( $ability_one_name, $ability_one_properties ), + $ability_two_name => new WP_Ability( $ability_two_name, $ability_two_properties ), + $ability_three_name => new WP_Ability( $ability_three_name, $ability_three_properties ), ); $result = wp_get_abilities(); diff --git a/tests/unit/WPAbilitiesRegistryTest.php b/tests/unit/WPAbilitiesRegistryTest.php index 1c2a196..3b741e0 100644 --- a/tests/unit/WPAbilitiesRegistryTest.php +++ b/tests/unit/WPAbilitiesRegistryTest.php @@ -5,10 +5,10 @@ * * @group abilities-api */ -class WPAbilitiesRegistryTest extends WP_UnitTestCase { +class Tests_Abilities_API_wpAbilitiesRegistry extends WP_UnitTestCase { public static $test_ability_name = 'test/add-numbers'; - public static $test_ability_properties = []; + public static $test_ability_properties = array(); /** * Mock abilities registry. @@ -25,40 +25,40 @@ public function set_up(): void { $this->registry = new WP_Abilities_Registry(); - self::$test_ability_properties = [ + self::$test_ability_properties = array( 'label' => 'Add numbers', 'description' => 'Calculates the result of adding two numbers.', - 'input_schema' => [ + 'input_schema' => array( 'type' => 'object', - 'properties' => [ - 'a' => [ + 'properties' => array( + 'a' => array( 'type' => 'number', 'description' => 'First number.', 'required' => true, - ], - 'b' => [ + ), + 'b' => array( 'type' => 'number', 'description' => 'Second number.', 'required' => true, - ], - ], + ), + ), 'additionalProperties' => false, - ], - 'output_schema' => [ + ), + 'output_schema' => array( 'type' => 'number', 'description' => 'The result of adding the two numbers.', 'required' => true, - ], + ), 'execute_callback' => function ( array $input ): int { return $input['a'] + $input['b']; }, 'permission_callback' => function (): bool { return true; }, - 'meta' => [ + 'meta' => array( 'category' => 'math', - ], - ]; + ), + ); } /** @@ -115,7 +115,7 @@ public function test_register_invalid_uppercase_characters_in_name() { */ public function test_register_invalid_name_using_instance() { $ability = new WP_Ability( 'invalid_name', array() ); - $result = $this->registry->register( $ability ); + $result = $this->registry->register( $ability ); $this->assertNull( $result ); } @@ -314,7 +314,7 @@ public function test_register_new_ability() { */ public function test_register_new_ability_using_instance() { $ability = new WP_Ability( self::$test_ability_name, self::$test_ability_properties ); - $result = $this->registry->register( $ability ); + $result = $this->registry->register( $ability ); $this->assertSame( $ability, $result ); } @@ -407,13 +407,13 @@ public function test_unregister_for_known_ability() { * @covers WP_Abilities_Registry::get_all_registered */ public function test_get_all_registered() { - $ability_one_name = 'test/one'; + $ability_one_name = 'test/one'; $this->registry->register( $ability_one_name, self::$test_ability_properties ); - $ability_two_name = 'test/two'; + $ability_two_name = 'test/two'; $this->registry->register( $ability_two_name, self::$test_ability_properties ); - $ability_three_name = 'test/three'; + $ability_three_name = 'test/three'; $this->registry->register( $ability_three_name, self::$test_ability_properties ); $result = $this->registry->get_all_registered();