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
1 change: 1 addition & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ jobs:
'EndToEnd/subscribe/order-pending-payment',
'EndToEnd/subscribe/order-processing',
'EndToEnd/sync-past-orders',
'EndToEnd/uninstall',
'Integration'
]

Expand Down
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": "2.1.3"
"convertkit/convertkit-wordpress-libraries": "2.1.5"
},
"require-dev": {
"php-webdriver/webdriver": "^1.0",
Expand Down
45 changes: 41 additions & 4 deletions includes/class-ckwc-integration.php
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,8 @@ public function delete_credentials() {
$this->update_option( 'access_token', '' );
$this->update_option( 'refresh_token', '' );
$this->update_option( 'token_expires', '' );
$this->update_option( 'api_key', '' );
$this->update_option( 'api_secret', '' );

// Clear any existing scheduled WordPress Cron event.
wp_clear_scheduled_hook( 'ckwc_refresh_token' );
Expand Down Expand Up @@ -171,11 +173,46 @@ public function maybe_disconnect() {
return;
}

// Delete resources.
$this->resources_delete();
// Setup API.
$api = new CKWC_API(
CKWC_OAUTH_CLIENT_ID,
CKWC_OAUTH_CLIENT_REDIRECT_URI,
$this->get_access_token(),
$this->get_refresh_token(),
$this->get_option_bool( 'debug' )
);

// Check that we're using the Kit WordPress Libraries 2.1.4 or higher.
// If another Kit Plugin is active and out of date, its libraries might
// be loaded that don't have this method.
if ( ! method_exists( $api, 'revoke_tokens' ) ) { // @phpstan-ignore-line Older WordPress Libraries won't have this function.
wp_safe_redirect(
ckwc_get_settings_link(
array(
'error' => __( 'The Kit WordPress Libraries is missing the `revoke_tokens` method. Please update all Kit WordPress Plugins to their latest versions, and click Disconnect again.', 'woocommerce-convertkit' ),
)
)
);
exit();
}

// Remove tokens from settings.
$this->delete_credentials();
// Revoke Access and Refresh Tokens.
// See ckwc_delete_credentials() method in functions.php, which is called
// by the `convertkit_api_revoke_tokens` action and deletes credentials from the Plugin's settings.
$result = $api->revoke_tokens();
if ( is_wp_error( $result ) ) {
wp_safe_redirect(
ckwc_get_settings_link(
array(
'error' => $result->get_error_message(),
)
)
);
exit();
}

// Delete cached resources.
$this->resources_delete();

// Redirect to General screen, which will now show the Plugin's settings, because the Plugin
// is now authenticated.
Expand Down
30 changes: 30 additions & 0 deletions includes/functions.php
Original file line number Diff line number Diff line change
Expand Up @@ -242,10 +242,40 @@ function ckwc_maybe_delete_credentials( $result, $client_id ) {

}

/**
* Deletes the stored access token, refresh token and its expiry from the Plugin settings,
* and clears any existing scheduled WordPress Cron event to refresh the token on expiry,
* when the user revokes the access token.
*
* @since 2.1.3
*
* @param string $client_id OAuth Client ID used for the Access and Refresh Tokens.
*/
function ckwc_delete_credentials( $client_id ) {

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

// Bail if the integration is unavailable.
if ( ! function_exists( 'WP_CKWC_Integration' ) ) {
return;
}

// Delete Access and Refresh Tokens.
WP_CKWC_Integration()->delete_credentials();

}

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

// Delete credentials when the user revokes the access and refresh tokens.
add_action( 'convertkit_api_revoke_tokens', 'ckwc_delete_credentials', 10, 1 );

// Delete credentials if the API class uses a invalid access token.
// This prevents the Plugin making repetitive API requests that will 401.
add_action( 'convertkit_api_access_token_invalid', 'ckwc_maybe_delete_credentials', 10, 2 );
52 changes: 47 additions & 5 deletions tests/EndToEnd/settings/SettingOAuthCest.php
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,8 @@ public function testInvalidCredentials(EndToEndTester $I)
// Setup Plugin.
$I->setupConvertKitPlugin(
$I,
'fakeAccessToken',
'fakeRefreshToken'
accessToken: 'fakeAccessToken',
refreshToken: 'fakeRefreshToken'
);

// Load Settings screen.
Expand Down Expand Up @@ -138,15 +138,57 @@ public function testValidCredentials(EndToEndTester $I)

// Wait for confirmation message to display.
$I->waitForElementVisible('div.updated.inline');
}

/**
* Test that the credentials and resources are deleted on disconnect.
*
* @since 2.1.3
*
* @param EndToEndTester $I Tester.
*/
public function testCredentialsAndResourcesAreDeletedOnDisconnect(EndToEndTester $I)
{
// Setup Plugin.
$I->setupConvertKitPlugin($I);

// Disconnect the Plugin connection to ConvertKit.
// Load Settings screen.
$I->loadConvertKitSettingsScreen($I);

// Fake the API Key, Access and Refresh Tokens; if we revoke the tokens used for tests, future tests will fail.
$I->setupConvertKitPlugin(
$I,
accessToken: 'fakeAccessToken',
refreshToken: 'fakeRefreshToken',
apiKey: 'fakeAPIKey',
apiSecret: 'fakeAPISecret'
);

// Disconnect the Plugin connection to Kit.
$I->click('Disconnect');

// Check credentials are removed from the settings.
$settings = $I->grabOptionFromDatabase('woocommerce_ckwc_settings');
$I->assertEmpty($settings['access_token']);
$I->assertEmpty($settings['refresh_token']);
$I->assertEmpty($settings['token_expires']);
$I->assertEmpty($settings['api_key']);
$I->assertEmpty($settings['api_secret']);

// Check cached resources are removed from the database on disconnection.
$I->dontSeeOptionInDatabase('ckwc_custom_fields');
$I->dontSeeOptionInDatabase('ckwc_custom_fields_last_queried');
$I->dontSeeOptionInDatabase('ckwc_forms');
$I->dontSeeOptionInDatabase('ckwc_forms_last_queried');
$I->dontSeeOptionInDatabase('ckwc_sequences');
$I->dontSeeOptionInDatabase('ckwc_sequences_last_queried');
$I->dontSeeOptionInDatabase('ckwc_tags');
$I->dontSeeOptionInDatabase('ckwc_tags_last_queried');

// Confirm the Connect button displays.
$I->waitForElementVisible('#oauth a.button-primary');
$I->see('Connect');
$I->dontSee('Disconnect');
$I->dontSeeElementInDOM('button.woocommerce-save-button');
$I->dontSeeElementInDOM('input#submit');
}

/**
Expand Down
102 changes: 102 additions & 0 deletions tests/EndToEnd/uninstall/UninstallCest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
<?php

namespace Tests\EndToEnd;

use Tests\Support\EndToEndTester;

/**
* Tests Plugin uninstallation.
*
* @since 2.1.3
*/
class UninstallCest
{
/**
* Test that the Plugin's access and refresh tokens are revoked, and all v4 and v3
* API credentials are removed from the Plugin's settings when the Plugin is deleted.
*
* @since 2.1.3
*
* @param EndToEndTester $I Tester.
*/
public function testPluginDeletionRevokesAndRemovesTokens(EndToEndTester $I)
{
// Activate this Plugin.
$I->activateConvertKitPlugin($I);

// Generate an access token and refresh token by API key and secret.
// We don't use the tokens from the environment, as revoking those
// would result in later tests failing.
$result = wp_remote_post(
'https://api.kit.com/wordpress/accounts/oauth_access_token',
[
'headers' => [
'Content-Type' => 'application/json',
],
'body' => wp_json_encode(
[
'api_key' => $_ENV['CONVERTKIT_API_KEY'],
'api_secret' => $_ENV['CONVERTKIT_API_SECRET'],
'client_id' => $_ENV['CONVERTKIT_OAUTH_CLIENT_ID'],
'tenant_name' => wp_generate_password( 10, false ), // Random tenant name to produce a token for this request only.
]
),
]
);
$tokens = json_decode(wp_remote_retrieve_body($result), true)['oauth'];

// Store the tokens and API keys in the Plugin's settings.
$I->setupConvertKitPlugin(
$I,
accessToken: $tokens['access_token'],
refreshToken: $tokens['refresh_token'],
apiKey: $_ENV['CONVERTKIT_API_KEY'],
apiSecret: $_ENV['CONVERTKIT_API_SECRET']
);

// Deactivate the Plugin.
$I->deactivateConvertKitPlugin($I);

// Delete the Plugin.
$I->deleteKitPlugin($I);

// Confirm the credentials have been removed from the Plugin's settings.
$I->wait(3);
$settings = $I->grabOptionFromDatabase('woocommerce_ckwc_settings');
$I->assertEmpty($settings['access_token']);
$I->assertEmpty($settings['refresh_token']);
$I->assertEmpty($settings['api_key']);
$I->assertEmpty($settings['api_secret']);

// Confirm attempting to use the revoked access token no longer works.
$result = wp_remote_get(
'https://api.kit.com/v4/account',
[
'headers' => [
'Authorization' => 'Bearer ' . $tokens['access_token'],
],
]
);
$data = json_decode(wp_remote_retrieve_body($result), true);
$I->assertArrayHasKey( 'errors', $data );
$I->assertEquals( 'The access token was revoked', $data['errors'][0] );

// Confirm attempting to use the revoked refresh token no longer works.
$result = wp_remote_post(
'https://api.kit.com/v4/oauth/token',
[
'headers' => [
'Authorization' => 'Bearer ' . $tokens['access_token'],
],
'body' => [
'client_id' => $_ENV['CONVERTKIT_OAUTH_CLIENT_ID'],
'grant_type' => 'refresh_token',
'refresh_token' => $tokens['refresh_token'],
],
]
);
$data = json_decode(wp_remote_retrieve_body($result), true);
$I->assertArrayHasKey( 'error', $data );
$I->assertEquals( 'invalid_grant', $data['error'] );
}
}
61 changes: 61 additions & 0 deletions tests/Integration/APITest.php
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,67 @@ public function testCronEventCreatedWhenTokenRefreshed()
$this->assertGreaterThanOrEqual( $nextScheduledTimestamp, time() + 10000 );
}

