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

v4 API: Save updated tokens on refresh #51

Merged
merged 3 commits into from
Jun 20, 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: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"type": "project",
"license": "GPLv3",
"require": {
"convertkit/convertkit-wordpress-libraries": "dev-v4-api"
"convertkit/convertkit-wordpress-libraries": "dev-v4-api-refresh-token-action-data"
},
"require-dev": {
"lucatume/wp-browser": "<3.5",
Expand Down
54 changes: 54 additions & 0 deletions integrate-convertkit-wpforms.php
Original file line number Diff line number Diff line change
Expand Up @@ -75,3 +75,57 @@ function integrate_convertkit_wpforms() {

}
add_action( 'wpforms_loaded', 'integrate_convertkit_wpforms' );

/**
* Saves the new access token, refresh token and its expiry when the API
* class automatically refreshes an outdated access token.
*
* @since 1.7.0
*
* @param array $result New Access Token, Refresh Token and Expiry.
* @param string $client_id OAuth Client ID used for the Access and Refresh Tokens.
* @param string $previous_access_token Existing (expired) Access Token.
*/
add_action(
'convertkit_api_refresh_token',
function ( $result, $client_id, $previous_access_token ) {

// Don't save these credentials if they're not for this Client ID.
// They're for another ConvertKit Plugin that uses OAuth.
if ( $client_id !== INTEGRATE_CONVERTKIT_WPFORMS_OAUTH_CLIENT_ID ) {
return;
}

// Get all registered providers in WPForms.
$providers = wpforms_get_providers_options();

// Bail if no ConvertKit providers were registered.
if ( ! array_key_exists( 'convertkit', $providers ) ) {
return;
}

// Iterate through providers to find the specific connection containing the now expired Access and Refresh Tokens.
foreach ( $providers['convertkit'] as $id => $settings ) {
// Skip if this isn't the connection.
if ( $settings['access_token'] !== $previous_access_token ) {
continue;
}

// Store the new credentials.
wpforms_update_providers_options(
'convertkit',
array(
'access_token' => sanitize_text_field( $result['access_token'] ),
'refresh_token' => sanitize_text_field( $result['refresh_token'] ),
'token_expires' => ( $result['created_at'] + $result['expires_in'] ),
'label' => $settings['label'],
'date' => $settings['date'],
),
$id
);
}

},
10,
3
);
129 changes: 129 additions & 0 deletions tests/wpunit/APITest.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ public function setUp(): void
parent::setUp();

// Activate Plugin, to include the Plugin's constants in tests.
activate_plugins('wpforms-lite/wpforms.php');
activate_plugins('convertkit-wpforms/integrate-convertkit-wpforms.php');

// Include class from /includes to test, as they won't be loaded by the Plugin
Expand Down Expand Up @@ -60,6 +61,134 @@ public function tearDown(): void
parent::tearDown();
}

/**
* Test that the Access Token is refreshed when a call is made to the API
* using an expired Access Token, and that the new tokens are saved in
* the Plugin settings.
*
* @since 1.7.0
*/
public function testAccessTokenRefreshedAndSavedWhenExpired()
{
// Add connection with "expired" token.
wpforms_update_providers_options(
'convertkit',
array(
'access_token' => $_ENV['CONVERTKIT_OAUTH_ACCESS_TOKEN'],
'refresh_token' => $_ENV['CONVERTKIT_OAUTH_REFRESH_TOKEN'],
'token_expires' => time(),
'label' => 'ConvertKit WordPress',
'date' => time(),
),
'wpunittest1234'
);

// 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();

// Confirm "new" tokens now exist in the Plugin's settings, which confirms the `convertkit_api_refresh_token` hook was called when
// the tokens were refreshed.
$providers = wpforms_get_providers_options();
$this->assertArrayHasKey('convertkit', $providers);

// Get first integration for ConvertKit, and confirm it has the expected array structure and values.
$account = reset( $providers['convertkit'] );
$this->assertArrayHasKey('access_token', $account);
$this->assertArrayHasKey('refresh_token', $account);
$this->assertArrayHasKey('label', $account);
$this->assertArrayHasKey('date', $account);
$this->assertEquals('newAccessToken', $account['access_token']);
$this->assertEquals('newRefreshToken', $account['refresh_token']);
}

/**
* Mocks an API response as if the Access Token expired.
*
* @since 1.7.0
*
* @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 1.7.0
*
* @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' => 'newAccessToken',
'refresh_token' => 'newRefreshToken',
'token_type' => 'bearer',
'created_at' => strtotime( 'now' ),
'expires_in' => 10000,
'scope' => 'public',
)
),
'response' => array(
'code' => 200,
'message' => 'OK',
),
'cookies' => array(),
'http_response' => null,
);
}

/**
* Test that the User Agent string is in the expected format and
* includes the Plugin's name and version number.
Expand Down
Loading