-
Notifications
You must be signed in to change notification settings - Fork 103
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add site health check to detect blocked REST API and short-circuit optimization when Inaccessible #1762
base: trunk
Are you sure you want to change the base?
Add site health check to detect blocked REST API and short-circuit optimization when Inaccessible #1762
Changes from 13 commits
88952a5
4bff9fe
56d769a
ba911e1
12a229c
a0f460d
c29cb7c
8d8ae3e
422a5bf
0bc74f7
36042fd
ad21df8
0bba468
e1b9004
dbede65
2b2ca3e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
<?php | ||
/** | ||
* Site Health checks loader. | ||
* | ||
* @package optimization-detective | ||
* @since n.e.x.t | ||
*/ | ||
|
||
if ( ! defined( 'ABSPATH' ) ) { | ||
exit; // Exit if accessed directly. | ||
} | ||
|
||
// REST API site health check. | ||
require_once __DIR__ . '/rest-api/helper.php'; | ||
require_once __DIR__ . '/rest-api/hooks.php'; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,122 @@ | ||
<?php | ||
/** | ||
* Helper functions for the Optimization Detective REST API health check. | ||
* | ||
* @package optimization-detective | ||
* @since n.e.x.t | ||
*/ | ||
|
||
if ( ! defined( 'ABSPATH' ) ) { | ||
exit; // Exit if accessed directly. | ||
} | ||
|
||
/** | ||
* Tests availability of the Optimization Detective REST API endpoint. | ||
* | ||
* @since n.e.x.t | ||
* | ||
* @return array{label: string, status: string, badge: array{label: string, color: string}, description: string, actions: string, test: string} Result. | ||
*/ | ||
function od_optimization_detective_rest_api_test(): array { | ||
$result = array( | ||
'label' => __( 'Your site has functional Optimization Detective REST API endpoint', 'optimization-detective' ), | ||
'status' => 'good', | ||
'badge' => array( | ||
'label' => __( 'Optimization Detective', 'optimization-detective' ), | ||
'color' => 'blue', | ||
), | ||
'description' => sprintf( | ||
'<p>%s</p>', | ||
__( 'Optimization Detective can send and store URL metrics via REST API endpoint', 'optimization-detective' ) | ||
), | ||
'actions' => '', | ||
'test' => 'optimization_detective_rest_api', | ||
); | ||
|
||
$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, | ||
) | ||
); | ||
|
||
if ( is_wp_error( $response ) ) { | ||
$result['status'] = 'recommended'; | ||
$result['label'] = __( 'Your site encountered error accessing Optimization Detective REST API endpoint', 'optimization-detective' ); | ||
$result['description'] = sprintf( | ||
'<p>%s</p>', | ||
esc_html__( 'The Optimization Detective endpoint could not be reached. This might mean the REST API is disabled or blocked.', 'optimization-detective' ) | ||
); | ||
$info = array( | ||
'error_message' => $response->get_error_message(), | ||
'error_code' => $response->get_error_code(), | ||
'available' => false, | ||
); | ||
} else { | ||
$status_code = wp_remote_retrieve_response_code( $response ); | ||
$data = json_decode( wp_remote_retrieve_body( $response ), true ); | ||
$expected_params = array( 'slug', 'current_etag', 'hmac', 'url', 'viewport', 'elements' ); | ||
$info = array( | ||
'status_code' => $status_code, | ||
'available' => false, | ||
); | ||
|
||
if ( | ||
400 === $status_code | ||
&& isset( $data['data']['params'] ) | ||
&& is_array( $data['data']['params'] ) | ||
&& count( $expected_params ) === count( array_intersect( $data['data']['params'], $expected_params ) ) | ||
) { | ||
// The REST API endpoint is available. | ||
$info['available'] = true; | ||
} elseif ( 401 === $status_code ) { | ||
$result['status'] = 'recommended'; | ||
$result['label'] = __( 'Your site encountered unauthorized error for Optimization Detective REST API endpoint', 'optimization-detective' ); | ||
$result['description'] = sprintf( | ||
'<p>%s</p>', | ||
esc_html__( 'The REST API endpoint requires authentication. Ensure proper credentials are provided.', 'optimization-detective' ) | ||
); | ||
} elseif ( 403 === $status_code ) { | ||
$result['status'] = 'recommended'; | ||
$result['label'] = __( 'Your site encountered forbidden error for Optimization Detective REST API endpoint', 'optimization-detective' ); | ||
$result['description'] = sprintf( | ||
'<p>%s</p>', | ||
esc_html__( 'The REST API endpoint is blocked check server or security settings.', 'optimization-detective' ) | ||
); | ||
} else { | ||
$result['status'] = 'recommended'; | ||
$result['label'] = __( 'Your site encountered error accessing Optimization Detective REST API endpoint', 'optimization-detective' ); | ||
$result['description'] = sprintf( | ||
'<p>%s</p>', | ||
esc_html__( 'The Optimization Detective endpoint could not be reached. This might mean the REST API is disabled or blocked.', 'optimization-detective' ) | ||
); | ||
} | ||
} | ||
|
||
update_option( 'od_rest_api_info', $info ); | ||
return $result; | ||
} | ||
|
||
/** | ||
* Periodically runs the Optimization Detective REST API health check. | ||
* | ||
* @since n.e.x.t | ||
*/ | ||
function od_schedule_rest_api_health_check(): void { | ||
if ( ! (bool) wp_next_scheduled( 'od_rest_api_health_check_event' ) ) { | ||
wp_schedule_event( time(), 'hourly', 'od_rest_api_health_check_event' ); | ||
} | ||
} | ||
add_action( 'wp', 'od_schedule_rest_api_health_check' ); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this a best practice? Should it rather go in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think scheduling on plugin activation hook will be better than |
||
|
||
/** | ||
* Hook for the scheduled REST API health check. | ||
* | ||
* @since n.e.x.t | ||
*/ | ||
function od_run_scheduled_rest_api_health_check(): void { | ||
od_optimization_detective_rest_api_test(); | ||
} | ||
add_action( 'od_rest_api_health_check_event', 'od_run_scheduled_rest_api_health_check' ); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
<?php | ||
/** | ||
* Hook callbacks used for the Optimization Detective REST API health check. | ||
* | ||
* @package optimization-detective | ||
* @since n.e.x.t | ||
*/ | ||
|
||
if ( ! defined( 'ABSPATH' ) ) { | ||
exit; // Exit if accessed directly. | ||
} | ||
|
||
/** | ||
* Adds the Optimization Detective REST API check to site health tests. | ||
* | ||
* @since n.e.x.t | ||
* | ||
* @param array{direct: array<string, array{label: string, test: string}>} $tests Site Health Tests. | ||
* @return array{direct: array<string, array{label: string, test: string}>} Amended tests. | ||
*/ | ||
function od_optimization_detective_add_rest_api_test( array $tests ): array { | ||
$tests['direct']['optimization_detective_rest_api'] = array( | ||
'label' => __( 'Optimization Detective REST API Endpoint Availability', 'optimization-detective' ), | ||
'test' => 'od_optimization_detective_rest_api_test', | ||
); | ||
|
||
return $tests; | ||
} | ||
add_filter( 'site_status_tests', 'od_optimization_detective_add_rest_api_test' ); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,131 @@ | ||
<?php | ||
/** | ||
* Tests for Optimization Detective REST API site health check. | ||
* | ||
* @package optimization-detective | ||
*/ | ||
|
||
class Test_OD_REST_API_Site_Health_Check extends WP_UnitTestCase { | ||
|
||
/** | ||
* Holds mocked response headers for different test scenarios. | ||
* | ||
* @var array<string, array<string, mixed>> | ||
*/ | ||
protected $mocked_responses = array(); | ||
|
||
/** | ||
* Setup each test. | ||
*/ | ||
public function setUp(): void { | ||
parent::setUp(); | ||
|
||
// Clear any filters or mocks. | ||
remove_all_filters( 'pre_http_request' ); | ||
|
||
// Add the filter to mock HTTP requests. | ||
add_filter( 'pre_http_request', array( $this, 'mock_http_requests' ), 10, 3 ); | ||
} | ||
|
||
/** | ||
* Test that the site health check is `good` when the REST API is available. | ||
*/ | ||
public function test_rest_api_available(): void { | ||
$this->mocked_responses = array( | ||
get_rest_url( null, OD_REST_API_NAMESPACE . OD_URL_METRICS_ROUTE ) => $this->build_mock_response( | ||
400, | ||
'Bad Request', | ||
array( | ||
'data' => array( | ||
'params' => array( 'slug', 'current_etag', 'hmac', 'url', 'viewport', 'elements' ), | ||
), | ||
) | ||
), | ||
); | ||
|
||
$result = od_optimization_detective_rest_api_test(); | ||
$od_rest_api_info = get_option( 'od_rest_api_info', array() ); | ||
|
||
$this->assertSame( 'good', $result['status'] ); | ||
$this->assertSame( 400, isset( $od_rest_api_info['status_code'] ) ? $od_rest_api_info['status_code'] : '' ); | ||
$this->assertTrue( isset( $od_rest_api_info['available'] ) ? $od_rest_api_info['available'] : false ); | ||
} | ||
|
||
/** | ||
* Test behavior when REST API returns an unauthorized error. | ||
*/ | ||
public function test_rest_api_unauthorized(): void { | ||
$this->mocked_responses = array( | ||
get_rest_url( null, OD_REST_API_NAMESPACE . OD_URL_METRICS_ROUTE ) => $this->build_mock_response( | ||
401, | ||
'Unauthorized' | ||
), | ||
); | ||
|
||
$result = od_optimization_detective_rest_api_test(); | ||
$od_rest_api_info = get_option( 'od_rest_api_info', array() ); | ||
|
||
$this->assertSame( 'recommended', $result['status'] ); | ||
$this->assertSame( 401, isset( $od_rest_api_info['status_code'] ) ? $od_rest_api_info['status_code'] : '' ); | ||
$this->assertFalse( isset( $od_rest_api_info['available'] ) ? $od_rest_api_info['available'] : true ); | ||
} | ||
|
||
/** | ||
* Test behavior when REST API returns an forbidden error. | ||
*/ | ||
public function test_rest_api_forbidden(): void { | ||
$this->mocked_responses = array( | ||
get_rest_url( null, OD_REST_API_NAMESPACE . OD_URL_METRICS_ROUTE ) => $this->build_mock_response( | ||
403, | ||
'Forbidden' | ||
), | ||
); | ||
|
||
$result = od_optimization_detective_rest_api_test(); | ||
$od_rest_api_info = get_option( 'od_rest_api_info', array() ); | ||
|
||
$this->assertSame( 'recommended', $result['status'] ); | ||
$this->assertSame( 403, isset( $od_rest_api_info['status_code'] ) ? $od_rest_api_info['status_code'] : '' ); | ||
$this->assertFalse( isset( $od_rest_api_info['available'] ) ? $od_rest_api_info['available'] : true ); | ||
} | ||
|
||
/** | ||
* Mock HTTP requests for assets to simulate different responses. | ||
* | ||
* @param bool $response A preemptive return value of an HTTP request. Default false. | ||
* @param array<string, mixed> $args Request arguments. | ||
* @param string $url The request URL. | ||
* @return array<string, mixed> Mocked response. | ||
*/ | ||
public function mock_http_requests( bool $response, array $args, string $url ): array { | ||
if ( isset( $this->mocked_responses[ $url ] ) ) { | ||
return $this->mocked_responses[ $url ]; | ||
} | ||
|
||
// If no specific mock set, default to a generic success response. | ||
return array( | ||
'response' => array( | ||
'code' => 200, | ||
'message' => 'OK', | ||
), | ||
); | ||
} | ||
|
||
/** | ||
* Build a mock response. | ||
* | ||
* @param int $status_code HTTP status code. | ||
* @param string $message HTTP status message. | ||
* @param array<string, mixed> $body Response body. | ||
* @return array<string, mixed> 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 ), | ||
); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think hourly is too much. Maybe weekly would make sense.