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/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ jobs:
'EndToEnd/forms',
'EndToEnd/general',
'EndToEnd/recommendations',
'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
25 changes: 25 additions & 0 deletions includes/class-integrate-convertkit-wpforms.php
Original file line number Diff line number Diff line change
Expand Up @@ -962,6 +962,31 @@ public function delete_resource_cache() {
// Get API instance.
$api = $this->get_api_instance( $account_id );

// 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_send_json_error(
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.', 'integrate-convertkit-wpforms' ),
)
);
}

// Revoke Access and Refresh Tokens.
// See integrate_convertkit_wpforms_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();

// Bail if an error occurred.
if ( is_wp_error( $result ) ) {
wp_send_json_error(
array(
'error' => $result->get_error_message(),
)
);
}

// Delete cached resources.
$resource_forms = new Integrate_ConvertKit_WPForms_Resource_Forms( $api, $account_id );
$resource_sequences = new Integrate_ConvertKit_WPForms_Resource_Sequences( $api, $account_id );
Expand Down
8 changes: 3 additions & 5 deletions tests/EndToEnd/forms/FormCest.php
Original file line number Diff line number Diff line change
Expand Up @@ -812,11 +812,9 @@ private function _wpFormsCompleteAndSubmitForm(EndToEndTester $I, int $pageID, s
// Check that a review request was created.
$I->reviewRequestExists($I);

// Disconnect the account.
$I->disconnectAccount($I, $this->accountID);

// Check that the resources are no longer cached under the given account ID.
$I->dontSeeCachedResourcesInDatabase($I, $this->accountID);
// Remove the provider connection.
// We don't disconnect the account, as this would now revoke the tokens and cause later tests to fail.
$I->removeProviderConnection($I, $this->accountID);
}

/**
Expand Down
66 changes: 53 additions & 13 deletions tests/EndToEnd/general/IntegrationsCest.php
Original file line number Diff line number Diff line change
Expand Up @@ -91,17 +91,6 @@ public function testAddIntegrationWithValidCredentials(EndToEndTester $I)
),
$reconnectURL
);

// Confirm that the connection can be disconnected.
$I->click('Disconnect');

// Confirm that we want to disconnect.
$I->waitForElementVisible('.jconfirm-box');
$I->click('.jconfirm-box button.btn-confirm');

// Confirm no connection is listed.
$I->wait(3);
$I->dontSee('Connected on:');
}

/**
Expand Down Expand Up @@ -138,8 +127,8 @@ public function testInvalidCredentials(EndToEndTester $I)
// Define connection with invalid API credentials.
$I->setupWPFormsIntegration(
$I,
'fakeAccessToken',
'fakeRefreshToken'
accessToken: 'fakeAccessToken',
refreshToken: 'fakeRefreshToken'
);

// Setup WPForms Form and configuration for this test.
Expand Down Expand Up @@ -178,6 +167,57 @@ public function testInvalidCredentials(EndToEndTester $I)
$I->seeErrorNotice($I, 'Kit for WPForms: Authorization failed. Please reconnect your Kit account.');
}

/**
* Test that the credentials and resources are deleted on disconnect.
*
* @since 1.9.2
*
* @param EndToEndTester $I Tester.
*/
public function testCredentialsAndResourcesAreDeletedOnDisconnect(EndToEndTester $I)
{
// Define a random account ID.
$accountID = 'kit-' . wp_generate_password( 10, false );

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

// Load WPForms > Settings > Integrations.
$I->amOnAdminPage('admin.php?page=wpforms-settings&view=integrations');

// Expand Kit integration section.
$I->click('#wpforms-integration-convertkit');

// Disconnect the connection to Kit.
$I->waitForElementVisible('a[data-provider="convertkit"]');
$I->click('Disconnect');

// Confirm that we want to disconnect.
$I->waitForElementVisible('.jconfirm-box');
$I->click('.jconfirm-box button.btn-confirm');

// Confirm no connection is listed.
$I->wait(3);
$I->dontSee('Connected on:');

// Check connection is removed from the settings.
// Clicking 'Disconnect' in WPForms removes the connection from the settings,
// including any credentials within that connection.
$providers = $I->grabOptionFromDatabase('wpforms_providers');
$I->assertArrayHasKey('convertkit', $providers);
$I->assertCount(0, $providers['convertkit']);

// Check cached resources are removed from the database on disconnection.
$I->dontSeeCachedResourcesInDatabase($I, $accountID);
}

