diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 57abdc1..34470b1 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -55,7 +55,8 @@ jobs: 'EndToEnd/general', 'EndToEnd/member', 'EndToEnd/member-update', - 'EndToEnd/product' + 'EndToEnd/product', + 'Integration' ] # Steps to install, configure and run tests diff --git a/admin/class-convertkit-mm-admin.php b/admin/class-convertkit-mm-admin.php index a08caaf..6d81ac4 100644 --- a/admin/class-convertkit-mm-admin.php +++ b/admin/class-convertkit-mm-admin.php @@ -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'] ), ) ); diff --git a/convertkit-membermouse.php b/convertkit-membermouse.php index c80bc27..07d5138 100644 --- a/convertkit-membermouse.php +++ b/convertkit-membermouse.php @@ -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'; /** diff --git a/includes/class-convertkit-mm-settings.php b/includes/class-convertkit-mm-settings.php index ab1b250..3d5ee07 100644 --- a/includes/class-convertkit-mm-settings.php +++ b/includes/class-convertkit-mm-settings.php @@ -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 ); } @@ -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 * @@ -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' ); + } /** diff --git a/includes/cron-functions.php b/includes/cron-functions.php new file mode 100644 index 0000000..6a8d12d --- /dev/null +++ b/includes/cron-functions.php @@ -0,0 +1,58 @@ +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' ); diff --git a/tests/Integration.suite.yml b/tests/Integration.suite.yml index ca29b4c..9229d2b 100644 --- a/tests/Integration.suite.yml +++ b/tests/Integration.suite.yml @@ -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: '' \ No newline at end of file diff --git a/tests/Integration/APITest.php b/tests/Integration/APITest.php index 17bcd22..71d345f 100644 --- a/tests/Integration/APITest.php +++ b/tests/Integration/APITest.php @@ -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(); @@ -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. * @@ -139,7 +179,7 @@ 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 ) { @@ -147,7 +187,7 @@ public function mockRefreshTokenResponse( $response, $parsed_args, $url ) } // 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, @@ -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', ) ),