Skip to content
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
3 changes: 2 additions & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,8 @@ jobs:
'EndToEnd/general',
'EndToEnd/member',
'EndToEnd/member-update',
'EndToEnd/product'
'EndToEnd/product',
'Integration'
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Integration tests weren't running on GitHub actions; this ensures they run.

]

# Steps to install, configure and run tests
Expand Down
2 changes: 1 addition & 1 deletion admin/class-convertkit-mm-admin.php
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ private function maybe_get_and_store_access_token() {
array(
'access_token' => $result['access_token'],
'refresh_token' => $result['refresh_token'],
'token_expires' => ( $result['created_at'] + $result['expires_in'] ),
'token_expires' => ( time() + $result['expires_in'] ),

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are you switching to this? Wouldn't this set the expiration to longer than it actually is?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

expires_in is the number of seconds before the access token expires, not a timestamp of when the access token expires, and the API has never returned a fixed value. Adding it to created_at results in the exact issue reported in this PR. Adding it to time() results in the correct calculation of the token's expiry. But if I'm missing something obvious, let me know.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@noelherrick Let me know if this needs another review. It's the same principle as the main Kit Plugin: Kit/convertkit-wordpress#840

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the reminder!

)
);

Expand Down
1 change: 1 addition & 0 deletions convertkit-membermouse.php
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
require CONVERTKIT_MM_PATH . 'includes/class-convertkit-mm-settings.php';
require CONVERTKIT_MM_PATH . 'includes/class-convertkit-mm.php';
require CONVERTKIT_MM_PATH . 'includes/convertkit-mm-functions.php';
require CONVERTKIT_MM_PATH . 'includes/cron-functions.php';
require CONVERTKIT_MM_PATH . 'admin/class-convertkit-mm-admin.php';

/**
Expand Down
13 changes: 10 additions & 3 deletions includes/class-convertkit-mm-settings.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ public function __construct() {
}

// Update Access Token when refreshed by the API class.
add_action( 'convertkit_api_get_access_token', array( $this, 'update_credentials' ), 10, 2 );
add_action( 'convertkit_api_refresh_token', array( $this, 'update_credentials' ), 10, 2 );

}
Expand Down Expand Up @@ -281,8 +282,8 @@ public function get_bundle_cancellation_mapping( $id ) {
}

/**
* Saves the new access token, refresh token and its expiry when the API
* class automatically refreshes an outdated access token.
* Saves the new access token, refresh token and its expiry, and schedules
* a WordPress Cron event to refresh the token on expiry.
*
* @since 1.3.0
*
Expand All @@ -301,10 +302,16 @@ public function update_credentials( $result, $client_id ) {
array(
'access_token' => $result['access_token'],
'refresh_token' => $result['refresh_token'],
'token_expires' => ( $result['created_at'] + $result['expires_in'] ),
'token_expires' => ( time() + $result['expires_in'] ),
)
);

// Clear any existing scheduled WordPress Cron event.
wp_clear_scheduled_hook( 'convertkit_mm_refresh_token' );

// Schedule a WordPress Cron event to refresh the token on expiry.
wp_schedule_single_event( ( time() + $result['expires_in'] ), 'convertkit_mm_refresh_token' );

}

/**
Expand Down
58 changes: 58 additions & 0 deletions includes/cron-functions.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<?php
/**
* WordPress Cron functions.
*
* @package ConvertKit_MM
* @author ConvertKit
*/

/**
* Refresh the OAuth access token, triggered by WordPress' Cron.
*
* @since 1.3.3
*/
function convertkit_mm_refresh_token() {

// Initialize settings class.
$settings = new ConvertKit_MM_Settings();

// Bail if no existing access and refresh token exists.
if ( ! $settings->has_access_token() ) {
return;
}
if ( ! $settings->has_refresh_token() ) {
return;
}

// Initialize the API.
$api = new ConvertKit_MM_API(
CONVERTKIT_OAUTH_CLIENT_ID,
CONVERTKIT_OAUTH_CLIENT_REDIRECT_URI,
$settings->get_access_token(),
$settings->get_refresh_token(),
$settings->debug_enabled(),
'cron_refresh_token'
);

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

// If an error occured, don't save the new tokens.
// Logging is handled by the ConvertKit_API_V4 class.
if ( is_wp_error( $result ) ) {
return;
}

$settings->save(
array(
'access_token' => $result['access_token'],
'refresh_token' => $result['refresh_token'],
'token_expires' => ( time() + $result['expires_in'] ),
)
);

}

