Skip to content

Commit 389a483

Browse files
authored
Merge pull request #79 from ConvertKit/refresh-token-check-environment
Check WordPress environment when automatically refreshing token
2 parents 07a2fb5 + ea8d558 commit 389a483

File tree

5 files changed

+154
-1
lines changed

5 files changed

+154
-1
lines changed

.env.dist.testing

+2
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ TEST_SITE_ADMIN_USERNAME=admin
88
TEST_SITE_ADMIN_PASSWORD=password
99
TEST_SITE_WP_ADMIN_PATH=/wp-admin
1010
WP_ROOT_FOLDER="/home/runner/work/convertkit-wordpress-libraries/convertkit-wordpress-libraries/wordpress"
11+
WP_ENVIRONMENT_TYPE=local
1112
TEST_DB_NAME=test
1213
TEST_DB_HOST=localhost
1314
TEST_DB_USER=root
@@ -16,6 +17,7 @@ TEST_TABLE_PREFIX=wp_
1617
TEST_SITE_WP_URL=http://127.0.0.1
1718
TEST_SITE_WP_DOMAIN=127.0.0.1
1819
20+
TEST_SITE_CONFIG_FILE="/home/runner/work/convertkit-wordpress-libraries/convertkit-wordpress-libraries/wordpress/wp-content/plugins/convertkit-wordpress-libraries/tests/_support/WpunitTesterConfig.php"
1921
CONVERTKIT_API_BROADCAST_ID="8697158"
2022
CONVERTKIT_API_CUSTOM_FIELD_ID="258240"
2123
CONVERTKIT_API_FORM_ID="2765139"

src/class-convertkit-api-v4.php

+29
Original file line numberDiff line numberDiff line change
@@ -1468,6 +1468,14 @@ public function request( $endpoint, $method = 'get', $params = array(), $retry_i
14681468
break;
14691469
}
14701470

1471+
// Don't automatically refresh the expired access token if we're not on a production environment.
1472+
// This prevents the same ConvertKit account used on both a staging and production site from
1473+
// reaching a race condition where the staging site refreshes the token first, resulting in
1474+
// the production site unable to later refresh its same expired access token.
1475+
if ( ! $this->is_production_site() ) {
1476+
break;
1477+
}
1478+
14711479
// Refresh the access token.
14721480
$result = $this->refresh_token();
14731481

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

15071515
}
15081516

1517+
/**
1518+
* Helper method to determine the WordPress environment type, checking
1519+
* if the wp_get_environment_type() function exists in WordPress (versions
1520+
* older than WordPress 5.5 won't have this function).
1521+
*
1522+
* @since 2.0.2
1523+
*
1524+
* @return bool
1525+
*/
1526+
private function is_production_site() {
1527+
1528+
// If the WordPress wp_get_environment_type() function isn't available,
1529+
// assume this is a production site.
1530+
if ( ! function_exists( 'wp_get_environment_type' ) ) {
1531+
return true;
1532+
}
1533+
1534+
return ( wp_get_environment_type() === 'production' );
1535+
1536+
}
1537+
15091538
/**
15101539
* Inspects the given API response for errors, returning them as a string.
15111540
*

tests/_support/WpunitTesterConfig.php

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?php
2+
/**
3+
* Define any constants not supported by WPLoader, such as the environment type.
4+
*
5+
* See: https://github.com/lucatume/wp-browser/blob/master/docs/v3/modules/WPLoader.md,
6+
* parameter `configFile`.
7+
*
8+
* @package ConvertKit
9+
*/
10+
11+
define( 'WP_ENVIRONMENT_TYPE', $_ENV['WP_ENVIRONMENT_TYPE'] );

tests/wpunit.suite.yml

+2-1
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,5 @@ modules:
2121
tablePrefix: "%TEST_TABLE_PREFIX%"
2222
domain: "%TEST_SITE_WP_DOMAIN%"
2323
adminEmail: "%TEST_SITE_ADMIN_EMAIL%"
24-
title: "Test"
24+
title: "Test"
25+
configFile: "%TEST_SITE_CONFIG_FILE%"

tests/wpunit/APITest.php

+110
Original file line numberDiff line numberDiff line change
@@ -491,6 +491,32 @@ public function testRefreshTokenWithInvalidToken()
491491
$this->assertEquals($result->get_error_code(), 'convertkit_api_error');
492492
}
493493

