|WP_Error $response REST API response.
+ * @return array{label: string, status: string, badge: array{label: string, color: string}, description: string, actions: string, test: string} Result.
+ */
+function od_compose_site_health_result( $response ): array {
+ $common_description_html = '' . wp_kses(
+ sprintf(
+ /* translators: %s is the REST API endpoint */
+ __( 'To collect URL Metrics from visitors the REST API must be available to unauthenticated users. Specifically, visitors must be able to perform a POST
request to the %s
endpoint.', 'optimization-detective' ),
+ '/' . OD_REST_API_NAMESPACE . OD_URL_METRICS_ROUTE
+ ),
+ array( 'code' => array() )
+ ) . '
';
+
+ $result = array(
+ 'label' => __( 'The Optimization Detective REST API endpoint is available', 'optimization-detective' ),
+ 'status' => 'good',
+ 'badge' => array(
+ 'label' => __( 'Optimization Detective', 'optimization-detective' ),
+ 'color' => 'blue',
+ ),
+ 'description' => $common_description_html . '' . esc_html__( 'This appears to be working properly.', 'optimization-detective' ) . '
',
+ 'actions' => '',
+ 'test' => 'optimization_detective_rest_api',
+ );
+
+ $error_label = __( 'The Optimization Detective REST API endpoint is unavailable', 'optimization-detective' );
+ $error_description_html = '' . esc_html__( 'You may have a plugin active or server configuration which restricts access to logged-in users. Unauthenticated access must be restored in order for Optimization Detective to work.', 'optimization-detective' ) . '
';
+
+ if ( is_wp_error( $response ) ) {
+ $result['status'] = 'recommended';
+ $result['label'] = $error_label;
+ $result['description'] = $common_description_html . $error_description_html . '' . wp_kses(
+ sprintf(
+ /* translators: %s is the error code */
+ __( 'The REST API responded with the error code %s
and the following error message:', 'optimization-detective' ),
+ esc_html( (string) $response->get_error_code() )
+ ),
+ array( 'code' => array() )
+ ) . '
' . esc_html( $response->get_error_message() ) . '
';
+ } else {
+ $code = wp_remote_retrieve_response_code( $response );
+ $message = wp_remote_retrieve_response_message( $response );
+ $body = wp_remote_retrieve_body( $response );
+ $data = json_decode( $body, true );
+
+ $is_expected = (
+ 400 === $code &&
+ isset( $data['code'], $data['data']['params'] ) &&
+ 'rest_missing_callback_param' === $data['code'] &&
+ is_array( $data['data']['params'] ) &&
+ count( $data['data']['params'] ) > 0
+ );
+ if ( ! $is_expected ) {
+ $result['status'] = 'recommended';
+ if ( 401 === $code ) {
+ $result['label'] = __( 'The Optimization Detective REST API endpoint is unavailable to logged-out users', 'optimization-detective' );
+ } else {
+ $result['label'] = $error_label;
+ }
+ $result['description'] = $common_description_html . $error_description_html . '' . wp_kses(
+ sprintf(
+ /* translators: %d is the HTTP status code, %s is the status header description */
+ __( 'The REST API returned with an HTTP status of %1$d %2$s
.', 'optimization-detective' ),
+ $code,
+ esc_html( $message )
+ ),
+ array( 'code' => array() )
+ ) . '
';
+
+ if ( isset( $data['message'] ) && is_string( $data['message'] ) ) {
+ $result['description'] .= '' . esc_html( $data['message'] ) . '
';
+ }
+
+ $result['description'] .= '' . esc_html__( 'Raw response:', 'optimization-detective' ) . '
' . esc_html( $body ) . '
';
+ }
+ }
+ return $result;
+}
+
+/**
+ * Gets the response to an Optimization Detective REST API store request to confirm it is available to unauthenticated requests.
+ *
+ * @since n.e.x.t
+ * @access private
+ *
+ * @param bool $use_cached Whether to use a previous response cached in a transient.
+ * @return array{ response: array{ code: int, message: string }, body: string }|WP_Error Response.
+ */
+function od_get_rest_api_health_check_response( bool $use_cached ) {
+ $transient_key = 'od_rest_api_health_check_response';
+ $response = $use_cached ? get_transient( $transient_key ) : false;
+ if ( false !== $response ) {
+ return $response;
+ }
+ $rest_url = get_rest_url( null, OD_REST_API_NAMESPACE . OD_URL_METRICS_ROUTE );
+ $response = wp_remote_post(
+ $rest_url,
+ array(
+ 'headers' => array( 'Content-Type' => 'application/json' ),
+ 'sslverify' => false,
+ )
+ );
+
+ // This transient will be used when showing the admin notice with the plugin on the plugins screen.
+ // The 1-day expiration allows for fresher content than the weekly check initiated by Site Health.
+ set_transient( $transient_key, $response, DAY_IN_SECONDS );
+ return $response;
+}
+
+/**
+ * Renders an admin notice if the REST API health check fails.
+ *
+ * @since n.e.x.t
+ * @access private
+ *
+ * @param bool $in_plugin_row Whether the notice is to be printed in the plugin row.
+ */
+function od_maybe_render_rest_api_health_check_admin_notice( bool $in_plugin_row ): void {
+ if ( ! od_is_rest_api_unavailable() ) {
+ return;
+ }
+
+ $response = od_get_rest_api_health_check_response( true );
+ $result = od_compose_site_health_result( $response );
+ if ( 'good' === $result['status'] ) {
+ // There's a slight chance the DB option is stale in the initial if statement.
+ return;
+ }
+
+ $message = sprintf(
+ $in_plugin_row
+ ? '%s %s'
+ : '%s %s
',
+ esc_html__( 'Warning:', 'optimization-detective' ),
+ esc_html( $result['label'] )
+ );
+
+ $message .= $result['description']; // This has already gone through Kses.
+
+ if ( current_user_can( 'view_site_health_checks' ) ) {
+ $site_health_message = wp_kses(
+ sprintf(
+ /* translators: %s is the URL to the Site Health admin screen */
+ __( 'Please visit Site Health to re-check this once you believe you have resolved the issue.', 'optimization-detective' ),
+ esc_url( admin_url( 'site-health.php' ) )
+ ),
+ array( 'a' => array( 'href' => array() ) )
+ );
+ $message .= "$site_health_message
";
+ }
+
+ if ( $in_plugin_row ) {
+ $message = "$message ";
+ }
+
+ wp_admin_notice(
+ $message,
+ array(
+ 'type' => 'warning',
+ 'additional_classes' => $in_plugin_row ? array( 'inline', 'notice-alt' ) : array(),
+ 'paragraph_wrap' => false,
+ )
+ );
+}
+
+/**
+ * Displays an admin notice on the plugin row if the REST API health check fails.
+ *
+ * @since n.e.x.t
+ * @access private
+ *
+ * @param string $plugin_file Plugin file.
+ */
+function od_render_rest_api_health_check_admin_notice_in_plugin_row( string $plugin_file ): void {
+ if ( 'optimization-detective/load.php' !== $plugin_file ) { // TODO: What if a different plugin slug is used?
+ return;
+ }
+ od_maybe_render_rest_api_health_check_admin_notice( true );
+}
+
+/**
+ * Runs the REST API health check if it hasn't been run yet.
+ *
+ * This happens at the `admin_init` action to avoid running the check on the frontend. This will run on the first admin
+ * page load after the plugin has been activated. This allows for this function to add an action at `admin_notices` so
+ * that an error message can be displayed after performing that plugin activation request. Note that a plugin activation
+ * hook cannot be used for this purpose due to not being compatible with multisite. While the site health notice is
+ * shown at the `admin_notices` action once, the notice will only be displayed inline with the plugin row thereafter
+ * via {@see od_render_rest_api_health_check_admin_notice_in_plugin_row()}.
+ *
+ * @since n.e.x.t
+ * @access private
+ */
+function od_maybe_run_rest_api_health_check(): void {
+ // If the option already exists, then the REST API health check has already been performed.
+ if ( false !== get_option( 'od_rest_api_unavailable' ) ) {
+ return;
+ }
+
+ // This will populate the od_rest_api_unavailable option so that the function won't execute on the next page load.
+ if ( 'good' !== od_test_rest_api_availability()['status'] ) {
+ // Show any notice in the main admin notices area for the first page load (e.g. after plugin activation).
+ add_action(
+ 'admin_notices',
+ static function (): void {
+ od_maybe_render_rest_api_health_check_admin_notice( false );
+ }
+ );
+ }
+}
diff --git a/plugins/optimization-detective/storage/rest-api.php b/plugins/optimization-detective/storage/rest-api.php
index 09ce02501e..ce278a9f57 100644
--- a/plugins/optimization-detective/storage/rest-api.php
+++ b/plugins/optimization-detective/storage/rest-api.php
@@ -34,6 +34,8 @@
*
* @since 0.1.0
* @access private
+ *
+ * @see od_compose_site_health_result()
*/
function od_register_endpoint(): void {
diff --git a/plugins/optimization-detective/tests/test-helper.php b/plugins/optimization-detective/tests/test-helper.php
index 42bcf62dd8..3d9ae9c5ad 100644
--- a/plugins/optimization-detective/tests/test-helper.php
+++ b/plugins/optimization-detective/tests/test-helper.php
@@ -93,5 +93,22 @@ public function test_od_render_generator_meta_tag(): void {
$this->assertStringStartsWith( 'assertStringContainsString( 'generator', $tag );
$this->assertStringContainsString( 'optimization-detective ' . OPTIMIZATION_DETECTIVE_VERSION, $tag );
+ $this->assertFalse( od_is_rest_api_unavailable() );
+ $this->assertStringNotContainsString( 'rest_api_unavailable', $tag );
+ }
+
+ /**
+ * Test printing the meta generator tag when the REST API is not available.
+ *
+ * @covers ::od_render_generator_meta_tag
+ */
+ public function test_od_render_generator_meta_tag_rest_api_unavailable(): void {
+ update_option( 'od_rest_api_unavailable', '1' );
+ $tag = get_echo( 'od_render_generator_meta_tag' );
+ $this->assertStringStartsWith( 'assertStringContainsString( 'generator', $tag );
+ $this->assertStringContainsString( 'optimization-detective ' . OPTIMIZATION_DETECTIVE_VERSION, $tag );
+ $this->assertTrue( od_is_rest_api_unavailable() );
+ $this->assertStringContainsString( '; rest_api_unavailable', $tag );
}
}
diff --git a/plugins/optimization-detective/tests/test-hooks.php b/plugins/optimization-detective/tests/test-hooks.php
index 7c53ea49d2..aa5d71ffc6 100644
--- a/plugins/optimization-detective/tests/test-hooks.php
+++ b/plugins/optimization-detective/tests/test-hooks.php
@@ -26,5 +26,8 @@ public function test_hooks_added(): void {
)
);
$this->assertEquals( 10, has_action( 'wp_head', 'od_render_generator_meta_tag' ) );
+ $this->assertEquals( 10, has_filter( 'site_status_tests', 'od_add_rest_api_availability_test' ) );
+ $this->assertEquals( 10, has_action( 'admin_init', 'od_maybe_run_rest_api_health_check' ) );
+ $this->assertEquals( 30, has_action( 'after_plugin_row_meta', 'od_render_rest_api_health_check_admin_notice_in_plugin_row' ) );
}
}
diff --git a/plugins/optimization-detective/tests/test-optimization.php b/plugins/optimization-detective/tests/test-optimization.php
index c0247b3508..879221c1a8 100644
--- a/plugins/optimization-detective/tests/test-optimization.php
+++ b/plugins/optimization-detective/tests/test-optimization.php
@@ -145,37 +145,74 @@ function ( $buffer ) use ( $template_start, $template_middle, $template_end, &$f
}
/**
- * Test od_maybe_add_template_output_buffer_filter().
+ * Data provider.
*
- * @covers ::od_maybe_add_template_output_buffer_filter
+ * @return array
*/
- public function test_od_maybe_add_template_output_buffer_filter(): void {
- $this->assertFalse( has_filter( 'od_template_output_buffer' ) );
-
- add_filter( 'od_can_optimize_response', '__return_false', 1 );
- od_maybe_add_template_output_buffer_filter();
- $this->assertFalse( od_can_optimize_response() );
- $this->assertFalse( has_filter( 'od_template_output_buffer' ) );
-
- add_filter( 'od_can_optimize_response', '__return_true', 2 );
- $this->go_to( home_url( '/' ) );
- $this->assertTrue( od_can_optimize_response() );
- od_maybe_add_template_output_buffer_filter();
- $this->assertTrue( has_filter( 'od_template_output_buffer' ) );
+ public function data_provider_test_od_maybe_add_template_output_buffer_filter(): array {
+ return array(
+ 'home_enabled' => array(
+ 'set_up' => static function (): string {
+ return home_url( '/' );
+ },
+ 'expected_has_filter' => true,
+ ),
+ 'home_disabled_by_filter' => array(
+ 'set_up' => static function (): string {
+ add_filter( 'od_can_optimize_response', '__return_false' );
+ return home_url( '/' );
+ },
+ 'expected_has_filter' => false,
+ ),
+ 'search_disabled' => array(
+ 'set_up' => static function (): string {
+ return home_url( '/?s=foo' );
+ },
+ 'expected_has_filter' => false,
+ ),
+ 'search_enabled_by_filter' => array(
+ 'set_up' => static function (): string {
+ add_filter( 'od_can_optimize_response', '__return_true' );
+ return home_url( '/?s=foo' );
+ },
+ 'expected_has_filter' => true,
+ ),
+ 'home_disabled_by_get_param' => array(
+ 'set_up' => static function (): string {
+ return home_url( '/?optimization_detective_disabled=1' );
+ },
+ 'expected_has_filter' => false,
+ ),
+ 'home_disabled_by_rest_api_unavailable' => array(
+ 'set_up' => static function (): string {
+ update_option( 'od_rest_api_unavailable', '1' );
+ return home_url( '/' );
+ },
+ 'expected_has_filter' => false,
+ ),
+ );
}
+
/**
* Test od_maybe_add_template_output_buffer_filter().
*
+ * @dataProvider data_provider_test_od_maybe_add_template_output_buffer_filter
+ *
* @covers ::od_maybe_add_template_output_buffer_filter
+ * @covers ::od_can_optimize_response
+ * @covers ::od_is_rest_api_unavailable
*/
- public function test_od_maybe_add_template_output_buffer_filter_with_query_var_to_disable(): void {
- $this->assertFalse( has_filter( 'od_template_output_buffer' ) );
+ public function test_od_maybe_add_template_output_buffer_filter( Closure $set_up, bool $expected_has_filter ): void {
+ // There needs to be a post so that there is a post in the loop so that od_get_cache_purge_post_id() returns a post ID.
+ // Otherwise, od_can_optimize_response() will return false unless forced by a filter.
+ self::factory()->post->create();
+
+ $url = $set_up();
+ $this->go_to( $url );
+ remove_all_filters( 'od_template_output_buffer' ); // In case go_to() caused them to be added.
- add_filter( 'od_can_optimize_response', '__return_true' );
- $this->go_to( home_url( '/?optimization_detective_disabled=1' ) );
- $this->assertTrue( od_can_optimize_response() );
od_maybe_add_template_output_buffer_filter();
- $this->assertFalse( has_filter( 'od_template_output_buffer' ) );
+ $this->assertSame( $expected_has_filter, has_filter( 'od_template_output_buffer' ) );
}
/**
@@ -188,66 +225,78 @@ public function data_provider_test_od_can_optimize_response(): array {
'home_as_anonymous' => array(
'set_up' => function (): void {
$this->go_to( home_url( '/' ) );
+ $this->assertIsInt( od_get_cache_purge_post_id() );
},
'expected' => true,
),
- 'home_filtered_as_anonymous' => array(
+ 'home_but_no_posts' => array(
'set_up' => function (): void {
+ $posts = get_posts();
+ foreach ( $posts as $post ) {
+ wp_delete_post( $post->ID, true );
+ }
$this->go_to( home_url( '/' ) );
+ $this->assertNull( od_get_cache_purge_post_id() );
+ },
+ 'expected' => false, // This is because od_get_cache_purge_post_id() will return false.
+ ),
+ 'home_filtered_as_anonymous' => array(
+ 'set_up' => static function (): string {
add_filter( 'od_can_optimize_response', '__return_false' );
+ return home_url( '/' );
},
'expected' => false,
),
'singular_as_anonymous' => array(
- 'set_up' => function (): void {
+ 'set_up' => function (): string {
$posts = get_posts();
$this->assertInstanceOf( WP_Post::class, $posts[0] );
- $this->go_to( get_permalink( $posts[0] ) );
+ return get_permalink( $posts[0] );
},
'expected' => true,
),
'search_as_anonymous' => array(
- 'set_up' => function (): void {
+ 'set_up' => static function (): string {
self::factory()->post->create( array( 'post_title' => 'Hello' ) );
- $this->go_to( home_url( '?s=Hello' ) );
+ return home_url( '?s=Hello' );
},
'expected' => false,
),
'home_customizer_preview_as_anonymous' => array(
- 'set_up' => function (): void {
- $this->go_to( home_url( '/' ) );
+ 'set_up' => static function (): string {
global $wp_customize;
require_once ABSPATH . 'wp-includes/class-wp-customize-manager.php';
$wp_customize = new WP_Customize_Manager();
$wp_customize->start_previewing_theme();
+ return home_url( '/' );
},
'expected' => false,
),
'home_post_request_as_anonymous' => array(
- 'set_up' => function (): void {
- $this->go_to( home_url( '/' ) );
+ 'set_up' => static function (): string {
$_SERVER['REQUEST_METHOD'] = 'POST';
+ return home_url( '/' );
},
'expected' => false,
),
'home_as_subscriber' => array(
- 'set_up' => function (): void {
+ 'set_up' => static function (): string {
wp_set_current_user( self::factory()->user->create( array( 'role' => 'subscriber' ) ) );
- $this->go_to( home_url( '/' ) );
+ return home_url( '/' );
},
'expected' => true,
),
'empty_author_page_as_anonymous' => array(
- 'set_up' => function (): void {
+ 'set_up' => static function (): string {
$user_id = self::factory()->user->create( array( 'role' => 'author' ) );
- $this->go_to( get_author_posts_url( $user_id ) );
+ return get_author_posts_url( $user_id );
},
'expected' => false,
),
'home_as_admin' => array(
- 'set_up' => function (): void {
+ 'set_up' => static function (): string {
wp_set_current_user( self::factory()->user->create( array( 'role' => 'administrator' ) ) );
- $this->go_to( home_url( '/' ) );
+ return home_url( '/' );
},
'expected' => true,
),
@@ -263,8 +312,12 @@ public function data_provider_test_od_can_optimize_response(): array {
* @dataProvider data_provider_test_od_can_optimize_response
*/
public function test_od_can_optimize_response( Closure $set_up, bool $expected ): void {
- self::factory()->post->create(); // Make sure there is at least one post in the DB.
- $set_up();
+ // Make sure there is at least one post in the DB as otherwise od_get_cache_purge_post_id() will return false,
+ // causing od_can_optimize_response() to return false.
+ self::factory()->post->create();
+
+ $url = $set_up();
+ $this->go_to( $url );
$this->assertSame( $expected, od_can_optimize_response() );
}
diff --git a/plugins/optimization-detective/tests/test-site-health.php b/plugins/optimization-detective/tests/test-site-health.php
new file mode 100644
index 0000000000..8ca97fd51f
--- /dev/null
+++ b/plugins/optimization-detective/tests/test-site-health.php
@@ -0,0 +1,411 @@
+ 'rest_missing_callback_param',
+ 'message' => 'Missing parameter(s): slug, current_etag, hmac, url, viewport, elements',
+ 'data' => array(
+ 'status' => 400,
+ 'params' => array(
+ 'slug',
+ 'current_etag',
+ 'hmac',
+ 'url',
+ 'viewport',
+ 'elements',
+ ),
+ ),
+ ),
+ );
+
+ const UNAUTHORISED_MOCKED_RESPONSE_ARGS = array(
+ 401,
+ 'Unauthorized',
+ array(
+ 'code' => 'unauthorized_without_message',
+ ),
+ );
+
+ const FORBIDDEN_MOCKED_RESPONSE_ARGS = array(
+ 403,
+ 'Forbidden',
+ array(
+ 'code' => 'rest_login_required',
+ 'message' => 'REST API restricted to authenticated users.',
+ 'data' => array( 'status' => 401 ),
+ ),
+ );
+
+ /**
+ * @covers ::od_add_rest_api_availability_test
+ */
+ public function test_od_add_rest_api_availability_test(): void {
+ $initial_tests = array(
+ 'direct' => array(
+ 'foo' => array(
+ 'label' => 'Foo',
+ 'test' => 'foo_test',
+ ),
+ ),
+ );
+
+ $tests = od_add_rest_api_availability_test(
+ $initial_tests
+ );
+ $this->assertCount( 2, $tests['direct'] );
+ $this->assertArrayHasKey( 'foo', $tests['direct'] );
+ $this->assertSame( $initial_tests['direct']['foo'], $tests['direct']['foo'] );
+ $this->assertArrayHasKey( 'optimization_detective_rest_api', $tests['direct'] );
+ $this->assertArrayHasKey( 'label', $tests['direct']['optimization_detective_rest_api'] );
+ $this->assertArrayHasKey( 'test', $tests['direct']['optimization_detective_rest_api'] );
+ $this->assertTrue( is_callable( $tests['direct']['optimization_detective_rest_api']['test'] ) );
+ $this->filter_rest_api_response( $this->build_mock_response( ...self::EXPECTED_MOCKED_RESPONSE_ARGS ) );
+ $result = call_user_func( $tests['direct']['optimization_detective_rest_api']['test'] );
+ $this->assertSame( 'good', $result['status'] );
+
+ $tests = od_add_rest_api_availability_test(
+ new WP_Error()
+ );
+ $this->assertCount( 1, $tests['direct'] );
+ $this->assertArrayHasKey( 'optimization_detective_rest_api', $tests['direct'] );
+ }
+
+ /**
+ * Test that we presume the REST API is accessible before we are able to perform the Site Health check.
+ *
+ * @covers ::od_is_rest_api_unavailable
+ */
+ public function test_rest_api_assumed_accessible(): void {
+ $this->assertFalse( get_option( 'od_rest_api_unavailable', false ) );
+ $this->assertFalse( od_is_rest_api_unavailable() );
+ }
+
+ /**
+ * Data provider.
+ *
+ * @return array
+ */
+ public function data_provider_test_rest_api_availability(): array {
+ return array(
+ 'available' => array(
+ 'mocked_response' => $this->build_mock_response( ...self::EXPECTED_MOCKED_RESPONSE_ARGS ),
+ 'expected_option' => '0',
+ 'expected_status' => 'good',
+ 'expected_unavailable' => false,
+ ),
+ 'unauthorized' => array(
+ 'mocked_response' => $this->build_mock_response( ...self::UNAUTHORISED_MOCKED_RESPONSE_ARGS ),
+ 'expected_option' => '1',
+ 'expected_status' => 'recommended',
+ 'expected_unavailable' => true,
+ ),
+ 'forbidden' => array(
+ 'mocked_response' => $this->build_mock_response( ...self::FORBIDDEN_MOCKED_RESPONSE_ARGS ),
+ 'expected_option' => '1',
+ 'expected_status' => 'recommended',
+ 'expected_unavailable' => true,
+ ),
+ 'nginx_forbidden' => array(
+ 'mocked_response' => array(
+ 'response' => array(
+ 'code' => 403,
+ 'message' => 'Forbidden',
+ ),
+ 'body' => "\n403 Forbidden\n\n403 Forbidden
\n
nginx\n\n",
+ ),
+ 'expected_option' => '1',
+ 'expected_status' => 'recommended',
+ 'expected_unavailable' => true,
+ ),
+ 'error' => array(
+ 'mocked_response' => new WP_Error( 'bad', 'Something terrible has happened' ),
+ 'expected_option' => '1',
+ 'expected_status' => 'recommended',
+ 'expected_unavailable' => true,
+ ),
+ );
+ }
+
+ /**
+ * Test various conditions for the REST API being available.
+ *
+ * @covers ::od_test_rest_api_availability
+ * @covers ::od_compose_site_health_result
+ * @covers ::od_get_rest_api_health_check_response
+ * @covers ::od_is_rest_api_unavailable
+ *
+ * @dataProvider data_provider_test_rest_api_availability
+ *
+ * @phpstan-param array|WP_Error $mocked_response
+ */
+ public function test_rest_api_availability( $mocked_response, string $expected_option, string $expected_status, bool $expected_unavailable ): void {
+ $this->filter_rest_api_response( $mocked_response );
+
+ $result = od_test_rest_api_availability();
+ $this->assertArrayHasKey( 'label', $result );
+ $this->assertArrayHasKey( 'status', $result );
+ $this->assertArrayHasKey( 'badge', $result );
+ $this->assertArrayHasKey( 'description', $result );
+ $this->assertArrayHasKey( 'test', $result );
+ $this->assertSame( $expected_option, get_option( 'od_rest_api_unavailable', '' ) );
+ $this->assertArrayHasKey( 'od_rest_api_unavailable', wp_load_alloptions(), 'Expected option to be autoloaded.' );
+ $this->assertSame( $expected_status, $result['status'] );
+ $this->assertSame( $expected_unavailable, od_is_rest_api_unavailable() );
+ }
+
+ /**
+ * Data provider.
+ *
+ * @return array
+ */
+ public function data_provider_test_od_get_rest_api_health_check_response(): array {
+ return array(
+ 'available' => array(
+ 'mocked_response' => $this->build_mock_response( ...self::EXPECTED_MOCKED_RESPONSE_ARGS ),
+ ),
+ 'unauthorized' => array(
+ 'mocked_response' => $this->build_mock_response( ...self::UNAUTHORISED_MOCKED_RESPONSE_ARGS ),
+ ),
+ 'error' => array(
+ 'mocked_response' => new WP_Error( 'bad' ),
+ ),
+ );
+ }
+
+ /**
+ * @covers ::od_get_rest_api_health_check_response
+ *
+ * @dataProvider data_provider_test_od_get_rest_api_health_check_response
+ *
+ * @param array|WP_Error $mocked_response Mocked response.
+ */
+ public function test_od_get_rest_api_health_check_response( $mocked_response ): void {
+ $transient_key = 'od_rest_api_health_check_response';
+ $filter_observer = $this->filter_rest_api_response( $mocked_response );
+ delete_transient( $transient_key );
+ $this->assertSame( 0, $filter_observer->counter );
+
+ $response = od_get_rest_api_health_check_response( false );
+ $this->assertEquals( $mocked_response, get_transient( $transient_key ) );
+ $this->assertEquals( $mocked_response, $response );
+ $this->assertSame( 1, $filter_observer->counter );
+
+ $response = od_get_rest_api_health_check_response( false );
+ $this->assertEquals( $mocked_response, get_transient( $transient_key ) );
+ $this->assertEquals( $mocked_response, $response );
+ $this->assertSame( 2, $filter_observer->counter );
+
+ $response = od_get_rest_api_health_check_response( true );
+ $this->assertEquals( $mocked_response, get_transient( $transient_key ) );
+ $this->assertEquals( $mocked_response, $response );
+ $this->assertSame( 2, $filter_observer->counter );
+
+ delete_transient( $transient_key );
+ $response = od_get_rest_api_health_check_response( true );
+ $this->assertEquals( $mocked_response, get_transient( $transient_key ) );
+ $this->assertEquals( $mocked_response, $response );
+ $this->assertSame( 3, $filter_observer->counter );
+ }
+
+ /**
+ * Data provider.
+ *
+ * @return array
+ */
+ public function data_provider_in_plugin_row(): array {
+ return array(
+ 'in_admin_notices' => array(
+ 'in_plugin_row' => false,
+ ),
+ 'in_plugin_row' => array(
+ 'in_plugin_row' => true,
+ ),
+ );
+ }
+
+ /**
+ * Initial state when there is no option set yet.
+ *
+ * @dataProvider data_provider_in_plugin_row
+ * @covers ::od_maybe_render_rest_api_health_check_admin_notice
+ */
+ public function test_od_maybe_render_rest_api_health_check_admin_notice_no_option_set( bool $in_plugin_row ): void {
+ $this->assertFalse( od_is_rest_api_unavailable() );
+ $this->assertSame( '', get_echo( 'od_maybe_render_rest_api_health_check_admin_notice', array( $in_plugin_row ) ) );
+ }
+
+ /**
+ * When the REST API works as expected.
+ *
+ * @dataProvider data_provider_in_plugin_row
+ * @covers ::od_maybe_render_rest_api_health_check_admin_notice
+ */
+ public function test_od_maybe_render_rest_api_health_check_admin_notice_rest_api_available( bool $in_plugin_row ): void {
+ $this->filter_rest_api_response( $this->build_mock_response( ...self::EXPECTED_MOCKED_RESPONSE_ARGS ) );
+ od_test_rest_api_availability();
+ $this->assertFalse( od_is_rest_api_unavailable() );
+ $this->assertSame( '', get_echo( 'od_maybe_render_rest_api_health_check_admin_notice', array( $in_plugin_row ) ) );
+ }
+
+ /**
+ * When the REST API is not available.
+ *
+ * @dataProvider data_provider_in_plugin_row
+ * @covers ::od_maybe_render_rest_api_health_check_admin_notice
+ */
+ public function test_od_maybe_render_rest_api_health_check_admin_notice_rest_api_not_available( bool $in_plugin_row ): void {
+ $super_admin_user_id = self::factory()->user->create( array( 'role' => 'administrator' ) );
+ grant_super_admin( $super_admin_user_id ); // Since Site Health is only available to super admins.
+ wp_set_current_user( $super_admin_user_id );
+
+ $this->filter_rest_api_response( $this->build_mock_response( ...self::UNAUTHORISED_MOCKED_RESPONSE_ARGS ) );
+ od_test_rest_api_availability();
+ $this->assertTrue( od_is_rest_api_unavailable() );
+ $notice = get_echo( 'od_maybe_render_rest_api_health_check_admin_notice', array( $in_plugin_row ) );
+ $this->assertStringContainsString( 'assertStringContainsString( '
', $notice );
+ $this->assertStringContainsString( '', $notice );
+ $this->assertStringNotContainsString( '', $notice );
+ }
+
+ /**
+ * Data provider.
+ *
+ * @return array
+ */
+ public function data_provider_test_od_maybe_run_rest_api_health_check(): array {
+ return array(
+ 'option_absent_and_rest_api_available' => array(
+ 'set_up' => function (): void {
+ delete_option( 'od_rest_api_unavailable' );
+ $this->filter_rest_api_response( $this->build_mock_response( ...self::EXPECTED_MOCKED_RESPONSE_ARGS ) );
+ },
+ 'expected' => false,
+ ),
+ 'option_present_and_cached_available' => array(
+ 'set_up' => static function (): void {
+ update_option( 'od_rest_api_unavailable', '0' );
+ },
+ 'expected' => false,
+ ),
+ 'rest_api_unavailable' => array(
+ 'set_up' => function (): void {
+ delete_option( 'od_rest_api_unavailable' );
+ $this->filter_rest_api_response( $this->build_mock_response( ...self::UNAUTHORISED_MOCKED_RESPONSE_ARGS ) );
+ },
+ 'expected' => true,
+ ),
+ );
+ }
+
+ /**
+ * @dataProvider data_provider_test_od_maybe_run_rest_api_health_check
+ *
+ * @covers ::od_maybe_run_rest_api_health_check
+ */
+ public function test_od_maybe_run_rest_api_health( Closure $set_up, bool $expected ): void {
+ remove_all_actions( 'admin_notices' );
+ $set_up();
+ od_maybe_run_rest_api_health_check();
+ $this->assertSame( $expected, (bool) has_action( 'admin_notices' ) );
+ $admin_notices_output = get_echo( 'do_action', array( 'admin_notices' ) );
+ if ( $expected ) {
+ $this->assertStringContainsString( '', $admin_notices_output );
+ } else {
+ $this->assertStringNotContainsString( '
', $admin_notices_output );
+ }
+ }
+
+ /**
+ * Filters REST API response with mock.
+ *
+ * @param array|WP_Error $mocked_response Mocked response.
+ * @return object{ counter: int } Value which contains a counter for the number of times the filter applied.
+ */
+ protected function filter_rest_api_response( $mocked_response ): object {
+ $observer = (object) array( 'counter' => 0 );
+ remove_all_filters( 'pre_http_request' );
+ add_filter(
+ 'pre_http_request',
+ static function ( $pre, array $args, string $url ) use ( $mocked_response, $observer ) {
+ if ( rest_url( OD_REST_API_NAMESPACE . OD_URL_METRICS_ROUTE ) === $url ) {
+ $observer->counter++;
+ return $mocked_response;
+ }
+ return $pre;
+ },
+ 10,
+ 3
+ );
+ return $observer;
+ }
+
+ /**
+ * Build a mock response.
+ *
+ * @param int $status_code HTTP status code.
+ * @param string $message HTTP status message.
+ * @param array $body Response body.
+ * @return array Mocked response.
+ */
+ protected function build_mock_response( int $status_code, string $message, array $body = array() ): array {
+ return array(
+ 'response' => array(
+ 'code' => $status_code,
+ 'message' => $message,
+ ),
+ 'body' => wp_json_encode( $body ),
+ );
+ }
+}
diff --git a/plugins/optimization-detective/uninstall.php b/plugins/optimization-detective/uninstall.php
index 6202323d9b..f5e360c6b3 100644
--- a/plugins/optimization-detective/uninstall.php
+++ b/plugins/optimization-detective/uninstall.php
@@ -17,6 +17,10 @@
// Delete all URL Metrics posts for the current site.
OD_URL_Metrics_Post_Type::delete_all_posts();
wp_unschedule_hook( OD_URL_Metrics_Post_Type::GC_CRON_EVENT_NAME );
+
+ // Clear out site health check data.
+ delete_option( 'od_rest_api_unavailable' );
+ delete_transient( 'od_rest_api_health_check_response' );
};
$od_delete_site_data();