// Register action to run above function; this action is created by WordPress' wp_schedule_event() function
// in update_credentials() in the ConvertKit_Settings class.
add_action( 'convertkit_mm_refresh_token', 'convertkit_mm_refresh_token' );
2 changes: 1 addition & 1 deletion tests/Integration.suite.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,5 @@ modules:
domain: '%WORDPRESS_DOMAIN%'
adminEmail: 'admin@%WORDPRESS_DOMAIN%'
title: 'Integration Tests'
plugins: ['./integrate-convertkit-wpforms.php']
plugins: ['./convertkit-membermouse.php']
theme: ''
50 changes: 45 additions & 5 deletions tests/Integration/APITest.php
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ public function testAccessTokenRefreshedAndSavedWhenExpired()

// 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 );
add_filter( 'pre_http_request', array( $this, 'mockTokenResponse' ), 10, 3 );

// Run request, which will trigger the above filters as if the token expired and refreshes automatically.
$result = $this->api->get_account();
Expand All @@ -89,6 +89,46 @@ public function testAccessTokenRefreshedAndSavedWhenExpired()
$this->assertEquals( $settings->get_refresh_token(), $_ENV['CONVERTKIT_OAUTH_REFRESH_TOKEN'] );
}

/**
* Test that a WordPress Cron event is created when an access token is obtained.
*
* @since 1.3.3
*/
public function testCronEventCreatedWhenAccessTokenObtained()
{
// Mock request as if the API returned an access and refresh token when a request
// was made to refresh the token.
add_filter( 'pre_http_request', array( $this, 'mockTokenResponse' ), 10, 3 );

// Run request, as if the access token was obtained successfully.
$result = $this->api->get_access_token( 'mockAuthCode' );

// Confirm the Cron event to refresh the access token was created, and the timestamp to
// run the refresh token call matches the expiry of the access token.
$nextScheduledTimestamp = wp_next_scheduled( 'convertkit_mm_refresh_token' );
$this->assertGreaterThanOrEqual( $nextScheduledTimestamp, time() + 10000 );
}

/**
* Test that a WordPress Cron event is created when an access token is refreshed.
*
* @since 1.3.3
*/
public function testCronEventCreatedWhenTokenRefreshed()
{
// Mock request as if the API returned an access and refresh token when a request
// was made to refresh the token.
add_filter( 'pre_http_request', array( $this, 'mockTokenResponse' ), 10, 3 );

// Run request, as if the token was refreshed.
$result = $this->api->refresh_token();

// Confirm the Cron event to refresh the access token was created, and the timestamp to
// run the refresh token call matches the expiry of the access token.
$nextScheduledTimestamp = wp_next_scheduled( 'convertkit_mm_refresh_token' );
$this->assertGreaterThanOrEqual( $nextScheduledTimestamp, time() + 10000 );
}

/**
* Mocks an API response as if the Access Token expired.
*
Expand Down Expand Up @@ -139,15 +179,15 @@ public function mockAccessTokenExpiredResponse( $response, $parsed_args, $url )
* @param string $url Request URL.
* @return mixed
*/
public function mockRefreshTokenResponse( $response, $parsed_args, $url )
public function mockTokenResponse( $response, $parsed_args, $url )
{
// Only mock requests made to the /token endpoint.
if ( strpos( $url, 'https://api.kit.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' ) );
remove_filter( 'pre_http_request', array( $this, 'mockTokenResponse' ) );

// 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,
Expand All @@ -159,8 +199,8 @@ public function mockRefreshTokenResponse( $response, $parsed_args, $url )
'access_token' => $_ENV['CONVERTKIT_OAUTH_ACCESS_TOKEN'],
'refresh_token' => $_ENV['CONVERTKIT_OAUTH_REFRESH_TOKEN'],
'token_type' => 'bearer',
'created_at' => strtotime( 'now' ),
'expires_in' => 10000,
'created_at' => 1735660800, // When the access token was created.
'expires_in' => 10000, // When the access token will expire, relative to the time the request was made.
'scope' => 'public',
)
),
Expand Down