494+
/**
495+
* Test that making a call with an expired access token results in refresh_token()
496+
* not being automatically called, when the WordPress site isn't a production site.
497+
*
498+
* @since 2.0.2
499+
*
500+
* @return void
501+
*/
502+
public function testRefreshTokenWhenAccessTokenExpiredErrorOnNonProductionSite()
503+
{
504+
// If the refresh token action in the libraries is triggered when calling get_account(), the test failed.
505+
add_action(
506+
'convertkit_api_refresh_token',
507+
function() {
508+
$this->fail('`convertkit_api_refresh_token` was triggered when calling `get_account` with an expired access token on a non-production site.');
509+
}
510+
);
511+
512+
// Filter requests to mock the token expiry and refreshing the token.
513+
add_filter( 'pre_http_request', array( $this, 'mockAccessTokenExpiredResponse' ), 10, 3 );
514+
add_filter( 'pre_http_request', array( $this, 'mockRefreshTokenResponse' ), 10, 3 );
515+
516+
// Run request, which will trigger the above filters as if the token expired and refreshes automatically.
517+
$result = $this->api->get_account();
518+
}
519+
494520
/**
495521
* Test that supplying no API credentials to the API class returns a WP_Error.
496522
*
@@ -6239,6 +6265,90 @@ function( $response ) use ( $httpCode, $httpMessage, $body ) { // phpcs:ignore G
62396265
);
62406266
}
62416267

6268+
/**
6269+
* Mocks an API response as if the Access Token expired.
6270+
*
6271+
* @since 2.0.2
6272+
*
6273+
* @param mixed $response HTTP Response.
6274+
* @param array $parsed_args Request arguments.
6275+
* @param string $url Request URL.
6276+
* @return mixed
6277+
*/
6278+
public function mockAccessTokenExpiredResponse( $response, $parsed_args, $url )
6279+
{
6280+
// Only mock requests made to the /account endpoint.
6281+
if ( strpos( $url, 'https://api.convertkit.com/v4/account' ) === false ) {
6282+
return $response;
6283+
}
6284+
6285+
// Remove this filter, so we don't end up in a loop when retrying the request.
6286+
remove_filter( 'pre_http_request', array( $this, 'mockAccessTokenExpiredResponse' ) );
6287+
6288+
// Return a 401 unauthorized response with the errors body as if the API
6289+
// returned "The access token expired".
6290+
return array(
6291+
'headers' => array(),
6292+
'body' => wp_json_encode(
6293+
array(
6294+
'errors' => array(
6295+
'The access token expired',
6296+
),
6297+
)
6298+
),
6299+
'response' => array(
6300+
'code' => 401,
6301+
'message' => 'The access token expired',
6302+
),
6303+
'cookies' => array(),
6304+
'http_response' => null,
6305+
);
6306+
}
6307+
6308+
/**
6309+
* Mocks an API response as if a refresh token was used to fetch new tokens.
6310+
*
6311+
* @since 2.0.2
6312+
*
6313+
* @param mixed $response HTTP Response.
6314+
* @param array $parsed_args Request arguments.
6315+
* @param string $url Request URL.
6316+
* @return mixed
6317+
*/
6318+
public function mockRefreshTokenResponse( $response, $parsed_args, $url )
6319+
{
6320+
// Only mock requests made to the /token endpoint.
6321+
if ( strpos( $url, 'https://api.convertkit.com/oauth/token' ) === false ) {
6322+
return $response;
6323+
}
6324+
6325+
// Remove this filter, so we don't end up in a loop when retrying the request.
6326+
remove_filter( 'pre_http_request', array( $this, 'mockRefreshTokenResponse' ) );
6327+
6328+
// Return a mock access and refresh token for this API request, as calling
6329+
// refresh_token results in a new access and refresh token being provided,
6330+
// which would result in other tests breaking due to changed tokens.
6331+
return array(
6332+
'headers' => array(),
6333+
'body' => wp_json_encode(
6334+
array(
6335+
'access_token' => 'new-' . $_ENV['CONVERTKIT_OAUTH_ACCESS_TOKEN'],
6336+
'refresh_token' => 'new-' . $_ENV['CONVERTKIT_OAUTH_REFRESH_TOKEN'],
6337+
'token_type' => 'bearer',
6338+
'created_at' => strtotime( 'now' ),
6339+
'expires_in' => 10000,
6340+
'scope' => 'public',
6341+
)
6342+
),
6343+
'response' => array(
6344+
'code' => 200,
6345+
'message' => 'OK',
6346+
),
6347+
'cookies' => array(),
6348+
'http_response' => null,
6349+
);
6350+
}
6351+
62426352
/**
62436353
* Helper method to assert the given key exists as an array
62446354
* in the API response.

0 commit comments

Comments
 (0)