From a3c2f01fead9db74093f53f36bccefb21b852af5 Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Wed, 24 Jun 2026 15:19:56 -0700 Subject: [PATCH 1/3] Comments: Add a REST API endpoint for comment types. Expose registered comment types through a read-only `/wp/v2/comment-types` controller, mirroring the post types controller (`/wp/v2/types`). This lets REST clients discover the registered types and their labels, which the block editor's inline-commenting work needs in order to add and query comments by type. Add a `show_in_rest` argument to `WP_Comment_Type` (defaulting to the value of `public`, as `show_ui` does) to gate which types the endpoint exposes. The built-in `comment`, `pingback`, and `trackback` types are public and therefore visible; the internal `note` type is not. Builds on the registration API in #12311. See #35214. --- src/wp-includes/class-wp-comment-type.php | 29 +- src/wp-includes/rest-api.php | 4 + ...class-wp-rest-comment-types-controller.php | 315 ++++++++++++++++++ src/wp-settings.php | 1 + 4 files changed, 343 insertions(+), 6 deletions(-) create mode 100644 src/wp-includes/rest-api/endpoints/class-wp-rest-comment-types-controller.php diff --git a/src/wp-includes/class-wp-comment-type.php b/src/wp-includes/class-wp-comment-type.php index 6f88c5e7b499d..5e8aa923f6012 100644 --- a/src/wp-includes/class-wp-comment-type.php +++ b/src/wp-includes/class-wp-comment-type.php @@ -94,6 +94,17 @@ final class WP_Comment_Type { */ public $show_ui; + /** + * Whether to include this comment type in the REST API. + * + * Comment types with this enabled are exposed by the `/wp/v2/comment-types` + * endpoint. Default is the value of $public. + * + * @since 7.1.0 + * @var bool + */ + public $show_in_rest; + /** * Whether this comment type is a native or "built-in" comment type. * @@ -187,12 +198,13 @@ public function set_props( $args ) { * treated as a provided value and overwrite the default name with false. */ $defaults = array( - 'labels' => array(), - 'description' => '', - 'public' => true, - 'internal' => false, - 'show_ui' => null, - '_builtin' => false, + 'labels' => array(), + 'description' => '', + 'public' => true, + 'internal' => false, + 'show_ui' => null, + 'show_in_rest' => null, + '_builtin' => false, ); $args = array_merge( $defaults, $args ); @@ -202,6 +214,11 @@ public function set_props( $args ) { $args['show_ui'] = $args['public']; } + // If not set, default to the setting for 'public'. + if ( null === $args['show_in_rest'] ) { + $args['show_in_rest'] = $args['public']; + } + $args['name'] = $this->name; foreach ( $args as $property_name => $property_value ) { diff --git a/src/wp-includes/rest-api.php b/src/wp-includes/rest-api.php index a4c22e8f1cca1..c49202811a8f1 100644 --- a/src/wp-includes/rest-api.php +++ b/src/wp-includes/rest-api.php @@ -324,6 +324,10 @@ function create_initial_rest_routes() { $controller = new WP_REST_Comments_Controller(); $controller->register_routes(); + // Comment types. + $controller = new WP_REST_Comment_Types_Controller(); + $controller->register_routes(); + $search_handlers = array( new WP_REST_Post_Search_Handler(), new WP_REST_Term_Search_Handler(), diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-comment-types-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-comment-types-controller.php new file mode 100644 index 0000000000000..f5913d1d9242a --- /dev/null +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-comment-types-controller.php @@ -0,0 +1,315 @@ +namespace = 'wp/v2'; + $this->rest_base = 'comment-types'; + } + + /** + * Registers the routes for comment types. + * + * @since 7.1.0 + * + * @see register_rest_route() + */ + public function register_routes() { + + register_rest_route( + $this->namespace, + '/' . $this->rest_base, + array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + 'args' => $this->get_collection_params(), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + + register_rest_route( + $this->namespace, + '/' . $this->rest_base . '/(?P[\w-]+)', + array( + 'args' => array( + 'type' => array( + 'description' => __( 'An alphanumeric identifier for the comment type.' ), + 'type' => 'string', + ), + ), + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => array( $this, 'get_item_permissions_check' ), + 'args' => array( + 'context' => $this->get_context_param( array( 'default' => 'view' ) ), + ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + } + + /** + * Checks whether a given request has permission to read comment types. + * + * @since 7.1.0 + * + * @param WP_REST_Request $request Full details about the request. + * @return true|WP_Error True if the request has read access, WP_Error object otherwise. + */ + public function get_items_permissions_check( $request ) { + if ( 'edit' === $request['context'] && ! current_user_can( 'moderate_comments' ) ) { + return new WP_Error( + 'rest_cannot_view', + __( 'Sorry, you are not allowed to manage comments.' ), + array( 'status' => rest_authorization_required_code() ) + ); + } + + return true; + } + + /** + * Retrieves all public comment types. + * + * @since 7.1.0 + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. + */ + public function get_items( $request ) { + if ( $request->is_method( 'HEAD' ) ) { + // Return early as this handler doesn't add any response headers. + return new WP_REST_Response( array() ); + } + + $data = array(); + $types = get_comment_types( array( 'show_in_rest' => true ), 'objects' ); + + foreach ( $types as $type ) { + $comment_type = $this->prepare_item_for_response( $type, $request ); + $data[ $type->name ] = $this->prepare_response_for_collection( $comment_type ); + } + + return rest_ensure_response( $data ); + } + + /** + * Checks if a given request has access to read a comment type. + * + * @since 7.1.0 + * + * @param WP_REST_Request $request Full details about the request. + * @return true|WP_Error True if the request has read access for the item, WP_Error object otherwise. + */ + public function get_item_permissions_check( $request ) { + if ( 'edit' === $request['context'] && ! current_user_can( 'moderate_comments' ) ) { + return new WP_Error( + 'rest_forbidden_context', + __( 'Sorry, you are not allowed to manage comments.' ), + array( 'status' => rest_authorization_required_code() ) + ); + } + + return true; + } + + /** + * Retrieves a specific comment type. + * + * @since 7.1.0 + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. + */ + public function get_item( $request ) { + $obj = get_comment_type_object( $request['type'] ); + + if ( empty( $obj ) ) { + return new WP_Error( + 'rest_type_invalid', + __( 'Invalid comment type.' ), + array( 'status' => 404 ) + ); + } + + if ( empty( $obj->show_in_rest ) ) { + return new WP_Error( + 'rest_cannot_read_type', + __( 'Cannot view comment type.' ), + array( 'status' => rest_authorization_required_code() ) + ); + } + + $data = $this->prepare_item_for_response( $obj, $request ); + + return rest_ensure_response( $data ); + } + + /** + * Prepares a comment type object for serialization. + * + * @since 7.1.0 + * + * @param WP_Comment_Type $item Comment type object. + * @param WP_REST_Request $request Full details about the request. + * @return WP_REST_Response Response object. + */ + public function prepare_item_for_response( $item, $request ) { + // Restores the more descriptive, specific name for use within this method. + $comment_type = $item; + + // Don't prepare the response body for HEAD requests. + if ( $request->is_method( 'HEAD' ) ) { + /** This filter is documented in wp-includes/rest-api/endpoints/class-wp-rest-comment-types-controller.php */ + return apply_filters( 'rest_prepare_comment_type', new WP_REST_Response( array() ), $comment_type, $request ); + } + + $fields = $this->get_fields_for_response( $request ); + $data = array(); + + if ( rest_is_field_included( 'description', $fields ) ) { + $data['description'] = $comment_type->description; + } + + if ( rest_is_field_included( 'labels', $fields ) ) { + $data['labels'] = $comment_type->labels; + } + + if ( rest_is_field_included( 'name', $fields ) ) { + $data['name'] = $comment_type->label; + } + + if ( rest_is_field_included( 'slug', $fields ) ) { + $data['slug'] = $comment_type->name; + } + + $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; + $data = $this->add_additional_fields_to_object( $data, $request ); + $data = $this->filter_response_by_context( $data, $context ); + + // Wrap the data in a response object. + $response = rest_ensure_response( $data ); + + if ( rest_is_field_included( '_links', $fields ) || rest_is_field_included( '_embedded', $fields ) ) { + $response->add_links( $this->prepare_links( $comment_type ) ); + } + + /** + * Filters a comment type returned from the REST API. + * + * Allows modification of the comment type data right before it is returned. + * + * @since 7.1.0 + * + * @param WP_REST_Response $response The response object. + * @param WP_Comment_Type $comment_type The original comment type object. + * @param WP_REST_Request $request Request used to generate the response. + */ + return apply_filters( 'rest_prepare_comment_type', $response, $comment_type, $request ); + } + + /** + * Prepares links for the request. + * + * @since 7.1.0 + * + * @param WP_Comment_Type $comment_type The comment type. + * @return array Links for the given comment type. + */ + protected function prepare_links( $comment_type ) { + return array( + 'collection' => array( + 'href' => rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ), + ), + 'https://api.w.org/items' => array( + 'href' => add_query_arg( 'type', $comment_type->name, rest_url( 'wp/v2/comments' ) ), + ), + ); + } + + /** + * Retrieves the comment type's schema, conforming to JSON Schema. + * + * @since 7.1.0 + * + * @return array Item schema data. + */ + public function get_item_schema() { + if ( $this->schema ) { + return $this->add_additional_fields_schema( $this->schema ); + } + + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'comment-type', + 'type' => 'object', + 'properties' => array( + 'description' => array( + 'description' => __( 'A human-readable description of the comment type.' ), + 'type' => 'string', + 'context' => array( 'view', 'edit', 'embed' ), + 'readonly' => true, + ), + 'labels' => array( + 'description' => __( 'Human-readable labels for the comment type for various contexts.' ), + 'type' => 'object', + 'context' => array( 'edit' ), + 'readonly' => true, + ), + 'name' => array( + 'description' => __( 'The title for the comment type.' ), + 'type' => 'string', + 'context' => array( 'view', 'edit', 'embed' ), + 'readonly' => true, + ), + 'slug' => array( + 'description' => __( 'An alphanumeric identifier for the comment type.' ), + 'type' => 'string', + 'context' => array( 'view', 'edit', 'embed' ), + 'readonly' => true, + ), + ), + ); + + $this->schema = $schema; + + return $this->add_additional_fields_schema( $this->schema ); + } + + /** + * Retrieves the query params for collections. + * + * @since 7.1.0 + * + * @return array Collection parameters. + */ + public function get_collection_params() { + return array( + 'context' => $this->get_context_param( array( 'default' => 'view' ) ), + ); + } +} diff --git a/src/wp-settings.php b/src/wp-settings.php index 9ec2c3607271d..56869e68e6c40 100644 --- a/src/wp-settings.php +++ b/src/wp-settings.php @@ -334,6 +334,7 @@ require ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-menu-locations-controller.php'; require ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-users-controller.php'; require ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-comments-controller.php'; +require ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-comment-types-controller.php'; require ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-search-controller.php'; require ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-blocks-controller.php'; require ABSPATH . WPINC . '/rest-api/endpoints/class-wp-rest-block-types-controller.php'; From c0d63b6611d3d4e2658c7a3ed50a03595f9ca494 Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Wed, 24 Jun 2026 15:20:01 -0700 Subject: [PATCH 2/3] Comments: Add tests for the comment types REST API endpoint. Cover the `/wp/v2/comment-types` controller: route registration, the context param, listing public types while excluding non-REST types (`note` and types that opt out via `show_in_rest`), single-type reads, permission checks for the `edit` context, and the item schema. Also assert the `show_in_rest` cascade on `WP_Comment_Type` and add the two new routes to the REST schema route list. See #35214. --- tests/phpunit/tests/comment/types.php | 26 ++ .../rest-comment-types-controller.php | 248 ++++++++++++++++++ .../tests/rest-api/rest-schema-setup.php | 2 + 3 files changed, 276 insertions(+) create mode 100644 tests/phpunit/tests/rest-api/rest-comment-types-controller.php diff --git a/tests/phpunit/tests/comment/types.php b/tests/phpunit/tests/comment/types.php index 54f8e2aa9ad33..bd5d845e445db 100644 --- a/tests/phpunit/tests/comment/types.php +++ b/tests/phpunit/tests/comment/types.php @@ -98,6 +98,32 @@ public function test_register_comment_type_show_ui_should_default_to_value_of_pu $this->assertFalse( get_comment_type_object( 'private_type' )->show_ui ); } + /** + * @ticket 35214 + */ + public function test_register_comment_type_show_in_rest_should_default_to_value_of_public() { + register_comment_type( 'public_type', array( 'public' => true ) ); + $this->assertTrue( get_comment_type_object( 'public_type' )->show_in_rest ); + + register_comment_type( 'private_type', array( 'public' => false ) ); + $this->assertFalse( get_comment_type_object( 'private_type' )->show_in_rest ); + } + + /** + * @ticket 35214 + */ + public function test_register_comment_type_show_in_rest_can_be_overridden() { + register_comment_type( + 'private_but_in_rest', + array( + 'public' => false, + 'show_in_rest' => true, + ) + ); + + $this->assertTrue( get_comment_type_object( 'private_but_in_rest' )->show_in_rest ); + } + /** * @ticket 35214 */ diff --git a/tests/phpunit/tests/rest-api/rest-comment-types-controller.php b/tests/phpunit/tests/rest-api/rest-comment-types-controller.php new file mode 100644 index 0000000000000..db413f4dbdb11 --- /dev/null +++ b/tests/phpunit/tests/rest-api/rest-comment-types-controller.php @@ -0,0 +1,248 @@ +user->create( array( 'role' => 'administrator' ) ); + self::$subscriber_id = $factory->user->create( array( 'role' => 'subscriber' ) ); + } + + public static function wpTearDownAfterClass() { + self::delete_user( self::$admin_id ); + self::delete_user( self::$subscriber_id ); + } + + /** + * Ensures any comment type registered during a test is cleaned up. + */ + public function tear_down() { + global $wp_comment_types; + + foreach ( array_keys( $wp_comment_types ) as $comment_type ) { + if ( ! $wp_comment_types[ $comment_type ]->_builtin ) { + unset( $wp_comment_types[ $comment_type ] ); + } + } + + parent::tear_down(); + } + + public function test_register_routes() { + $routes = rest_get_server()->get_routes(); + $this->assertArrayHasKey( '/wp/v2/comment-types', $routes ); + $this->assertArrayHasKey( '/wp/v2/comment-types/(?P[\w-]+)', $routes ); + } + + public function test_context_param() { + // Collection. + $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/comment-types' ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $this->assertSame( 'view', $data['endpoints'][0]['args']['context']['default'] ); + $this->assertSameSets( array( 'view', 'edit', 'embed' ), $data['endpoints'][0]['args']['context']['enum'] ); + // Single. + $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/comment-types/comment' ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $this->assertSame( 'view', $data['endpoints'][0]['args']['context']['default'] ); + $this->assertSameSets( array( 'view', 'edit', 'embed' ), $data['endpoints'][0]['args']['context']['enum'] ); + } + + public function test_get_items() { + $request = new WP_REST_Request( 'GET', '/wp/v2/comment-types' ); + $response = rest_get_server()->dispatch( $request ); + + $data = $response->get_data(); + $comment_types = get_comment_types( array( 'show_in_rest' => true ), 'objects' ); + $this->assertCount( count( $comment_types ), $data ); + $this->assertSame( $comment_types['comment']->name, $data['comment']['slug'] ); + $this->check_comment_type_obj( 'view', $comment_types['comment'], $data['comment'], $data['comment']['_links'] ); + $this->assertArrayHasKey( 'pingback', $data ); + $this->assertArrayHasKey( 'trackback', $data ); + } + + /** + * The internal `note` type is not exposed (show_in_rest defaults to public, which is false). + */ + public function test_get_items_excludes_non_rest_types() { + $request = new WP_REST_Request( 'GET', '/wp/v2/comment-types' ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + $this->assertArrayNotHasKey( 'note', $data ); + } + + public function test_get_items_includes_registered_custom_type() { + register_comment_type( 'review', array( 'public' => true ) ); + + $request = new WP_REST_Request( 'GET', '/wp/v2/comment-types' ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + $this->assertArrayHasKey( 'review', $data ); + } + + public function test_get_items_excludes_custom_type_opted_out_of_rest() { + register_comment_type( + 'review', + array( + 'public' => true, + 'show_in_rest' => false, + ) + ); + + $request = new WP_REST_Request( 'GET', '/wp/v2/comment-types' ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + $this->assertArrayNotHasKey( 'review', $data ); + } + + /** + * @dataProvider data_readable_http_methods + * + * @param string $method HTTP method to use. + */ + public function test_get_items_invalid_permission_for_context( $method ) { + wp_set_current_user( 0 ); + $request = new WP_REST_Request( $method, '/wp/v2/comment-types' ); + $request->set_param( 'context', 'edit' ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_cannot_view', $response, 401 ); + } + + /** + * Data provider intended to provide HTTP method names for testing GET and HEAD requests. + * + * @return array + */ + public static function data_readable_http_methods() { + return array( + 'GET request' => array( 'GET' ), + 'HEAD request' => array( 'HEAD' ), + ); + } + + public function test_get_item() { + $request = new WP_REST_Request( 'GET', '/wp/v2/comment-types/comment' ); + $response = rest_get_server()->dispatch( $request ); + $this->check_comment_type_object_response( 'view', $response ); + } + + public function test_get_item_invalid_type() { + $request = new WP_REST_Request( 'GET', '/wp/v2/comment-types/invalid' ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_type_invalid', $response, 404 ); + } + + public function test_get_item_non_rest_type_is_not_readable() { + $request = new WP_REST_Request( 'GET', '/wp/v2/comment-types/note' ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_cannot_read_type', $response, 401 ); + } + + public function test_get_item_edit_context_requires_permission() { + wp_set_current_user( self::$subscriber_id ); + $request = new WP_REST_Request( 'GET', '/wp/v2/comment-types/comment' ); + $request->set_param( 'context', 'edit' ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_forbidden_context', $response, 403 ); + } + + public function test_get_item_edit_context_returns_labels() { + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'GET', '/wp/v2/comment-types/comment' ); + $request->set_param( 'context', 'edit' ); + $response = rest_get_server()->dispatch( $request ); + $this->check_comment_type_object_response( 'edit', $response ); + } + + public function test_create_item() { + /** Comment types can't be created */ + $request = new WP_REST_Request( 'POST', '/wp/v2/comment-types' ); + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( 404, $response->get_status() ); + } + + public function test_update_item() { + /** Comment types can't be updated */ + $request = new WP_REST_Request( 'POST', '/wp/v2/comment-types/comment' ); + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( 404, $response->get_status() ); + } + + public function test_delete_item() { + /** Comment types can't be deleted */ + $request = new WP_REST_Request( 'DELETE', '/wp/v2/comment-types/comment' ); + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( 404, $response->get_status() ); + } + + public function test_prepare_item() { + $obj = get_comment_type_object( 'comment' ); + $endpoint = new WP_REST_Comment_Types_Controller(); + $request = new WP_REST_Request(); + $request->set_param( 'context', 'view' ); + $response = $endpoint->prepare_item_for_response( $obj, $request ); + $this->check_comment_type_obj( 'view', $obj, $response->get_data(), $response->get_links() ); + } + + public function test_get_item_schema() { + $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/comment-types' ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $properties = $data['schema']['properties']; + $this->assertCount( 4, $properties ); + $this->assertArrayHasKey( 'name', $properties ); + $this->assertArrayHasKey( 'slug', $properties ); + $this->assertArrayHasKey( 'description', $properties ); + $this->assertArrayHasKey( 'labels', $properties ); + } + + protected function check_comment_type_obj( $context, $comment_type_obj, $data, $links ) { + $this->assertSame( $comment_type_obj->label, $data['name'] ); + $this->assertSame( $comment_type_obj->name, $data['slug'] ); + $this->assertSame( $comment_type_obj->description, $data['description'] ); + + $links = test_rest_expand_compact_links( $links ); + $this->assertSame( rest_url( 'wp/v2/comment-types' ), $links['collection'][0]['href'] ); + $this->assertArrayHasKey( 'https://api.w.org/items', $links ); + + if ( 'edit' === $context ) { + $this->assertSame( (array) $comment_type_obj->labels, (array) $data['labels'] ); + } else { + $this->assertArrayNotHasKey( 'labels', $data ); + } + } + + protected function check_comment_type_object_response( $context, $response, $comment_type = 'comment' ) { + $this->assertSame( 200, $response->get_status() ); + $data = $response->get_data(); + $obj = get_comment_type_object( $comment_type ); + $this->check_comment_type_obj( $context, $obj, $data, $response->get_links() ); + } +} diff --git a/tests/phpunit/tests/rest-api/rest-schema-setup.php b/tests/phpunit/tests/rest-api/rest-schema-setup.php index 89bf2c481c567..c1aa2977e6829 100644 --- a/tests/phpunit/tests/rest-api/rest-schema-setup.php +++ b/tests/phpunit/tests/rest-api/rest-schema-setup.php @@ -138,6 +138,8 @@ public function test_expected_routes_in_schema() { '/wp/v2/users/(?P(?:[\\d]+|me))/application-passwords/(?P[\\w\\-]+)', '/wp/v2/comments', '/wp/v2/comments/(?P[\\d]+)', + '/wp/v2/comment-types', + '/wp/v2/comment-types/(?P[\\w-]+)', '/wp/v2/global-styles/(?P[\/\d+]+)', '/wp/v2/global-styles/(?P[\d]+)/revisions', '/wp/v2/global-styles/(?P[\d]+)/revisions/(?P[\d]+)', From 7daed7e2c489a9ef868785313eda2fb560ee8868 Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Wed, 24 Jun 2026 20:25:51 -0700 Subject: [PATCH 3/3] Comments: Expand REST comment-types test coverage for HEAD requests. Add happy-path coverage for the controller's dedicated HEAD-request handling in get_items() and prepare_item_for_response(), mirroring the post-types controller (ticket 56481): a collection HEAD request returns 200 without preparing item data, single-item HEAD requests still run the rest_prepare_comment_type filter and allow header injection, and HEAD requests with _fields succeed. Also assert the api.w.org/items link points at the type-filtered comments collection. See #35214. --- .../rest-comment-types-controller.php | 101 ++++++++++++++++++ 1 file changed, 101 insertions(+) diff --git a/tests/phpunit/tests/rest-api/rest-comment-types-controller.php b/tests/phpunit/tests/rest-api/rest-comment-types-controller.php index db413f4dbdb11..2dfa184cbd342 100644 --- a/tests/phpunit/tests/rest-api/rest-comment-types-controller.php +++ b/tests/phpunit/tests/rest-api/rest-comment-types-controller.php @@ -223,6 +223,107 @@ public function test_get_item_schema() { $this->assertArrayHasKey( 'labels', $properties ); } + /** + * The `api.w.org/items` link should point at the type-filtered comments collection. + * + * @ticket 35214 + */ + public function test_get_item_links_to_filtered_comments_collection() { + $request = new WP_REST_Request( 'GET', '/wp/v2/comment-types/comment' ); + $response = rest_get_server()->dispatch( $request ); + $links = test_rest_expand_compact_links( $response->get_links() ); + + $this->assertArrayHasKey( 'https://api.w.org/items', $links ); + $this->assertSame( + add_query_arg( 'type', 'comment', rest_url( 'wp/v2/comments' ) ), + $links['https://api.w.org/items'][0]['href'] + ); + } + + /** + * A HEAD request to the collection should succeed without preparing any item data. + * + * @ticket 56481 + */ + public function test_get_items_with_head_request_should_not_prepare_comment_types_data() { + $request = new WP_REST_Request( 'HEAD', '/wp/v2/comment-types' ); + $hook_name = 'rest_prepare_comment_type'; + $filter = new MockAction(); + $callback = array( $filter, 'filter' ); + add_filter( $hook_name, $callback ); + $response = rest_get_server()->dispatch( $request ); + remove_filter( $hook_name, $callback ); + $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' ); + $this->assertSame( 0, $filter->get_call_count(), 'The "' . $hook_name . '" filter was called when it should not be for HEAD requests.' ); + $this->assertSame( array(), $response->get_data(), 'The server should not generate a body in response to a HEAD request.' ); + } + + /** + * @dataProvider data_readable_http_methods + * @ticket 56481 + * + * @param string $method The HTTP method to use. + */ + public function test_get_item_should_allow_adding_headers_via_filter( $method ) { + $request = new WP_REST_Request( $method, '/wp/v2/comment-types/comment' ); + + $hook_name = 'rest_prepare_comment_type'; + $filter = new MockAction(); + $callback = array( $filter, 'filter' ); + add_filter( $hook_name, $callback ); + $header_filter = new class() { + public static function add_custom_header( $response ) { + $response->header( 'X-Test-Header', 'Test' ); + + return $response; + } + }; + add_filter( $hook_name, array( $header_filter, 'add_custom_header' ) ); + $response = rest_get_server()->dispatch( $request ); + remove_filter( $hook_name, $callback ); + remove_filter( $hook_name, array( $header_filter, 'add_custom_header' ) ); + + $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' ); + $this->assertSame( 1, $filter->get_call_count(), 'The "' . $hook_name . '" filter should be called once.' ); + $headers = $response->get_headers(); + $this->assertArrayHasKey( 'X-Test-Header', $headers, 'The "X-Test-Header" header should be present in the response.' ); + $this->assertSame( 'Test', $headers['X-Test-Header'], 'The "X-Test-Header" header value should be equal to "Test".' ); + if ( 'HEAD' !== $method ) { + return null; + } + $this->assertSame( array(), $response->get_data(), 'The server should not generate a body in response to a HEAD request.' ); + } + + /** + * @dataProvider data_head_request_with_specified_fields_returns_success_response + * @ticket 56481 + * + * @param string $path The path to test. + */ + public function test_head_request_with_specified_fields_returns_success_response( $path ) { + $request = new WP_REST_Request( 'HEAD', $path ); + $request->set_param( '_fields', 'slug' ); + $server = rest_get_server(); + $response = $server->dispatch( $request ); + add_filter( 'rest_post_dispatch', 'rest_filter_response_fields', 10, 3 ); + $response = apply_filters( 'rest_post_dispatch', $response, $server, $request ); + remove_filter( 'rest_post_dispatch', 'rest_filter_response_fields', 10 ); + + $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' ); + } + + /** + * Data provider intended to provide paths for testing HEAD requests. + * + * @return array + */ + public static function data_head_request_with_specified_fields_returns_success_response() { + return array( + 'get_item request' => array( '/wp/v2/comment-types/comment' ), + 'get_items request' => array( '/wp/v2/comment-types' ), + ); + } + protected function check_comment_type_obj( $context, $comment_type_obj, $data, $links ) { $this->assertSame( $comment_type_obj->label, $data['name'] ); $this->assertSame( $comment_type_obj->name, $data['slug'] );