Skip to content

Commit

Permalink
Merge pull request #79 from ConvertKit/refresh-token-check-environment
Browse files Browse the repository at this point in the history
Check WordPress environment when automatically refreshing token
  • Loading branch information
n7studios authored Sep 12, 2024
2 parents 07a2fb5 + ea8d558 commit 389a483
Show file tree
Hide file tree
Showing 5 changed files with 154 additions and 1 deletion.
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 @@ -6239,6 +6265,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

0 comments on commit 389a483

Please sign in to comment.