From 4480a8a0c8a8668bff33bb3db7350c3fcb92e392 Mon Sep 17 00:00:00 2001 From: tellyworth Date: Wed, 20 May 2020 17:22:50 +1000 Subject: [PATCH] Clean up block directory controller This draws on https://github.com/WordPress/gutenberg/pull/17669 and https://github.com/tellyworth/wordpress-develop/pull/1. It fixes many small issues with the block directory controller, and adds unit tests. --- ...ass-wp-rest-block-directory-controller.php | 455 +++++++++++++----- ...p-rest-block-directory-controller-test.php | 215 +++++++++ 2 files changed, 545 insertions(+), 125 deletions(-) create mode 100644 phpunit/class-wp-rest-block-directory-controller-test.php diff --git a/lib/class-wp-rest-block-directory-controller.php b/lib/class-wp-rest-block-directory-controller.php index abc7a2c6c9ae7a..061fe01519b9d9 100644 --- a/lib/class-wp-rest-block-directory-controller.php +++ b/lib/class-wp-rest-block-directory-controller.php @@ -1,7 +1,7 @@ namespace, '/' . $this->rest_base . '/search', array( - array( - 'methods' => WP_REST_Server::READABLE, - 'callback' => array( $this, 'get_items' ), - 'permission_callback' => array( $this, 'permissions_check' ), + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + 'args' => array( + 'term' => array( + 'required' => true, + ), ), - 'schema' => array( $this, 'get_item_schema' ), + 'schema' => array( $this, 'get_item_schema' ), ) ); + register_rest_route( $this->namespace, '/' . $this->rest_base . '/install', array( - array( - 'methods' => WP_REST_Server::CREATABLE, - 'callback' => array( $this, 'install_block' ), - 'permission_callback' => array( $this, 'permissions_check' ), + 'methods' => WP_REST_Server::CREATABLE, + 'callback' => array( $this, 'install_block' ), + 'permission_callback' => array( $this, 'install_items_permissions_check' ), + 'args' => array( + 'slug' => array( + 'required' => true, + ), ), - 'schema' => array( $this, 'get_item_schema' ), + 'schema' => array( $this, 'get_item_schema' ), ) ); + register_rest_route( $this->namespace, '/' . $this->rest_base . '/uninstall', array( - array( - 'methods' => WP_REST_Server::DELETABLE, - 'callback' => array( $this, 'uninstall_block' ), - 'permission_callback' => array( $this, 'permissions_check' ), + 'methods' => WP_REST_Server::DELETABLE, + 'callback' => array( $this, 'uninstall_block' ), + 'permission_callback' => array( $this, 'delete_items_permissions_check' ), + 'args' => array( + 'slug' => array( + 'required' => true, + ), ), - 'schema' => array( $this, 'get_item_schema' ), + 'schema' => array( $this, 'get_item_schema' ), ) ); } @@ -71,9 +82,11 @@ public function register_routes() { * * @since 6.5.0 * + * @param WP_REST_Request $request Full details about the request. * @return WP_Error|bool True if the request has permission, WP_Error object otherwise. + * phpcs:disable VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable */ - public function permissions_check() { + public function get_items_permissions_check( $request ) { if ( ! current_user_can( 'install_plugins' ) || ! current_user_can( 'activate_plugins' ) ) { return new WP_Error( 'rest_user_cannot_view', @@ -83,6 +96,59 @@ public function permissions_check() { return true; } + /* phpcs:enable */ + + /** + * Checks whether a given request has permission to install and activate plugins. + * + * @since 6.5.0 + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|bool True if the request has permission, WP_Error object otherwise. + */ + public function install_items_permissions_check( $request ) { + $plugin = $request->get_param( 'slug' ); + + if ( + ! current_user_can( 'install_plugins' ) || + ! current_user_can( 'activate_plugins' ) || + ! current_user_can( 'activate_plugin', $plugin ) + ) { + return new WP_Error( + 'rest_user_cannot_view', + __( 'Sorry, you are not allowed to install blocks.', 'gutenberg' ), + array( 'status' => rest_authorization_required_code() ) + ); + } + + return true; + } + + /** + * Checks whether a given request has permission to remove/deactivate plugins. + * + * @since 6.5.0 + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_Error|bool True if the request has permission, WP_Error object otherwise. + */ + public function delete_items_permissions_check( $request ) { + $plugin = $request->get_param( 'slug' ); + + if ( + ! current_user_can( 'delete_plugins' ) || + ! current_user_can( 'deactivate_plugins' ) || + ! current_user_can( 'deactivate_plugin', $plugin ) + ) { + return new WP_Error( + 'rest_user_cannot_delete', + __( 'Sorry, you are not allowed to uninstall blocks.', 'gutenberg' ), + array( 'status' => rest_authorization_required_code() ) + ); + } + + return true; + } /** * Installs and activates a plugin @@ -99,65 +165,63 @@ public function install_block( $request ) { include_once( ABSPATH . 'wp-admin/includes/class-wp-upgrader.php' ); include_once( ABSPATH . 'wp-admin/includes/plugin-install.php' ); + $slug = $request->get_param( 'slug' ); + + // Verify filesystem is accessible first. + $filesystem_available = self::is_filesystem_available(); + if ( is_wp_error( $filesystem_available ) ) { + return $filesystem_available; + } + $api = plugins_api( 'plugin_information', array( - 'slug' => $request->get_param( 'slug' ), + 'slug' => $slug, 'fields' => array( 'sections' => false, ), ) ); - // Check if the plugin is already installed. - $installed_plugins = get_plugins( '/' . $api->slug ); - - if ( empty( $installed_plugins ) ) { - - if ( is_wp_error( $api ) ) { - return WP_Error( $api->get_error_code(), $api->get_error_message() ); - } - - $skin = new WP_Ajax_Upgrader_Skin(); - $upgrader = new Plugin_Upgrader( $skin ); - - $filesystem_method = get_filesystem_method(); + if ( is_wp_error( $api ) ) { + $api->add_data( array( 'status' => 500 ) ); + return $api; + } - if ( 'direct' !== $filesystem_method ) { - return WP_Error( null, 'Only direct FS_METHOD is supported.' ); - } + $skin = new WP_Ajax_Upgrader_Skin(); + $upgrader = new Plugin_Upgrader( $skin ); - $result = $upgrader->install( $api->download_link ); + $result = $upgrader->install( $api->download_link ); - if ( is_wp_error( $result ) ) { - return WP_Error( $result->get_error_code(), $result->get_error_message() ); - } + if ( is_wp_error( $result ) ) { + return $result; + } - if ( is_wp_error( $skin->result ) ) { - return WP_Error( $skin->$result->get_error_code(), $skin->$result->get_error_message() ); - } + // This should be the same as $result above. + if ( is_wp_error( $skin->result ) ) { + return $skin->result; + } - if ( $skin->get_errors()->has_errors() ) { - return WP_Error( $skin->$result->get_error_code(), $skin->$result->get_error_messages() ); - } + if ( $skin->get_errors()->has_errors() ) { + return $skin->get_errors(); + } - if ( is_null( $result ) ) { - global $wp_filesystem; - // Pass through the error from WP_Filesystem if one was raised. - if ( $wp_filesystem instanceof WP_Filesystem_Base && is_wp_error( $wp_filesystem->errors ) && $wp_filesystem->errors->has_errors() ) { - return WP_Error( 'unable_to_connect_to_filesystem', esc_html( $wp_filesystem->errors->get_error_message() ) ); - } - return WP_Error( 'unable_to_connect_to_filesystem', __( 'Unable to connect to the filesystem. Please confirm your credentials.', 'gutenberg' ) ); + if ( is_null( $result ) ) { + global $wp_filesystem; + // Pass through the error from WP_Filesystem if one was raised. + if ( $wp_filesystem instanceof WP_Filesystem_Base && is_wp_error( $wp_filesystem->errors ) && $wp_filesystem->errors->has_errors() ) { + return new WP_Error( 'unable_to_connect_to_filesystem', esc_html( $wp_filesystem->errors->get_error_message() ), array( 'status' => 500 ) ); } + return new WP_Error( 'unable_to_connect_to_filesystem', __( 'Unable to connect to the filesystem. Please confirm your credentials.', 'gutenberg' ), array( 'status' => 500 ) ); } - $install_status = install_plugin_install_status( $api ); + // Find the plugin to activate it. + $plugin_files = get_plugins( '/' . $slug ); + $plugin_files = array_keys( $plugin_files ); - $activate_result = activate_plugin( $install_status['file'] ); + $plugin_file = $slug . '/' . reset( $plugin_files ); - if ( is_wp_error( $activate_result ) ) { - return WP_Error( $activate_result->get_error_code(), $activate_result->get_error_message() ); - } + activate_plugin( $plugin_file ); return rest_ensure_response( array( 'success' => true ) ); } @@ -177,32 +241,33 @@ public function uninstall_block( $request ) { include_once( ABSPATH . 'wp-admin/includes/class-wp-upgrader.php' ); include_once( ABSPATH . 'wp-admin/includes/plugin-install.php' ); - $api = plugins_api( - 'plugin_information', - array( - 'slug' => $request->get_param( 'slug' ), - 'fields' => array( - 'sections' => false, - ), - ) - ); + $slug = trim( $request->get_param( 'slug' ) ); - if ( is_wp_error( $api ) ) { - return WP_Error( $api->get_error_code(), $api->get_error_message() ); + if ( ! $slug ) { + return new WP_Error( 'slug_not_provided', 'Valid slug not provided.', array( 'status' => 400 ) ); } - $install_status = install_plugin_install_status( $api ); + // Verify filesystem is accessible first. + $filesystem_available = self::is_filesystem_available(); + if ( is_wp_error( $filesystem_available ) ) { + return $filesystem_available; + } - $deactivate_result = deactivate_plugins( $install_status['file'] ); + $plugin_files = get_plugins( '/' . $slug ); - if ( is_wp_error( $deactivate_result ) ) { - return WP_Error( $deactivate_result->get_error_code(), $deactivate_result->get_error_message() ); + if ( ! $plugin_files ) { + return new WP_Error( 'block_not_found', 'Valid slug not provided.', array( 'status' => 400 ) ); } - $delete_result = delete_plugins( array( $install_status['file'] ) ); + $plugin_files = array_keys( $plugin_files ); + $plugin_file = $slug . '/' . reset( $plugin_files ); + + deactivate_plugins( $plugin_file ); + + $delete_result = delete_plugins( array( $plugin_file ) ); if ( is_wp_error( $delete_result ) ) { - return WP_Error( $delete_result->get_error_code(), $delete_result->get_error_message() ); + return $delete_result; } return rest_ensure_response( true ); @@ -218,95 +283,235 @@ public function uninstall_block( $request ) { */ public function get_items( $request ) { - $search_string = $request->get_param( 'term' ); + $search_string = trim( $request->get_param( 'term' ) ); if ( empty( $search_string ) ) { return rest_ensure_response( array() ); } - include( ABSPATH . WPINC . '/version.php' ); + require_once ABSPATH . 'wp-admin/includes/plugin-install.php'; + require_once ABSPATH . 'wp-admin/includes/plugin.php'; - $url = 'http://api.wordpress.org/plugins/info/1.2/'; - $url = add_query_arg( + $response = plugins_api( + 'query_plugins', array( - 'action' => 'query_plugins', - 'request[block]' => $search_string, - 'request[wp_version]' => '5.3', - 'request[per_page]' => '3', - ), - $url + 'block' => $search_string, + 'per_page' => 3, + ) ); - $ssl = wp_http_supports( array( 'ssl' ) ); - if ( $ssl ) { - $url = set_url_scheme( $url, 'https' ); + + if ( is_wp_error( $response ) ) { + $response->add_data( array( 'status' => 500 ) ); + return $response; } - global $wp_version; - $http_args = array( - 'timeout' => 15, - 'user-agent' => 'WordPress/' . $wp_version . '; ' . home_url( '/' ), - ); + $result = array(); - $request = wp_remote_get( $url, $http_args ); - $response = json_decode( wp_remote_retrieve_body( $request ), true ); + foreach ( $response->plugins as $plugin ) { + $installed_plugins = get_plugins( '/' . $plugin['slug'] ); - if ( ! function_exists( 'get_plugins' ) ) { - require_once ABSPATH . 'wp-admin/includes/plugin.php'; + // Only show uninstalled blocks. + if ( empty( $installed_plugins ) ) { + $data = $this->prepare_item_for_response( $plugin, $request ); + $result[] = $this->prepare_response_for_collection( $data ); + } } - $result = array(); + return rest_ensure_response( $result ); + } + + /** + * Determine if the endpoints are available. + * + * Only the 'Direct' filesystem transport, and SSH/FTP when credentials are stored are supported at present. + * + * @since 6.5.0 + * + * @return bool|WP_Error True if filesystem is available, WP_Error otherwise. + */ + private static function is_filesystem_available() { + $filesystem_method = get_filesystem_method(); - foreach ( $response['plugins'] as $plugin ) { - $result[] = self::parse_block_metadata( $plugin ); + if ( 'direct' === $filesystem_method ) { + return true; } - return rest_ensure_response( $result ); + ob_start(); + $filesystem_credentials_are_stored = request_filesystem_credentials( self_admin_url() ); + ob_end_clean(); + + if ( $filesystem_credentials_are_stored ) { + return true; + } + + return new WP_Error( 'fs_unavailable', __( 'The filesystem is currently unavailable for installing blocks.' ) ); } /** - * Parse block metadata for a block + * Parse block metadata for a block, and prepare it for an API repsonse. * * @since 6.5.0 * * @param WP_Object $plugin The plugin metadata. + * @param WP_REST_Request $request Request object. * @return WP_Error|WP_REST_Response Response object on success, or WP_Error object on failure. */ - private static function parse_block_metadata( $plugin ) { - $block = new stdClass(); + public function prepare_item_for_response( $plugin, $request ) { // There might be multiple blocks in a plugin. Only the first block is mapped. $block_data = reset( $plugin['blocks'] ); - $block->name = $block_data['name']; - $block->title = $block_data['title']; - // Plugin's description, not description in block.json. - $block->description = wp_trim_words( wp_strip_all_tags( $plugin['description'] ), 30, '...' ); + // A data array containing the properties we'll return. + $block = array( + 'name' => $block_data['name'], + 'title' => ( $block_data['title'] ? $block_data['title'] : $plugin['name'] ), + 'description' => wp_trim_words( $plugin['description'], 30, '...' ), + 'id' => $plugin['slug'], + 'rating' => $plugin['rating'] / 20, + 'rating_count' => intval( $plugin['num_ratings'] ), + 'active_installs' => intval( $plugin['active_installs'] ), + 'author_block_rating' => $plugin['author_block_rating'] / 20, + 'author_block_count' => intval( $plugin['author_block_count'] ), + 'author' => wp_strip_all_tags( $plugin['author'] ), + 'icon' => ( isset( $plugin['icons']['1x'] ) ? $plugin['icons']['1x'] : 'block-default' ), + 'assets' => array(), + 'humanized_updated' => sprintf( + /* translators: %s: Human-readable time difference. */ + __( '%s ago' ), + human_time_diff( strtotime( $plugin['last_updated'] ), current_time( 'timestamp' ) ) + ), + ); - $block->id = $plugin['slug']; - $block->rating = $plugin['rating'] / 20; - $block->rating_count = $plugin['num_ratings']; - $block->active_installs = $plugin['active_installs']; - $block->author_block_rating = $plugin['author_block_rating'] / 20; - $block->author_block_count = (int) $plugin['author_block_count']; + foreach ( $plugin['block_assets'] as $asset ) { + // TODO: Return from API, not client-set. + $block[ 'assets' ][] = 'https://plugins.svn.wordpress.org/' . $plugin['slug'] . $asset; + } - // Plugin's author, not author in block.json. - $block->author = wp_strip_all_tags( $plugin['author'] ); + $response = new WP_REST_Response( $block ); - // Plugin's icons or icon in block.json. - $block->icon = isset( $plugin['icons']['1x'] ) ? $plugin['icons']['1x'] : 'block-default'; + return $response; + } - $block->assets = array(); + /** + * Retrieves the theme's schema, conforming to JSON Schema. + * + * @since 5.5.0 + * + * @return array Item schema data. + */ + public function get_item_schema() { + $this->schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'block-directory-item', + 'type' => 'object', + 'properties' => array( + 'name' => array( + 'description' => __( "The block name, in namespace/block-name format." ), + 'type' => 'string', + 'context' => array( 'view' ), + ), + 'title' => array( + 'description' => __( "The block title, in human readable format." ), + 'type' => 'string', + 'context' => array( 'view' ), + ), + 'description' => array( + 'description' => __( "A short description of the block, in human readable format." ), + 'type' => 'string', + 'context' => array( 'view' ), + ), + 'id' => array( + 'description' => __( "The block slug." ), + 'type' => 'string', + 'context' => array( 'view' ), + ), + 'rating' => array( + 'description' => __( "The star rating of the block." ), + 'type' => 'integer', + 'context' => array( 'view' ), + ), + 'rating_count' => array( + 'description' => __( "The number of ratings." ), + 'type' => 'integer', + 'context' => array( 'view' ), + ), + 'active_installs' => array( + 'description' => __( "The number sites that have activated this block." ), + 'type' => 'string', + 'context' => array( 'view' ), + ), + 'author_block_rating' => array( + 'description' => __( "The average rating of blocks published by the same author." ), + 'type' => 'integer', + 'context' => array( 'view' ), + ), + 'author_block_count' => array( + 'description' => __( "The number of blocks published by the same author." ), + 'type' => 'integer', + 'context' => array( 'view' ), + ), + 'author' => array( + 'description' => __( "The WordPress.org username of the block author." ), + 'type' => 'string', + 'context' => array( 'view' ), + ), + 'icon' => array( + 'description' => __( "The block icon." ), + 'type' => 'string', + 'format' => 'uri', + 'context' => array( 'view' ), + ), + 'humanized_updated' => array( + 'description' => __( "The date when the block was last updated, in fuzzy human readable format." ), + 'type' => 'string', + 'context' => array( 'view' ), + ), + 'assets' => array( + 'description' => __( 'An object representing the block CSS and JavaScript assets.' ), + 'type' => 'array', + 'context' => array( 'view' ), + 'readonly' => true, + 'items' => array( + 'type' => 'string', + 'format' => 'uri', + ), - foreach ( $plugin['block_assets'] as $asset ) { - $block->assets[] = 'https://plugins.svn.wordpress.org/' . $plugin['slug'] . $asset; - } + ), + + ), + ); + + return $this->schema; + } - $block->humanized_updated = sprintf( - /* translators: %s: Human-readable time difference. */ - __( '%s ago', 'gutenberg' ), - human_time_diff( strtotime( $plugin['last_updated'] ), time() ) + + /** + * Retrieves the search params for the blocks collection. + * + * @since 5.5.0 + * + * @return array Collection parameters. + */ + public function get_collection_params() { + $query_params = parent::get_collection_params(); + + $query_params['term'] = array( + 'description' => __( 'Limit result set to blocks matching the search term.' ), + 'type' => 'array', + 'term' => array( + 'type' => 'string', + ), + 'required' => true, ); - return $block; + /** + * Filter collection parameters for the block directory controller. + * + * @since 5.0.0 + * + * @param array $query_params JSON Schema-formatted collection parameters. + */ + return apply_filters( 'rest_block_directory_collection_params', $query_params ); } + } diff --git a/phpunit/class-wp-rest-block-directory-controller-test.php b/phpunit/class-wp-rest-block-directory-controller-test.php new file mode 100644 index 00000000000000..9f4ffd49c9d7ff --- /dev/null +++ b/phpunit/class-wp-rest-block-directory-controller-test.php @@ -0,0 +1,215 @@ +user->create( + array( + 'role' => 'administrator', + ) + ); + + // Ensure routes are registered regardless of the `gutenberg-block-directory` experimental setting. + // This should be removed when `gutenberg_register_rest_block_directory()` is unconditional. + add_filter( 'rest_api_init', function() { + $block_directory_controller = new WP_REST_Block_Directory_Controller(); + $block_directory_controller->register_routes(); + } ); + } + + public static function wpTearDownAfterClass() { + self::delete_user( self::$admin_id ); + } + + public function test_register_routes() { + $routes = rest_get_server()->get_routes(); + + $this->assertArrayHasKey( '/__experimental/block-directory/search', $routes ); + $this->assertArrayHasKey( '/__experimental/block-directory/install', $routes ); + $this->assertArrayHasKey( '/__experimental/block-directory/uninstall', $routes ); + } + + /** + * Tests that an error is returned if the block plugin slug is not provided + */ + function test_should_throw_no_slug_error() { + wp_set_current_user( self::$admin_id ); + + $request = new WP_REST_Request( 'POST', '/__experimental/block-directory/install', [] ); + $result = rest_do_request( $request ); + + $this->assertErrorResponse( 'rest_missing_callback_param', $result, 400 ); + } + + /** + * Tests that the search endpoint does not return an error + */ + function test_simple_search() { + wp_set_current_user( self::$admin_id ); + + $request = new WP_REST_Request( 'GET', '/__experimental/block-directory/search' ); + $request->set_query_params( array( 'term' => 'foo' ) ); + + $result = rest_do_request( $request ); + $this->assertNotWPError( $result ); + $this->assertEquals( 200, $result->status ); + } + + /** + * Simulate a network failure on outbound http requests to a given hostname. + */ + function prevent_requests_to_host( $blocked_host = 'api.wordpress.org' ) { + // apply_filters( 'pre_http_request', false, $parsed_args, $url ); + add_filter( 'pre_http_request', function( $return, $args, $url ) use ( $blocked_host ) { + if ( @parse_url( $url, PHP_URL_HOST ) === $blocked_host ) { + return new WP_Error( 'plugins_api_failed', "An expected error occurred connecting to $blocked_host because of a unit test", "cURL error 7: Failed to connect to $blocked_host port 80: Connection refused" ); + + } + return $return; + }, 10, 3 ); + } + + /** + * Tests that the search endpoint returns WP_Error when the server is unreachable. + */ + function test_search_unreachable() { + wp_set_current_user( self::$admin_id ); + + $request = new WP_REST_Request( 'GET', '/__experimental/block-directory/search' ); + $request->set_query_params( array( 'term' => 'foo' ) ); + + $this->prevent_requests_to_host( 'api.wordpress.org' ); + + $this->expectException('PHPUnit_Framework_Error_Warning'); + $response = rest_do_request( $request ); + $this->assertErrorResponse( 'plugins_api_failed', $response, 500 ); + } + + /** + * Tests that the install endpoint returns WP_Error when the server is unreachable. + */ + function test_install_unreachable() { + wp_set_current_user( self::$admin_id ); + + $request = new WP_REST_Request( 'POST', '/__experimental/block-directory/install' ); + $request->set_query_params( array( 'slug' => 'foo' ) ); + + $this->prevent_requests_to_host( 'api.wordpress.org' ); + + $this->expectException('PHPUnit_Framework_Error_Warning'); + $response = rest_do_request( $request ); + $this->assertErrorResponse( 'plugins_api_failed', $response, 500 ); + } + + /** + * Should fail with a permission error if requesting user is not logged in. + */ + function test_simple_search_no_perms() { + $request = new WP_REST_Request( 'GET', '/__experimental/block-directory/search' ); + $request->set_query_params( array( 'term' => 'foo' ) ); + $response = rest_do_request( $request ); + $data = $response->get_data(); + + $this->assertEquals( $data['code'], 'rest_user_cannot_view' ); + } + + /** + * Make sure a search with the right permissions returns something. + */ + function test_simple_search_with_perms() { + wp_set_current_user( self::$admin_id ); + + // This will hit the live API. We're searching for `block` which should definitely return at least one result. + $request = new WP_REST_Request( 'GET', '/__experimental/block-directory/search' ); + $request->set_query_params( array( 'term' => 'block' ) ); + $response = rest_do_request( $request ); + $data = $response->get_data(); + + $this->assertEquals( 200, $response->status ); + // At least one result + $this->assertGreaterThanOrEqual( 1, count( $data ) ); + // Each result should be an object with important attributes set + foreach ( $data as $plugin ) { + $this->assertArrayHasKey( 'name', $plugin ); + $this->assertArrayHasKey( 'title', $plugin ); + $this->assertArrayHasKey( 'id', $plugin ); + $this->assertArrayHasKey( 'author_block_rating', $plugin ); + $this->assertArrayHasKey( 'assets', $plugin ); + $this->assertArrayHasKey( 'humanized_updated', $plugin ); + } + } + + /** + * A search with zero results should return a 200 response. + */ + function test_simple_search_no_results() { + wp_set_current_user( self::$admin_id ); + + $request = new WP_REST_Request( 'GET', '/__experimental/block-directory/search' ); + $request->set_query_params( array( 'term' => '0c4549ee68f24eaaed46a49dc983ecde' ) ); + $response = rest_do_request( $request ); + $data = $response->get_data(); + + // Should produce a 200 status with an empty array. + $this->assertEquals( 200, $response->status ); + $this->assertEquals( array(), $data ); + } + + /** + * Should fail with a permission error if requesting user is not logged in. + */ + function test_simple_install_no_perms() { + $request = new WP_REST_Request( 'POST', '/__experimental/block-directory/install' ); + $request->set_query_params( array( 'slug' => 'foo' ) ); + $response = rest_do_request( $request ); + $data = $response->get_data(); + + $this->assertEquals( $data['code'], 'rest_user_cannot_view' ); + } + + /** + * Make sure an install with permissions correctly handles an unknown slug. + */ + function test_simple_install_with_perms_bad_slug() { + wp_set_current_user( self::$admin_id ); + + // This will hit the live API. + $request = new WP_REST_Request( 'POST', '/__experimental/block-directory/install' ); + $request->set_query_params( array( 'slug' => 'alex-says-this-block-definitely-doesnt-exist' ) ); + $response = rest_do_request( $request ); + + // Is this an appropriate status? + $this->assertErrorResponse( 'plugins_api_failed', $response, 500 ); + } + + /** + * Make sure the search schema is available and correct. + */ + function test_search_schema() { + wp_set_current_user( self::$admin_id ); + + $request = new WP_REST_Request( 'OPTIONS', '/__experimental/block-directory/search' ); + $request->set_query_params( array( 'term' => 'foo' ) ); + $response = rest_do_request( $request ); + $data = $response->get_data(); + + // Check endpoints + $this->assertEquals( [ 'GET' ], $data['endpoints'][0]['methods'] ); + $this->assertEquals( [ 'term' => [ 'required' => true ] ], $data['endpoints'][0]['args'] ); + + // Check schema + $this->assertEquals( [ + 'description' => __( "The block name, in namespace/block-name format." ), + 'type' => [ 'string' ], + 'context' => [ 'view' ], + ], $data['schema']['properties']['name'] ); + // TODO: ..etc.. + } + +}