diff --git a/plugins/woocommerce/changelog/add-plugin-install-queue-endpoint b/plugins/woocommerce/changelog/add-plugin-install-queue-endpoint new file mode 100644 index 00000000000..f24bafa05db --- /dev/null +++ b/plugins/woocommerce/changelog/add-plugin-install-queue-endpoint @@ -0,0 +1,4 @@ +Significance: minor +Type: update + +Add new REST endpoints at onboarding/plugins to support async plugin installation with real time error tracking. diff --git a/plugins/woocommerce/src/Admin/API/Init.php b/plugins/woocommerce/src/Admin/API/Init.php index 338bda5277d..cb60a85197f 100644 --- a/plugins/woocommerce/src/Admin/API/Init.php +++ b/plugins/woocommerce/src/Admin/API/Init.php @@ -87,6 +87,7 @@ class Init { 'Automattic\WooCommerce\Admin\API\OnboardingProfile', 'Automattic\WooCommerce\Admin\API\OnboardingTasks', 'Automattic\WooCommerce\Admin\API\OnboardingThemes', + 'Automattic\WooCommerce\Admin\API\OnboardingPlugins', 'Automattic\WooCommerce\Admin\API\NavigationFavorites', 'Automattic\WooCommerce\Admin\API\Taxes', 'Automattic\WooCommerce\Admin\API\MobileAppMagicLink', diff --git a/plugins/woocommerce/src/Admin/API/OnboardingPlugins.php b/plugins/woocommerce/src/Admin/API/OnboardingPlugins.php new file mode 100644 index 00000000000..cbafe06c163 --- /dev/null +++ b/plugins/woocommerce/src/Admin/API/OnboardingPlugins.php @@ -0,0 +1,324 @@ +namespace, + '/' . $this->rest_base . '/install-async', + array( + array( + 'methods' => 'POST', + 'callback' => array( $this, 'install_async' ), + 'permission_callback' => array( $this, 'can_install_plugins' ), + 'args' => array( + 'plugins' => array( + 'description' => 'A list of plugins to install', + 'type' => 'array', + 'items' => 'string', + 'sanitize_callback' => function ( $value ) { + return array_map( + function ( $value ) { + return sanitize_text_field( $value ); + }, + $value + ); + }, + 'required' => true, + ), + ), + ), + 'schema' => array( $this, 'get_install_async_schema' ), + ) + ); + register_rest_route( + $this->namespace, + '/' . $this->rest_base . '/install-and-activate', + array( + array( + 'methods' => 'POST', + 'callback' => array( $this, 'install_and_activate' ), + 'permission_callback' => array( $this, 'can_install_and_activate_plugins' ), + + ), + 'schema' => array( $this, 'get_install_activate_schema' ), + ) + ); + register_rest_route( + $this->namespace, + '/' . $this->rest_base . '/scheduled-installs/(?P\w+)', + array( + array( + 'methods' => 'GET', + 'callback' => array( $this, 'get_scheduled_installs' ), + 'permission_callback' => array( $this, 'can_install_plugins' ), + ), + 'schema' => array( $this, 'get_install_async_schema' ), + ) + ); + } + + /** + * Install and activate a plugin. + * + * @param WP_REST_Request $request WP Request object. + * + * @return WP_REST_Response + */ + public function install_and_activate( WP_REST_Request $request ) { + $response = array(); + $response['install'] = PluginsHelper::install_plugins( $request->get_param( 'plugins' ) ); + $response['activate'] = PluginsHelper::activate_plugins( $response['install']['installed'] ); + + return new WP_REST_Response( $response ); + } + + /** + * Queue plugin install request. + * + * @param WP_REST_Request $request WP_REST_Request object. + * + * @return array + */ + public function install_async( WP_REST_Request $request ) { + $plugins = $request->get_param( 'plugins' ); + $job_id = uniqid(); + + WC()->queue()->add( 'woocommerce_plugins_install_async_callback', array( $plugins, $job_id ) ); + + $plugin_status = array(); + foreach ( $plugins as $plugin ) { + $plugin_status[ $plugin ] = array( + 'status' => 'pending', + 'errors' => array(), + ); + } + + return array( + 'job_id' => $job_id, + 'status' => 'pending', + 'plugins' => $plugin_status, + ); + } + + /** + * Returns current status of given job. + * + * @param WP_REST_Request $request WP_REST_Request object. + * + * @return array|WP_REST_Response + */ + public function get_scheduled_installs( WP_REST_Request $request ) { + $job_id = $request->get_param( 'job_id' ); + + $actions = WC()->queue()->search( + array( + 'hook' => 'woocommerce_plugins_install_async_callback', + 'search' => $job_id, + 'orderby' => 'date', + 'order' => 'DESC', + ) + ); + + $actions = array_filter( + PluginsHelper::get_action_data( $actions ), + function( $action ) use ( $job_id ) { + return $action['job_id'] === $job_id; + } + ); + + if ( empty( $actions ) ) { + return new WP_REST_Response( null, 404 ); + } + + $response = array( + 'job_id' => $actions[0]['job_id'], + 'status' => $actions[0]['status'], + ); + + $option = get_option( 'woocommerce_onboarding_plugins_install_async_' . $job_id ); + if ( isset( $option['plugins'] ) ) { + $response['plugins'] = $option['plugins']; + } + + return $response; + } + + /** + * Check whether the current user has permission to install plugins + * + * @return WP_Error|boolean + */ + public function can_install_plugins() { + if ( ! current_user_can( 'install_plugins' ) ) { + return new WP_Error( + 'woocommerce_rest_cannot_update', + __( 'Sorry, you cannot manage plugins.', 'woocommerce' ), + array( 'status' => rest_authorization_required_code() ) + ); + } + + return true; + } + + /** + * Check whether the current user has permission to install and activate plugins + * + * @return WP_Error|boolean + */ + public function can_install_and_activate_plugins() { + if ( ! current_user_can( 'install_plugins' ) || ! current_user_can( 'activate_plugins' ) ) { + return new WP_Error( + 'woocommerce_rest_cannot_update', + __( 'Sorry, you cannot manage plugins.', 'woocommerce' ), + array( 'status' => rest_authorization_required_code() ) + ); + } + + return true; + } + + /** + * JSON Schema for both install-async and scheduled-installs endpoints. + * + * @return array + */ + public function get_install_async_schema() { + return array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'Install Async Schema', + 'type' => 'object', + 'properties' => array( + 'type' => 'object', + 'properties' => array( + 'job_id' => 'integer', + 'status' => array( + 'type' => 'string', + 'enum' => array( 'pending', 'complete', 'failed' ), + ), + ), + ), + ); + } + + /** + * JSON Schema for install-and-activate endpoint. + * + * @return array + */ + public function get_install_activate_schema() { + $error_schema = array( + 'type' => 'object', + 'patternProperties' => array( + '^.*$' => array( + 'type' => 'string', + ), + ), + 'items' => array( + 'type' => 'string', + ), + ); + + $install_schema = array( + 'type' => 'object', + 'properties' => array( + 'installed' => array( + 'type' => 'array', + 'items' => array( + 'type' => 'string', + ), + ), + 'results' => array( + 'type' => 'array', + 'items' => array( + 'type' => 'string', + ), + ), + 'errors' => array( + 'type' => 'object', + 'properties' => array( + 'errors' => $error_schema, + 'error_data' => $error_schema, + ), + ), + ), + ); + + $activate_schema = array( + 'type' => 'object', + 'properties' => array( + 'activated' => array( + 'type' => 'array', + 'items' => array( + 'type' => 'string', + ), + ), + 'active' => array( + 'type' => 'array', + 'items' => array( + 'type' => 'string', + ), + ), + 'errors' => array( + 'type' => 'object', + 'properties' => array( + 'errors' => $error_schema, + 'error_data' => $error_schema, + ), + ), + ), + ); + + return array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'Install and Activate Schema', + 'type' => 'object', + 'properties' => array( + 'type' => 'object', + 'properties' => array( + 'install' => $install_schema, + 'activate' => $activate_schema, + ), + ), + ); + } +} diff --git a/plugins/woocommerce/src/Admin/PluginsHelper.php b/plugins/woocommerce/src/Admin/PluginsHelper.php index 3f9e41a380a..414b1cd1327 100644 --- a/plugins/woocommerce/src/Admin/PluginsHelper.php +++ b/plugins/woocommerce/src/Admin/PluginsHelper.php @@ -7,6 +7,16 @@ namespace Automattic\WooCommerce\Admin; +use ActionScheduler; +use ActionScheduler_DBStore; +use ActionScheduler_QueueRunner; +use Automatic_Upgrader_Skin; +use Automattic\WooCommerce\Admin\PluginsInstallLoggers\AsyncPluginsInstallLogger; +use Automattic\WooCommerce\Admin\PluginsInstallLoggers\PluginsInstallLogger; +use Plugin_Upgrader; +use WP_Error; +use WP_Upgrader; + defined( 'ABSPATH' ) || exit; if ( ! function_exists( 'get_plugins' ) ) { @@ -23,6 +33,7 @@ class PluginsHelper { */ public static function init() { add_action( 'woocommerce_plugins_install_callback', array( __CLASS__, 'install_plugins' ), 10, 2 ); + add_action( 'woocommerce_plugins_install_async_callback', array( __CLASS__, 'install_plugins_async_callback' ), 10, 2 ); add_action( 'woocommerce_plugins_activate_callback', array( __CLASS__, 'activate_plugins' ), 10, 2 ); } @@ -60,8 +71,9 @@ class PluginsHelper { */ public static function get_installed_plugin_slugs() { return array_map( - function( $plugin_path ) { + function ( $plugin_path ) { $path_parts = explode( '/', $plugin_path ); + return $path_parts[0]; }, array_keys( get_plugins() ) @@ -93,8 +105,9 @@ class PluginsHelper { */ public static function get_active_plugin_slugs() { return array_map( - function( $plugin_path ) { + function ( $plugin_path ) { $path_parts = explode( '/', $plugin_path ); + return $path_parts[0]; }, get_option( 'active_plugins', array() ) @@ -110,6 +123,7 @@ class PluginsHelper { */ public static function is_plugin_installed( $plugin ) { $plugin_path = self::get_plugin_path_from_slug( $plugin ); + return $plugin_path ? array_key_exists( $plugin_path, get_plugins() ) : false; } @@ -122,6 +136,7 @@ class PluginsHelper { */ public static function is_plugin_active( $plugin ) { $plugin_path = self::get_plugin_path_from_slug( $plugin ); + return $plugin_path ? in_array( $plugin_path, get_option( 'active_plugins', array() ), true ) : false; } @@ -142,20 +157,26 @@ class PluginsHelper { /** * Install an array of plugins. * - * @param array $plugins Plugins to install. + * @param array $plugins Plugins to install. + * @param PluginsInstallLogger|null $logger an optional logger. + * * @return array */ - public static function install_plugins( $plugins ) { + public static function install_plugins( $plugins, PluginsInstallLogger $logger = null ) { /** * Filter the list of plugins to install. * * @param array $plugins A list of the plugins to install. + * * @since 6.4.0 */ $plugins = apply_filters( 'woocommerce_admin_plugins_pre_install', $plugins ); if ( empty( $plugins ) || ! is_array( $plugins ) ) { - return new \WP_Error( 'woocommerce_plugins_invalid_plugins', __( 'Plugins must be a non-empty array.', 'woocommerce' ) ); + return new WP_Error( + 'woocommerce_plugins_invalid_plugins', + __( 'Plugins must be a non-empty array.', 'woocommerce' ) + ); } require_once ABSPATH . 'wp-admin/includes/plugin.php'; @@ -169,13 +190,15 @@ class PluginsHelper { $installed_plugins = array(); $results = array(); $time = array(); - $errors = new \WP_Error(); + $errors = new WP_Error(); foreach ( $plugins as $plugin ) { $slug = sanitize_key( $plugin ); + $logger && $logger->install_requested( $plugin ); if ( isset( $existing_plugins[ $slug ] ) ) { $installed_plugins[] = $plugin; + $logger && $logger->installed( $plugin, 0 ); continue; } @@ -193,8 +216,14 @@ class PluginsHelper { if ( is_wp_error( $api ) ) { $properties = array( - /* translators: %s: plugin slug (example: woocommerce-services) */ - 'error_message' => sprintf( __( 'The requested plugin `%s` could not be installed. Plugin API call failed.', 'woocommerce' ), $slug ), + 'error_message' => sprintf( + // translators: %s: plugin slug (example: woocommerce-services). + __( + 'The requested plugin `%s` could not be installed. Plugin API call failed.', + 'woocommerce' + ), + $slug + ), 'api_error_message' => $api->get_error_message(), 'slug' => $slug, ); @@ -204,32 +233,40 @@ class PluginsHelper { * Action triggered when a plugin API call failed. * * @param string $slug The plugin slug. - * @param \WP_Error $api The API response. + * @param WP_Error $api The API response. + * * @since 6.4.0 */ do_action( 'woocommerce_plugins_install_api_error', $slug, $api ); - $errors->add( - $plugin, - sprintf( - /* translators: %s: plugin slug (example: woocommerce-services) */ - __( 'The requested plugin `%s` could not be installed. Plugin API call failed.', 'woocommerce' ), - $slug - ) + $error_message = sprintf( + /* translators: %s: plugin slug (example: woocommerce-services) */ + __( 'The requested plugin `%s` could not be installed. Plugin API call failed.', 'woocommerce' ), + $slug ); + $errors->add( $plugin, $error_message ); + $logger && $logger->add_error( $plugin, $error_message ); + continue; } - $upgrader = new \Plugin_Upgrader( new \Automatic_Upgrader_Skin() ); - $result = $upgrader->install( $api->download_link ); + $upgrader = new Plugin_Upgrader( new Automatic_Upgrader_Skin() ); + $result = $upgrader->install( $api->download_link ); + // result can be false or WP_Error. $results[ $plugin ] = $result; $time[ $plugin ] = round( ( microtime( true ) - $start_time ) * 1000 ); if ( is_wp_error( $result ) || is_null( $result ) ) { $properties = array( - /* translators: %s: plugin slug (example: woocommerce-services) */ - 'error_message' => sprintf( __( 'The requested plugin `%s` could not be installed.', 'woocommerce' ), $slug ), + 'error_message' => sprintf( + /* translators: %s: plugin slug (example: woocommerce-services) */ + __( + 'The requested plugin `%s` could not be installed.', + 'woocommerce' + ), + $slug + ), 'slug' => $slug, 'api_version' => $api->version, 'api_download_link' => $api->download_link, @@ -243,26 +280,33 @@ class PluginsHelper { * * @param string $slug The plugin slug. * @param object $api The plugin API object. - * @param \WP_Error|null $result The result of the plugin installation. - * @param \Plugin_Upgrader $upgrader The plugin upgrader. + * @param WP_Error|null $result The result of the plugin installation. + * @param Plugin_Upgrader $upgrader The plugin upgrader. + * * @since 6.4.0 - */ + */ do_action( 'woocommerce_plugins_install_error', $slug, $api, $result, $upgrader ); + $install_error_message = sprintf( + /* translators: %s: plugin slug (example: woocommerce-services) */ + __( 'The requested plugin `%s` could not be installed. Upgrader install failed.', 'woocommerce' ), + $slug + ); $errors->add( $plugin, - sprintf( - /* translators: %s: plugin slug (example: woocommerce-services) */ - __( 'The requested plugin `%s` could not be installed. Upgrader install failed.', 'woocommerce' ), - $slug - ) + $install_error_message ); + $logger && $logger->add_error( $plugin, $install_error_message ); + continue; } $installed_plugins[] = $plugin; + $logger && $logger->installed( $plugin, $time[ $plugin ] ); } + $logger && $logger->complete(); + $data = array( 'installed' => $installed_plugins, 'results' => $results, @@ -273,19 +317,40 @@ class PluginsHelper { return $data; } + /** + * Callback regsitered by OnboardingPlugins::install_async. + * + * It is used to call install_plugins with a custom logger. + * + * @param array $plugins A list of plugins to install. + * @param string $job_id An unique job I.D. + * @return bool + */ + public function install_plugins_async_callback( array $plugins, string $job_id ) { + $option_name = 'woocommerce_onboarding_plugins_install_async_' . $job_id; + $logger = new AsyncPluginsInstallLogger( $option_name ); + self::install_plugins( $plugins, $logger ); + return true; + } + /** * Schedule plugin installation. * * @param array $plugins Plugins to install. + * * @return string Job ID. */ public static function schedule_install_plugins( $plugins ) { if ( empty( $plugins ) || ! is_array( $plugins ) ) { - return new \WP_Error( 'woocommerce_plugins_invalid_plugins', __( 'Plugins must be a non-empty array.', 'woocommerce' ), 404 ); + return new WP_Error( + 'woocommerce_plugins_invalid_plugins', + __( 'Plugins must be a non-empty array.', 'woocommerce' ), + 404 + ); } $job_id = uniqid(); - WC()->queue()->schedule_single( time() + 5, 'woocommerce_plugins_install_callback', array( $plugins, $job_id ) ); + WC()->queue()->schedule_single( time() + 5, 'woocommerce_plugins_install_callback', array( $plugins ) ); return $job_id; } @@ -294,11 +359,16 @@ class PluginsHelper { * Activate the requested plugins. * * @param array $plugins Plugins. + * * @return WP_Error|array Plugin Status */ public static function activate_plugins( $plugins ) { if ( empty( $plugins ) || ! is_array( $plugins ) ) { - return new \WP_Error( 'woocommerce_plugins_invalid_plugins', __( 'Plugins must be a non-empty array.', 'woocommerce' ), 404 ); + return new WP_Error( + 'woocommerce_plugins_invalid_plugins', + __( 'Plugins must be a non-empty array.', 'woocommerce' ), + 404 + ); } require_once ABSPATH . 'wp-admin/includes/plugin.php'; @@ -310,12 +380,13 @@ class PluginsHelper { * Filter the list of plugins to activate. * * @param array $plugins A list of the plugins to activate. + * * @since 6.4.0 */ $plugins = apply_filters( 'woocommerce_admin_plugins_pre_activate', $plugins ); $plugin_paths = self::get_installed_plugins_paths(); - $errors = new \WP_Error(); + $errors = new WP_Error(); $activated_plugins = array(); foreach ( $plugins as $plugin ) { @@ -337,7 +408,8 @@ class PluginsHelper { * Action triggered when a plugin activation fails. * * @param string $slug The plugin slug. - * @param null|\WP_Error $result The result of the plugin activation. + * @param null|WP_Error $result The result of the plugin activation. + * * @since 6.4.0 */ do_action( 'woocommerce_plugins_activate_error', $slug, $result ); @@ -366,15 +438,24 @@ class PluginsHelper { * Schedule plugin activation. * * @param array $plugins Plugins to activate. + * * @return string Job ID. */ public static function schedule_activate_plugins( $plugins ) { if ( empty( $plugins ) || ! is_array( $plugins ) ) { - return new \WP_Error( 'woocommerce_plugins_invalid_plugins', __( 'Plugins must be a non-empty array.', 'woocommerce' ), 404 ); + return new WP_Error( + 'woocommerce_plugins_invalid_plugins', + __( 'Plugins must be a non-empty array.', 'woocommerce' ), + 404 + ); } $job_id = uniqid(); - WC()->queue()->schedule_single( time() + 5, 'woocommerce_plugins_activate_callback', array( $plugins, $job_id ) ); + WC()->queue()->schedule_single( + time() + 5, + 'woocommerce_plugins_activate_callback', + array( $plugins, $job_id ) + ); return $job_id; } @@ -383,6 +464,7 @@ class PluginsHelper { * Installation status. * * @param int $job_id Job ID. + * * @return array Job data. */ public static function get_installation_status( $job_id = null ) { @@ -402,14 +484,14 @@ class PluginsHelper { * Gets the plugin data for the first action. * * @param array $actions Array of AS actions. + * * @return array Array of action data. */ public static function get_action_data( $actions ) { - $data = []; + $data = array(); foreach ( $actions as $action_id => $action ) { - $store = new \ActionScheduler_DBStore(); - $status = $store->get_status( $action_id ); + $store = new ActionScheduler_DBStore(); $args = $action->get_args(); $data[] = array( 'job_id' => $args[1], @@ -425,6 +507,7 @@ class PluginsHelper { * Activation status. * * @param int $job_id Job ID. + * * @return array Array of action data. */ public static function get_activation_status( $job_id = null ) { diff --git a/plugins/woocommerce/src/Admin/PluginsInstallLoggers/AsynPluginsInstallLogger.php b/plugins/woocommerce/src/Admin/PluginsInstallLoggers/AsynPluginsInstallLogger.php new file mode 100644 index 00000000000..306aeccf898 --- /dev/null +++ b/plugins/woocommerce/src/Admin/PluginsInstallLoggers/AsynPluginsInstallLogger.php @@ -0,0 +1,134 @@ +option_name = $option_name; + add_option( + $this->option_name, + array( + 'created_time' => time(), + 'status' => 'pending', + 'plugins' => array(), + ), + '', + 'no' + ); + + // Set status as failed in case we run out of exectuion time. + register_shutdown_function( + function () { + $error = error_get_last(); + if ( isset( $error['type'] ) && E_ERROR === $error['type'] ) { + $option = $this->get(); + $option['status'] = 'failed'; + $this->update( $option ); + } + } + ); + } + + /** + * Update the option. + * + * @param array $data New data. + * + * @return bool + */ + private function update( array $data ) { + return update_option( $this->option_name, $data ); + } + + /** + * Retreive the option. + * + * @return false|mixed|void + */ + private function get() { + return get_option( $this->option_name ); + } + + /** + * Add requested plugin. + * + * @param string $plugin_name plugin name. + * + * @return void + */ + public function install_requested( string $plugin_name ) { + $option = $this->get(); + if ( ! isset( $option['plugins'][ $plugin_name ] ) ) { + $option['plugins'][ $plugin_name ] = array( + 'status' => 'installing', + 'errors' => array(), + 'install_duration' => 0, + ); + } + $this->update( $option ); + } + + /** + * Add installed plugin. + * + * @param string $plugin_name plugin name. + * @param int $duration time took to install plugin. + * + * @return void + */ + public function installed( string $plugin_name, int $duration ) { + $option = $this->get(); + + $option['plugins'][ $plugin_name ]['status'] = 'installed'; + $option['plugins'][ $plugin_name ]['install_duration'] = $duration; + $this->update( $option ); + } + + /** + * Add an error. + * + * @param string $plugin_name plugin name. + * @param string|null $error_message error message. + * + * @return void + */ + public function add_error( string $plugin_name, string $error_message = null ) { + $option = $this->get(); + + $option['plugins'][ $plugin_name ]['errors'][] = $error_message; + $option['plugins'][ $plugin_name ]['status'] = 'failed'; + $option['status'] = 'failed'; + + $this->update( $option ); + } + + /** + * Record completed_time. + * + * @return void + */ + public function complete() { + $option = $this->get(); + + $option['complete_time'] = time(); + $option['status'] = 'complete'; + + $this->update( $option ); + } +} diff --git a/plugins/woocommerce/src/Admin/PluginsInstallLoggers/PluginsInstallLogger.php b/plugins/woocommerce/src/Admin/PluginsInstallLoggers/PluginsInstallLogger.php new file mode 100644 index 00000000000..0ef3ddc4655 --- /dev/null +++ b/plugins/woocommerce/src/Admin/PluginsInstallLoggers/PluginsInstallLogger.php @@ -0,0 +1,43 @@ +useAdmin(); + } + + /** + * Use a user with administrator role. + * + * @return void + */ + public function useAdmin() { + // Register an administrator user and log in. + $this->user = $this->factory->user->create( + array( + 'role' => 'administrator', + ) + ); + wp_set_current_user( $this->user ); + } + + /** + * Use a user without any permissions. + * + * @return void + */ + public function useUserWithoutPluginsPermission() { + $this->user = $this->factory->user->create(); + wp_set_current_user( $this->user ); + } + + /** + * Request to install-async endpoint. + * + * @param string $endpoint Request endpoint. + * @param string $body Request body. + * + * @return mixed + */ + private function request( $endpoint, $body ) { + $request = new WP_REST_Request( 'POST', self::ENDPOINT . $endpoint ); + $request->set_header( 'content-type', 'application/json' ); + $request->set_body( $body ); + $response = $this->server->dispatch( $request ); + + return $response; + } + + /** + * Request to scheduled-installs endpoint. + * + * @param string $job_id job id. + * + * @return mixed + */ + private function get( $job_id ) { + $request = new WP_REST_Request( 'GET', self::ENDPOINT . '/scheduled-installs/' . $job_id ); + + return $this->server->dispatch( $request )->get_data(); + } + + /** + * Test to confirm install-async response format. + * + * @return void + */ + public function test_response_format() { + $data = $this->request( + '/install-async', + wp_json_encode( + array( + 'plugins' => array( 'test' ), + ) + ) + )->get_data(); + $this->assertArrayHasKey( 'job_id', $data ); + $this->assertArrayHasKey( 'status', $data ); + $this->assertArrayHasKey( 'plugins', $data ); + $this->assertTrue( isset( $data['plugins']['test'] ) ); + } + + /** + * Test to confirm it queues an action scheduler job. + * + * @return void + */ + public function test_it_queues_action() { + $this->markTestSkipped( 'Skipping it for now until we find a better way of testing it.' ); + $data = $this->request( + '/install-async', + wp_json_encode( + array( + 'plugins' => array( 'test' ), + ) + ) + )->get_data(); + $action_id = $data['job_id']; + $data = $this->get( $action_id ); + $this->assertIsArray( $data ); + $this->assertEquals( $action_id, $data['job_id'] ); + } + + /** + * Test it returns 404 when an unknown job id is given. + * + * @return void + */ + public function test_it_returns_404_with_unknown_job_id() { + $request = new WP_REST_Request( 'GET', self::ENDPOINT . '/scheduled-installs/i-do-not-exist' ); + $response = $this->server->dispatch( $request ); + $this->assertEquals( 404, $response->get_status() ); + } + + /** + * Test permissions. + * + * @return void + */ + public function test_permissions() { + $this->useUserWithoutPluginsPermission(); + foreach ( array( '/install-and-activate', '/install-async' ) as $endpoint ) { + $response = $this->request( + $endpoint, + wp_json_encode( + array( + 'plugins' => array( 'test' ), + ) + ) + ); + $this->assertEquals( 403, $response->get_status() ); + } + } +}