diff --git a/plugins/woocommerce-admin/changelogs/update-as-plugin-install b/plugins/woocommerce-admin/changelogs/update-as-plugin-install new file mode 100644 index 00000000000..9bb35a167c3 --- /dev/null +++ b/plugins/woocommerce-admin/changelogs/update-as-plugin-install @@ -0,0 +1,4 @@ +Significance: minor +Type: Add + +Add asynchronous plugin install and activation endpoints #8079 diff --git a/plugins/woocommerce-admin/src/API/Plugins.php b/plugins/woocommerce-admin/src/API/Plugins.php index 7e77396a59a..65541c2217a 100644 --- a/plugins/woocommerce-admin/src/API/Plugins.php +++ b/plugins/woocommerce-admin/src/API/Plugins.php @@ -51,6 +51,32 @@ class Plugins extends \WC_REST_Data_Controller { ) ); + register_rest_route( + $this->namespace, + '/' . $this->rest_base . '/install/status', + array( + array( + 'methods' => \WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_installation_status' ), + 'permission_callback' => array( $this, 'update_item_permissions_check' ), + ), + 'schema' => array( $this, 'get_item_schema' ), + ) + ); + + register_rest_route( + $this->namespace, + '/' . $this->rest_base . '/install/status/(?P[a-z0-9_\-]+)', + array( + array( + 'methods' => \WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_job_installation_status' ), + 'permission_callback' => array( $this, 'update_item_permissions_check' ), + ), + 'schema' => array( $this, 'get_item_schema' ), + ) + ); + register_rest_route( $this->namespace, '/' . $this->rest_base . '/active', @@ -90,6 +116,32 @@ class Plugins extends \WC_REST_Data_Controller { ) ); + register_rest_route( + $this->namespace, + '/' . $this->rest_base . '/activate/status', + array( + array( + 'methods' => \WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_activation_status' ), + 'permission_callback' => array( $this, 'update_item_permissions_check' ), + ), + 'schema' => array( $this, 'get_item_schema' ), + ) + ); + + register_rest_route( + $this->namespace, + '/' . $this->rest_base . '/activate/status/(?P[a-z0-9_\-]+)', + array( + array( + 'methods' => \WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_job_activation_status' ), + 'permission_callback' => array( $this, 'update_item_permissions_check' ), + ), + 'schema' => array( $this, 'get_item_schema' ), + ) + ); + register_rest_route( $this->namespace, '/' . $this->rest_base . '/recommended-payment-plugins', @@ -195,22 +247,6 @@ class Plugins extends \WC_REST_Data_Controller { return true; } - /** - * Create an alert notification in response to an error installing a plugin. - * - * @todo This should be moved to a filter to make this API more generic and less plugin-specific. - * - * @param string $slug The slug of the plugin being installed. - */ - private function create_install_plugin_error_inbox_notification_for_jetpack_installs( $slug ) { - // Exit early if we're not installing the Jetpack or the WooCommerce Shipping & Tax plugins. - if ( 'jetpack' !== $slug && 'woocommerce-services' !== $slug ) { - return; - } - - InstallJPAndWCSPlugins::possibly_add_note(); - } - /** * Install the requested plugin. * @@ -233,119 +269,60 @@ class Plugins extends \WC_REST_Data_Controller { public function install_plugins( $request ) { $plugins = explode( ',', $request['plugins'] ); - /** - * Filter the list of plugins to install. - * - * @param array $plugins A list of the plugins to install. - */ - $plugins = apply_filters( 'woocommerce_admin_plugins_pre_install', $plugins ); - if ( empty( $request['plugins'] ) || ! is_array( $plugins ) ) { return new \WP_Error( 'woocommerce_rest_invalid_plugins', __( 'Plugins must be a non-empty array.', 'woocommerce-admin' ), 404 ); } - require_once ABSPATH . 'wp-admin/includes/plugin.php'; - include_once ABSPATH . '/wp-admin/includes/admin.php'; - include_once ABSPATH . '/wp-admin/includes/plugin-install.php'; - include_once ABSPATH . '/wp-admin/includes/plugin.php'; - include_once ABSPATH . '/wp-admin/includes/class-wp-upgrader.php'; - include_once ABSPATH . '/wp-admin/includes/class-plugin-upgrader.php'; + if ( isset( $request['async'] ) && $request['async'] ) { + $job_id = PluginsHelper::schedule_install_plugins( $plugins ); - $existing_plugins = PluginsHelper::get_installed_plugins_paths(); - $installed_plugins = array(); - $results = array(); - $install_time = array(); - $errors = new \WP_Error(); - - foreach ( $plugins as $plugin ) { - $slug = sanitize_key( $plugin ); - - if ( isset( $existing_plugins[ $slug ] ) ) { - $installed_plugins[] = $plugin; - continue; - } - - $start_time = microtime( true ); - - $api = plugins_api( - 'plugin_information', - array( - 'slug' => $slug, - 'fields' => array( - 'sections' => false, - ), - ) + return array( + 'data' => array( + 'job_id' => $job_id, + 'plugins' => $plugins, + ), + 'message' => __( 'Plugin installation has been scheduled.', 'woocommerce-admin' ), ); - - if ( is_wp_error( $api ) ) { - $properties = array( - /* translators: %s: plugin slug (example: woocommerce-services) */ - 'error_message' => __( 'The requested plugin `%s` could not be installed. Plugin API call failed.', 'woocommerce-admin' ), - 'api' => $api, - 'slug' => $slug, - ); - wc_admin_record_tracks_event( 'install_plugin_error', $properties ); - - $this->create_install_plugin_error_inbox_notification_for_jetpack_installs( $slug ); - - $errors->add( - $plugin, - sprintf( - /* translators: %s: plugin slug (example: woocommerce-services) */ - __( 'The requested plugin `%s` could not be installed. Plugin API call failed.', 'woocommerce-admin' ), - $slug - ) - ); - - continue; - } - - $upgrader = new \Plugin_Upgrader( new \Automatic_Upgrader_Skin() ); - $result = $upgrader->install( $api->download_link ); - $results[ $plugin ] = $result; - $install_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' => __( 'The requested plugin `%s` could not be installed.', 'woocommerce-admin' ), - 'slug' => $slug, - 'api' => $api, - 'upgrader' => $upgrader, - 'result' => $result, - ); - wc_admin_record_tracks_event( 'install_plugin_error', $properties ); - - $this->create_install_plugin_error_inbox_notification_for_jetpack_installs( $slug ); - - $errors->add( - $plugin, - sprintf( - /* translators: %s: plugin slug (example: woocommerce-services) */ - __( 'The requested plugin `%s` could not be installed. Upgrader install failed.', 'woocommerce-admin' ), - $slug - ) - ); - continue; - } - - $installed_plugins[] = $plugin; } + $data = PluginsHelper::install_plugins( $plugins ); + return array( 'data' => array( - 'installed' => $installed_plugins, - 'results' => $results, - 'install_time' => $install_time, + 'installed' => $data['installed'], + 'results' => $data['results'], + 'install_time' => $data['time'], ), - 'errors' => $errors, - 'success' => count( $errors->errors ) === 0, - 'message' => count( $errors->errors ) === 0 + 'errors' => $data['errors'], + 'success' => count( $data['errors']->errors ) === 0, + 'message' => count( $data['errors']->errors ) === 0 ? __( 'Plugins were successfully installed.', 'woocommerce-admin' ) : __( 'There was a problem installing some of the requested plugins.', 'woocommerce-admin' ), ); } + /** + * Returns a list of recently scheduled installation jobs. + * + * @param WP_REST_Request $request Full details about the request. + * @return array Jobs. + */ + public function get_installation_status( $request ) { + return PluginsHelper::get_installation_status(); + } + + /** + * Returns a list of recently scheduled installation jobs. + * + * @param WP_REST_Request $request Full details about the request. + * @return array Job. + */ + public function get_job_installation_status( $request ) { + $job_id = $request->get_param( 'job_id' ); + $jobs = PluginsHelper::get_installation_status( $job_id ); + return reset( $jobs ); + } + /** * Returns a list of active plugins in API format. * @@ -384,68 +361,61 @@ class Plugins extends \WC_REST_Data_Controller { * @return WP_Error|array Plugin Status */ public function activate_plugins( $request ) { - $plugin_paths = PluginsHelper::get_installed_plugins_paths(); - $plugins = explode( ',', $request['plugins'] ); - $errors = new \WP_Error(); - $activated_plugins = array(); + $plugins = explode( ',', $request['plugins'] ); if ( empty( $request['plugins'] ) || ! is_array( $plugins ) ) { return new \WP_Error( 'woocommerce_rest_invalid_plugins', __( 'Plugins must be a non-empty array.', 'woocommerce-admin' ), 404 ); } - require_once ABSPATH . 'wp-admin/includes/plugin.php'; + if ( isset( $request['async'] ) && $request['async'] ) { + $job_id = PluginsHelper::schedule_activate_plugins( $plugins ); - // the mollie-payments-for-woocommerce plugin calls `WP_Filesystem()` during it's activation hook, which crashes without this include. - require_once ABSPATH . 'wp-admin/includes/file.php'; - - /** - * Filter the list of plugins to activate. - * - * @param array $plugins A list of the plugins to activate. - */ - $plugins = apply_filters( 'woocommerce_admin_plugins_pre_activate', $plugins ); - - foreach ( $plugins as $plugin ) { - $slug = $plugin; - $path = isset( $plugin_paths[ $slug ] ) ? $plugin_paths[ $slug ] : false; - - if ( ! $path ) { - $errors->add( - $plugin, - /* translators: %s: plugin slug (example: woocommerce-services) */ - sprintf( __( 'The requested plugin `%s`. is not yet installed.', 'woocommerce-admin' ), $slug ) - ); - continue; - } - - $result = activate_plugin( $path ); - if ( ! is_null( $result ) ) { - $this->create_install_plugin_error_inbox_notification_for_jetpack_installs( $slug ); - - $errors->add( - $plugin, - /* translators: %s: plugin slug (example: woocommerce-services) */ - sprintf( __( 'The requested plugin `%s` could not be activated.', 'woocommerce-admin' ), $slug ) - ); - continue; - } - - $activated_plugins[] = $plugin; + return array( + 'data' => array( + 'job_id' => $job_id, + 'plugins' => $plugins, + ), + 'message' => __( 'Plugin activation has been scheduled.', 'woocommerce-admin' ), + ); } + $data = PluginsHelper::activate_plugins( $plugins ); + return( array( 'data' => array( - 'activated' => $activated_plugins, - 'active' => self::get_active_plugins(), + 'activated' => $data['activated'], + 'active' => $data['active'], ), - 'errors' => $errors, - 'success' => count( $errors->errors ) === 0, - 'message' => count( $errors->errors ) === 0 + 'errors' => $data['errors'], + 'success' => count( $data['errors']->errors ) === 0, + 'message' => count( $data['errors']->errors ) === 0 ? __( 'Plugins were successfully activated.', 'woocommerce-admin' ) : __( 'There was a problem activating some of the requested plugins.', 'woocommerce-admin' ), ) ); } + /** + * Returns a list of recently scheduled activation jobs. + * + * @param WP_REST_Request $request Full details about the request. + * @return array Job. + */ + public function get_activation_status( $request ) { + return PluginsHelper::get_activation_status(); + } + + /** + * Returns a list of recently scheduled activation jobs. + * + * @param WP_REST_Request $request Full details about the request. + * @return array Jobs. + */ + public function get_job_activation_status( $request ) { + $job_id = $request->get_param( 'job_id' ); + $jobs = PluginsHelper::get_activation_status( $job_id ); + return reset( $jobs ); + } + /** * Return recommended payment plugins. * diff --git a/plugins/woocommerce-admin/src/FeaturePlugin.php b/plugins/woocommerce-admin/src/FeaturePlugin.php index 112c4a7ecfd..e18455feebb 100644 --- a/plugins/woocommerce-admin/src/FeaturePlugin.php +++ b/plugins/woocommerce-admin/src/FeaturePlugin.php @@ -167,6 +167,7 @@ class FeaturePlugin { // Initialize Plugins Installer. PluginsInstaller::init(); + PluginsHelper::init(); // Initialize API. API\Init::instance(); diff --git a/plugins/woocommerce-admin/src/Notes/InstallJPAndWCSPlugins.php b/plugins/woocommerce-admin/src/Notes/InstallJPAndWCSPlugins.php index ed25910b215..44ec762306c 100644 --- a/plugins/woocommerce-admin/src/Notes/InstallJPAndWCSPlugins.php +++ b/plugins/woocommerce-admin/src/Notes/InstallJPAndWCSPlugins.php @@ -33,6 +33,9 @@ class InstallJPAndWCSPlugins { public function __construct() { add_action( 'woocommerce_note_action_install-jp-and-wcs-plugins', array( $this, 'install_jp_and_wcs_plugins' ) ); add_action( 'activated_plugin', array( $this, 'action_note' ) ); + add_action( 'woocommerce_plugins_install_api_error', array( $this, 'on_install_error' ) ); + add_action( 'woocommerce_plugins_install_error', array( $this, 'on_install_error' ) ); + add_action( 'woocommerce_plugins_activate_error', array( $this, 'on_install_error' ) ); } /** @@ -123,4 +126,18 @@ class InstallJPAndWCSPlugins { $installer->activate_plugins( $activate_request ); } + + /** + * Create an alert notification in response to an error installing a plugin. + * + * @param string $slug The slug of the plugin being installed. + */ + public function on_install_error( $slug ) { + // Exit early if we're not installing the Jetpack or the WooCommerce Shipping & Tax plugins. + if ( 'jetpack' !== $slug && 'woocommerce-services' !== $slug ) { + return; + } + + self::possibly_add_note(); + } } diff --git a/plugins/woocommerce-admin/src/PluginsHelper.php b/plugins/woocommerce-admin/src/PluginsHelper.php index 7ce11b34dc7..8c44e8959b2 100644 --- a/plugins/woocommerce-admin/src/PluginsHelper.php +++ b/plugins/woocommerce-admin/src/PluginsHelper.php @@ -18,6 +18,14 @@ if ( ! function_exists( 'get_plugins' ) ) { */ class PluginsHelper { + /** + * Initialize hooks. + */ + public static function init() { + add_action( 'woocommerce_plugins_install_callback', array( __CLASS__, 'install_plugins' ), 10, 2 ); + add_action( 'woocommerce_plugins_activate_callback', array( __CLASS__, 'activate_plugins' ), 10, 2 ); + } + /** * Get the path to the plugin file relative to the plugins directory from the plugin slug. * @@ -131,4 +139,279 @@ class PluginsHelper { return isset( $plugins[ $plugin_path ] ) ? $plugins[ $plugin_path ] : false; } + /** + * Install an array of plugins. + * + * @param array $plugins Plugins to install. + * @return array + */ + public static function install_plugins( $plugins ) { + /** + * Filter the list of plugins to install. + * + * @param array $plugins A list of the plugins to install. + */ + $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-admin' ) ); + } + + require_once ABSPATH . 'wp-admin/includes/plugin.php'; + include_once ABSPATH . '/wp-admin/includes/admin.php'; + include_once ABSPATH . '/wp-admin/includes/plugin-install.php'; + include_once ABSPATH . '/wp-admin/includes/plugin.php'; + include_once ABSPATH . '/wp-admin/includes/class-wp-upgrader.php'; + include_once ABSPATH . '/wp-admin/includes/class-plugin-upgrader.php'; + + $existing_plugins = self::get_installed_plugins_paths(); + $installed_plugins = array(); + $results = array(); + $time = array(); + $errors = new \WP_Error(); + + foreach ( $plugins as $plugin ) { + $slug = sanitize_key( $plugin ); + + if ( isset( $existing_plugins[ $slug ] ) ) { + $installed_plugins[] = $plugin; + continue; + } + + $start_time = microtime( true ); + + $api = plugins_api( + 'plugin_information', + array( + 'slug' => $slug, + 'fields' => array( + 'sections' => false, + ), + ) + ); + + if ( is_wp_error( $api ) ) { + $properties = array( + /* translators: %s: plugin slug (example: woocommerce-services) */ + 'error_message' => __( 'The requested plugin `%s` could not be installed. Plugin API call failed.', 'woocommerce-admin' ), + 'api' => $api, + 'slug' => $slug, + ); + wc_admin_record_tracks_event( 'install_plugin_error', $properties ); + + 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-admin' ), + $slug + ) + ); + + continue; + } + + $upgrader = new \Plugin_Upgrader( new \Automatic_Upgrader_Skin() ); + $result = $upgrader->install( $api->download_link ); + $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' => __( 'The requested plugin `%s` could not be installed.', 'woocommerce-admin' ), + 'slug' => $slug, + 'api' => $api, + 'upgrader' => $upgrader, + 'result' => $result, + ); + wc_admin_record_tracks_event( 'install_plugin_error', $properties ); + + do_action( 'woocommerce_plugins_install_error', $slug, $api, $result, $upgrader ); + + $errors->add( + $plugin, + sprintf( + /* translators: %s: plugin slug (example: woocommerce-services) */ + __( 'The requested plugin `%s` could not be installed. Upgrader install failed.', 'woocommerce-admin' ), + $slug + ) + ); + continue; + } + + $installed_plugins[] = $plugin; + } + + $data = array( + 'installed' => $installed_plugins, + 'results' => $results, + 'errors' => $errors, + 'time' => $time, + ); + + return $data; + } + + /** + * 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-admin' ), 404 ); + } + + $job_id = uniqid(); + WC()->queue()->schedule_single( time() + 5, 'woocommerce_plugins_install_callback', array( $plugins, $job_id ) ); + + return $job_id; + } + + /** + * 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-admin' ), 404 ); + } + + require_once ABSPATH . 'wp-admin/includes/plugin.php'; + + // the mollie-payments-for-woocommerce plugin calls `WP_Filesystem()` during it's activation hook, which crashes without this include. + require_once ABSPATH . 'wp-admin/includes/file.php'; + + /** + * Filter the list of plugins to activate. + * + * @param array $plugins A list of the plugins to activate. + */ + $plugins = apply_filters( 'woocommerce_admin_plugins_pre_activate', $plugins ); + + $plugin_paths = self::get_installed_plugins_paths(); + $errors = new \WP_Error(); + $activated_plugins = array(); + + foreach ( $plugins as $plugin ) { + $slug = $plugin; + $path = isset( $plugin_paths[ $slug ] ) ? $plugin_paths[ $slug ] : false; + + if ( ! $path ) { + $errors->add( + $plugin, + /* translators: %s: plugin slug (example: woocommerce-services) */ + sprintf( __( 'The requested plugin `%s`. is not yet installed.', 'woocommerce-admin' ), $slug ) + ); + continue; + } + + $result = activate_plugin( $path ); + if ( ! is_null( $result ) ) { + do_action( 'woocommerce_plugins_activate_error', $slug, $result ); + + $errors->add( + $plugin, + /* translators: %s: plugin slug (example: woocommerce-services) */ + sprintf( __( 'The requested plugin `%s` could not be activated.', 'woocommerce-admin' ), $slug ) + ); + continue; + } + + $activated_plugins[] = $plugin; + } + + $data = array( + 'activated' => $activated_plugins, + 'active' => self::get_active_plugin_slugs(), + 'errors' => $errors, + ); + + return $data; + } + + /** + * 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-admin' ), 404 ); + } + + $job_id = uniqid(); + WC()->queue()->schedule_single( time() + 5, 'woocommerce_plugins_activate_callback', array( $plugins, $job_id ) ); + + return $job_id; + } + + /** + * Installation status. + * + * @param int $job_id Job ID. + * @return array Job data. + */ + public static function get_installation_status( $job_id = null ) { + $actions = WC()->queue()->search( + array( + 'hook' => 'woocommerce_plugins_install_callback', + 'search' => $job_id, + 'orderby' => 'date', + 'order' => 'DESC', + ) + ); + + return self::get_action_data( $actions ); + } + + /** + * 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 = []; + + foreach ( $actions as $action_id => $action ) { + $store = new \ActionScheduler_DBStore(); + $status = $store->get_status( $action_id ); + $args = $action->get_args(); + $data[] = array( + 'job_id' => $args[1], + 'plugins' => $args[0], + 'status' => $store->get_status( $action_id ), + ); + } + + return $data; + } + + /** + * Activation status. + * + * @param int $job_id Job ID. + * @return array Array of action data. + */ + public static function get_activation_status( $job_id = null ) { + $actions = WC()->queue()->search( + array( + 'hook' => 'woocommerce_plugins_activate_callback', + 'search' => $job_id, + 'orderby' => 'date', + 'order' => 'DESC', + ) + ); + + return self::get_action_data( $actions ); + } + } diff --git a/plugins/woocommerce-admin/tests/api/plugins.php b/plugins/woocommerce-admin/tests/api/plugins.php index c0b28d209a9..cc443599858 100644 --- a/plugins/woocommerce-admin/tests/api/plugins.php +++ b/plugins/woocommerce-admin/tests/api/plugins.php @@ -63,6 +63,27 @@ class WC_Tests_API_Plugins extends WC_REST_Unit_Test_Case { $this->assertArrayHasKey( 'facebook-for-woocommerce/facebook-for-woocommerce.php', $plugins ); } + /** + * Test that scheduling a plugin install works. + */ + public function test_install_plugin_async() { + wp_set_current_user( $this->user ); + + $request = new WP_REST_Request( 'POST', $this->endpoint . '/install' ); + $request->set_query_params( + array( + 'async' => true, + 'plugins' => 'facebook-for-woocommerce', + ) + ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + $plugins = get_plugins(); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertArrayHasKey( 'job_id', $data['data'] ); + } + /** * Test that installing with invalid params fails. */ @@ -98,6 +119,27 @@ class WC_Tests_API_Plugins extends WC_REST_Unit_Test_Case { $this->assertContains( 'facebook-for-woocommerce', $active_plugins ); } + /** + * Test that scheduling a plugin activation works. + */ + public function test_activate_plugin_async() { + wp_set_current_user( $this->user ); + + $request = new WP_REST_Request( 'POST', $this->endpoint . '/activate' ); + $request->set_query_params( + array( + 'async' => true, + 'plugins' => 'facebook-for-woocommerce', + ) + ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + $plugins = get_plugins(); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertArrayHasKey( 'job_id', $data['data'] ); + } + /** * Test that activating with invalid params fails. */