/**
* Deactivate and reset Plugin(s) after each test, if the test passes.
* We don't use _after, as this would provide a screenshot of the Plugin
Expand Down
103 changes: 103 additions & 0 deletions tests/EndToEnd/uninstall/UninstallCest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
<?php

namespace Tests\EndToEnd;

use Tests\Support\EndToEndTester;

/**
* Tests Plugin uninstallation.
*
* @since 1.9.2
*/
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 1.9.2
*
* @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->setupWPFormsIntegration(
$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('wpforms_providers');
$connection = reset($settings['convertkit']);
$I->assertEmpty($connection['access_token']);
$I->assertEmpty($connection['refresh_token']);
$I->assertEmpty($connection['api_key']);
$I->assertEmpty($connection['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'] );
}
}
52 changes: 52 additions & 0 deletions tests/Integration/APITest.php
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,58 @@ 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 1.9.2
*/
public function testCredentialsDeletedAndInvalidWhenRevoked()
{
// Initialize the API without an access token or refresh token.
$api = new \Integrate_ConvertKit_WPForms_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.
);

// Initialize the API with the access token and refresh token.
$api = new \Integrate_ConvertKit_WPForms_API(
$_ENV['CONVERTKIT_OAUTH_CLIENT_ID'],
$_ENV['KIT_OAUTH_REDIRECT_URI'],
$result['oauth']['access_token'],
$result['oauth']['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();

// 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 \Integrate_ConvertKit_WPForms_API(
$_ENV['CONVERTKIT_OAUTH_CLIENT_ID'],
$_ENV['KIT_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() );
}

/**
* Mocks an API response as if the Access Token expired.
*
Expand Down
12 changes: 12 additions & 0 deletions tests/Support/Helper/Plugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,18 @@ public function deactivateConvertKitPlugin($I)
$I->deactivateThirdPartyPlugin($I, 'integrate-convertkit-wpforms');
}

/**
* Helper method to delete the Kit Plugin.
*
* @since 1.9.2
*
* @param EndToEndTester $I EndToEndTester.
*/
public function deleteKitPlugin($I)
{
$I->deleteThirdPartyPlugin($I, 'integrate-convertkit-wpforms');
}

/**
* Helper method to determine that the order of the Form resources in the given
* select element are in the expected alphabetical order.
Expand Down
33 changes: 33 additions & 0 deletions tests/Support/Helper/ThirdPartyPlugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,39 @@ public function deactivateThirdPartyPlugin($I, $name)
$I->waitForElementVisible('table.plugins tr[data-slug=' . $name . '].inactive');
}

/**
* Helper method to delete a third party Plugin, checking
* it deleted and no errors were output.
*
* @since 1.9.2
*
* @param EndToEndTester $I EndToEnd Tester.
* @param string $name Plugin Slug.
*/
public function deleteThirdPartyPlugin($I, $name)
{
// Login as the Administrator, if we're not already logged in.
if ( ! $this->amLoggedInAsAdmin($I) ) {
$this->doLoginAsAdmin($I);
}

// Go to the Plugins screen in the WordPress Administration interface.
$I->amOnPluginsPage();

// Wait for the Plugins page to load.
$I->waitForElementVisible('body.plugins-php');

// Delete the Plugin.
$I->waitForElementVisible('a#delete-' . $name);
$I->click('a#delete-' . $name);

// Click the confirmation dialog.
$I->acceptPopup();

// Wait for the Plugin to be marked as deleted.
$I->waitForElementNotVisible('table.plugins tr.deleted[data-slug=' . $name . ']');
}

/**
* Helper method to check if the Administrator is logged in.
*
Expand Down
Loading
Loading