/**
* Test that the access token and refresh token are deleted from the Plugin's settings
* when the access token is revoked.
*
* @since 2.1.3
*/
public function testCredentialsDeletedAndInvalidWhenRevoked()
{
// Initialize the API without an access token or refresh token.
$api = new \CKWC_API(
$_ENV['CONVERTKIT_OAUTH_CLIENT_ID'],
$_ENV['KIT_OAUTH_REDIRECT_URI']
);

// Generate an access token by API key and secret.
$result = $api->get_access_token_by_api_key_and_secret(
$_ENV['CONVERTKIT_API_KEY'],
$_ENV['CONVERTKIT_API_SECRET'],
wp_generate_password( 10, false ) // Random tenant name to produce a token for this request only.
);

// Store the access token in the Plugin's settings.
WP_CKWC_Integration()->update_option( 'access_token', $result['oauth']['access_token'] );
WP_CKWC_Integration()->update_option( 'refresh_token', $result['oauth']['refresh_token'] );
WP_CKWC_Integration()->update_option( 'token_expires', $result['oauth']['expires_at'] );

// Initialize the API with the access token and refresh token.
$api = new \CKWC_API(
$_ENV['CONVERTKIT_OAUTH_CLIENT_ID'],
$_ENV['KIT_OAUTH_REDIRECT_URI'],
WP_CKWC_Integration()->get_access_token(),
WP_CKWC_Integration()->get_refresh_token()
);

// Confirm the token works when making an authenticated request.
$this->assertNotInstanceOf( 'WP_Error', $api->get_account() );

// Revoke the access and refresh tokens.
$api->revoke_tokens();

// Confirm the access token and refresh token are deleted from the Plugin's settings.
$this->assertEmpty( WP_CKWC_Integration()->get_access_token() );
$this->assertEmpty( WP_CKWC_Integration()->get_refresh_token() );

// Initialize the API with the (now revoked) access token and refresh token.
// revoke_tokens() will have removed the access token and refresh token from the API class, so we need to provide them again
// to test they're revoked.
$api = new \CKWC_API(
$_ENV['CONVERTKIT_OAUTH_CLIENT_ID'],
$_ENV['CONVERTKIT_OAUTH_REDIRECT_URI'],
$result['oauth']['access_token'],
$result['oauth']['refresh_token']
);

// Confirm attempting to use the revoked access token no longer works.
$this->assertInstanceOf( 'WP_Error', $api->get_account() );

// Confirm attempting to use the revoked refresh token no longer works.
$this->assertInstanceOf( 'WP_Error', $api->refresh_token() );
}

