Skip to content
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

Check WordPress environment when automatically refreshing token #79

Merged
merged 10 commits into from
Sep 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .env.dist.testing
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ TEST_SITE_ADMIN_USERNAME=admin
TEST_SITE_ADMIN_PASSWORD=password
TEST_SITE_WP_ADMIN_PATH=/wp-admin
WP_ROOT_FOLDER="/home/runner/work/convertkit-wordpress-libraries/convertkit-wordpress-libraries/wordpress"
WP_ENVIRONMENT_TYPE=local
TEST_DB_NAME=test
TEST_DB_HOST=localhost
TEST_DB_USER=root
Expand All @@ -16,6 +17,7 @@ TEST_TABLE_PREFIX=wp_
TEST_SITE_WP_URL=http://127.0.0.1
TEST_SITE_WP_DOMAIN=127.0.0.1
TEST_SITE_ADMIN_EMAIL=wordpress@convertkit.local
TEST_SITE_CONFIG_FILE="/home/runner/work/convertkit-wordpress-libraries/convertkit-wordpress-libraries/wordpress/wp-content/plugins/convertkit-wordpress-libraries/tests/_support/WpunitTesterConfig.php"
CONVERTKIT_API_BROADCAST_ID="8697158"
CONVERTKIT_API_CUSTOM_FIELD_ID="258240"
CONVERTKIT_API_FORM_ID="2765139"
Expand Down
29 changes: 29 additions & 0 deletions src/class-convertkit-api-v4.php
Original file line number Diff line number Diff line change
Expand Up @@ -1468,6 +1468,14 @@ public function request( $endpoint, $method = 'get', $params = array(), $retry_i
break;
}

// Don't automatically refresh the expired access token if we're not on a production environment.
// This prevents the same ConvertKit account used on both a staging and production site from
// reaching a race condition where the staging site refreshes the token first, resulting in
// the production site unable to later refresh its same expired access token.
if ( ! $this->is_production_site() ) {
break;
}

// Refresh the access token.
$result = $this->refresh_token();

Expand Down Expand Up @@ -1506,6 +1514,27 @@ public function request( $endpoint, $method = 'get', $params = array(), $retry_i

}

/**
* Helper method to determine the WordPress environment type, checking
* if the wp_get_environment_type() function exists in WordPress (versions
* older than WordPress 5.5 won't have this function).
*
* @since 2.0.2
*
* @return bool
*/
private function is_production_site() {

// If the WordPress wp_get_environment_type() function isn't available,
// assume this is a production site.
if ( ! function_exists( 'wp_get_environment_type' ) ) {
return true;
}

return ( wp_get_environment_type() === 'production' );

}

/**
* Inspects the given API response for errors, returning them as a string.
*
Expand Down
11 changes: 11 additions & 0 deletions tests/_support/WpunitTesterConfig.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php
/**
* Define any constants not supported by WPLoader, such as the environment type.
*
* See: https://github.com/lucatume/wp-browser/blob/master/docs/v3/modules/WPLoader.md,
* parameter `configFile`.
*
* @package ConvertKit
*/

define( 'WP_ENVIRONMENT_TYPE', $_ENV['WP_ENVIRONMENT_TYPE'] );
3 changes: 2 additions & 1 deletion tests/wpunit.suite.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,5 @@ modules:
tablePrefix: "%TEST_TABLE_PREFIX%"
domain: "%TEST_SITE_WP_DOMAIN%"
adminEmail: "%TEST_SITE_ADMIN_EMAIL%"
title: "Test"
title: "Test"
configFile: "%TEST_SITE_CONFIG_FILE%"
110 changes: 110 additions & 0 deletions tests/wpunit/APITest.php
Original file line number Diff line number Diff line change
Expand Up @@ -491,6 +491,32 @@ public function testRefreshTokenWithInvalidToken()
$this->assertEquals($result->get_error_code(), 'convertkit_api_error');
}

/**
* Test that making a call with an expired access token results in refresh_token()
* not being automatically called, when the WordPress site isn't a production site.
*
* @since 2.0.2
*
* @return void
*/
public function testRefreshTokenWhenAccessTokenExpiredErrorOnNonProductionSite()
{
// If the refresh token action in the libraries is triggered when calling get_account(), the test failed.
add_action(
'convertkit_api_refresh_token',
function() {
$this->fail('`convertkit_api_refresh_token` was triggered when calling `get_account` with an expired access token on a non-production site.');
}
);

// Filter requests to mock the token expiry and refreshing the token.
add_filter( 'pre_http_request', array( $this, 'mockAccessTokenExpiredResponse' ), 10, 3 );
add_filter( 'pre_http_request', array( $this, 'mockRefreshTokenResponse' ), 10, 3 );

// Run request, which will trigger the above filters as if the token expired and refreshes automatically.
$result = $this->api->get_account();
}

/**
* Test that supplying no API credentials to the API class returns a WP_Error.
*
Expand Down Expand Up @@ -6230,6 +6256,90 @@ function( $response ) use ( $httpCode, $httpMessage, $body ) { // phpcs:ignore G
);
}

/**
* Mocks an API response as if the Access Token expired.
*
* @since 2.0.2
*
* @param mixed $response HTTP Response.
* @param array $parsed_args Request arguments.
* @param string $url Request URL.
* @return mixed
*/
public function mockAccessTokenExpiredResponse( $response, $parsed_args, $url )
{
// Only mock requests made to the /account endpoint.
if ( strpos( $url, 'https://api.convertkit.com/v4/account' ) === false ) {
return $response;
}

// Remove this filter, so we don't end up in a loop when retrying the request.
remove_filter( 'pre_http_request', array( $this, 'mockAccessTokenExpiredResponse' ) );

// Return a 401 unauthorized response with the errors body as if the API
// returned "The access token expired".
return array(
'headers' => array(),
'body' => wp_json_encode(
array(
'errors' => array(
'The access token expired',
),
)
),
'response' => array(
'code' => 401,
'message' => 'The access token expired',
),
'cookies' => array(),
'http_response' => null,
);
}

/**
* Mocks an API response as if a refresh token was used to fetch new tokens.
*
* @since 2.0.2
*
* @param mixed $response HTTP Response.
* @param array $parsed_args Request arguments.
* @param string $url Request URL.
* @return mixed
*/
public function mockRefreshTokenResponse( $response, $parsed_args, $url )
{
// Only mock requests made to the /token endpoint.
if ( strpos( $url, 'https://api.convertkit.com/oauth/token' ) === false ) {
return $response;
}

// Remove this filter, so we don't end up in a loop when retrying the request.
remove_filter( 'pre_http_request', array( $this, 'mockRefreshTokenResponse' ) );

// Return a mock access and refresh token for this API request, as calling
// refresh_token results in a new access and refresh token being provided,
// which would result in other tests breaking due to changed tokens.
return array(
'headers' => array(),
'body' => wp_json_encode(
array(
'access_token' => 'new-' . $_ENV['CONVERTKIT_OAUTH_ACCESS_TOKEN'],
'refresh_token' => 'new-' . $_ENV['CONVERTKIT_OAUTH_REFRESH_TOKEN'],
'token_type' => 'bearer',
'created_at' => strtotime( 'now' ),
'expires_in' => 10000,
'scope' => 'public',
)
),
'response' => array(
'code' => 200,
'message' => 'OK',
),
'cookies' => array(),
'http_response' => null,
);
}

/**
* Helper method to assert the given key exists as an array
* in the API response.
Expand Down
Loading