/**
* Test that the User Agent string is in the expected format and
* includes the Plugin's name and version number.
Expand Down
17 changes: 17 additions & 0 deletions tests/Support/Helper/Plugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,19 @@ public function deactivateConvertKitPlugin($I)
$I->deactivateThirdPartyPlugin($I, 'convertkit-for-woocommerce');
}

/**
* Helper method to delete the Kit Plugin, checking
* it deleted and no errors were output.
*
* @since 2.1.3
*
* @param EndToEndTester $I EndToEndTester.
*/
public function deleteKitPlugin($I)
{
$I->deleteThirdPartyPlugin($I, 'convertkit-for-woocommerce');
}

/**
* Helper method to activate the following Plugins:
* - WooCommerce
Expand Down Expand Up @@ -93,6 +106,8 @@ public function deactivateWooCommerceAndConvertKitPlugins($I)
* @param EndToEndTester $I Acceptance Tester.
* @param bool|string $accessToken Access Token (if specified, used instead of CONVERTKIT_OAUTH_ACCESS_TOKEN).
* @param bool|string $refreshToken Refresh Token (if specified, used instead of CONVERTKIT_OAUTH_REFRESH_TOKEN).
* @param bool|string $apiKey v3 API Key.
* @param bool|string $apiSecret v3 API Secret.
* @param string $subscriptionEvent Subscribe Event.
* @param bool|string $subscription Form, Tag or Sequence to subscribe customer to.
* @param string $nameFormat Name Format.
Expand All @@ -108,6 +123,8 @@ public function setupConvertKitPlugin(
$I,
$accessToken = false,
$refreshToken = false,
$apiKey = false,
$apiSecret = false,
$subscriptionEvent = 'pending',
$subscription = false,
$nameFormat = 'first',
Expand Down
Loading