From e8da87a5680d03a13e2e72cd8ad1631dd391b799 Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Fri, 10 Apr 2026 18:06:23 +0800 Subject: [PATCH 1/8] Reovke and Remove Tokens on Disconnect --- composer.json | 2 +- .../class-integrate-convertkit-wpforms.php | 25 ++++++ includes/functions.php | 75 +++++++++++++++++ tests/EndToEnd/general/IntegrationsCest.php | 83 ++++++++++++++++--- tests/Integration/APITest.php | 71 ++++++++++++++++ tests/Support/Helper/WPForms.php | 6 +- 6 files changed, 247 insertions(+), 15 deletions(-) diff --git a/composer.json b/composer.json index 99b621c..7a4850e 100644 --- a/composer.json +++ b/composer.json @@ -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", diff --git a/includes/class-integrate-convertkit-wpforms.php b/includes/class-integrate-convertkit-wpforms.php index b87e7e1..023c4cf 100644 --- a/includes/class-integrate-convertkit-wpforms.php +++ b/includes/class-integrate-convertkit-wpforms.php @@ -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 ); diff --git a/includes/functions.php b/includes/functions.php index e322f5c..5477ee8 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -148,9 +148,84 @@ function integrate_convertkit_wpforms_maybe_delete_credentials( $result, $client } +/** + * 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 1.9.2 + * + * @param string $client_id OAuth Client ID used for the Access and Refresh Tokens. + * @param string $revoked_access_token Revoked Access Token. + */ +function integrate_convertkit_wpforms_delete_credentials( $client_id, $revoked_access_token ) { + + error_log( 'integrate_convertkit_wpforms_delete_credentials: ' . $client_id . ' - ' . $revoked_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(); + + error_log( print_r( $providers, true ) ); + + // Bail if no ConvertKit providers were registered. + if ( ! array_key_exists( 'convertkit', $providers ) ) { + return; + } + + error_log( print_r( $providers['convertkit'], true ) ); + + // Iterate through providers to find the specific connection containing the now revoked Access Token. + foreach ( $providers['convertkit'] as $id => $settings ) { + // Skip if this isn't the connection. + if ( $settings['access_token'] !== $revoked_access_token ) { + continue; + } + + error_log( 'id: ' . $id ); + + // Remove the invalid tokens from the connection. + // Keep the connection so the user doesn't lose settings on WPForms Forms. + // They can use the Reconnect link at WPForms > Settings > Integrations > Kit > Reconnect. + wpforms_update_providers_options( + 'convertkit', + array( + 'access_token' => '', + 'refresh_token' => '', + 'token_expires' => 0, + 'api_key' => '', + 'api_secret' => '', + 'label' => $settings['label'], + 'date' => time(), + ), + $id + ); + + // Clear any existing scheduled WordPress Cron event for this connection. + wp_clear_scheduled_hook( + 'integrate_convertkit_wpforms_refresh_token', + array( + $id, + ) + ); + + // Break out of the loop now the credentials have been removed. + break; + } + +} + // Update Access Token when refreshed by the API class. add_action( 'convertkit_api_refresh_token', 'integrate_convertkit_wpforms_maybe_update_credentials', 10, 3 ); +// Delete credentials when the user revokes the access and refresh tokens. +add_action( 'convertkit_api_revoke_tokens', 'integrate_convertkit_wpforms_delete_credentials', 10, 2 ); + // 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', 'integrate_convertkit_wpforms_maybe_delete_credentials', 10, 3 ); diff --git a/tests/EndToEnd/general/IntegrationsCest.php b/tests/EndToEnd/general/IntegrationsCest.php index 08858f6..d188af6 100644 --- a/tests/EndToEnd/general/IntegrationsCest.php +++ b/tests/EndToEnd/general/IntegrationsCest.php @@ -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:'); } /** @@ -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. @@ -178,6 +167,74 @@ 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) + { + // 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' + ); + + $providers = $I->grabOptionFromDatabase('wpforms_providers'); + var_dump($providers); + die(); + + // 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's credentials are removed from the settings. + $providers = $I->grabOptionFromDatabase('wpforms_providers'); + $I->assertArrayHasKey('convertkit', $providers); + + var_dump($providers); + + // Get first integration for Kit, and confirm it has the expected array structure and values. + $account = reset( $providers['convertkit'] ); + var_dump($account); + die(); + + $I->assertEquals('', $account['access_token']); + $I->assertEquals('', $account['refresh_token']); + $I->assertEquals('', $account['token_expires']); + $I->assertEquals('', $account['api_key']); + $I->assertEquals('', $account['api_secret']); + + // Check cached resources are removed from the database on disconnection. + $I->dontSeeOptionInDatabase('integrate_convertkit_wpforms_custom_fields'); + $I->dontSeeOptionInDatabase('integrate_convertkit_wpforms_custom_fields_last_queried'); + $I->dontSeeOptionInDatabase('integrate_convertkit_wpforms_forms'); + $I->dontSeeOptionInDatabase('integrate_convertkit_wpforms_forms_last_queried'); + $I->dontSeeOptionInDatabase('integrate_convertkit_wpforms_sequences'); + $I->dontSeeOptionInDatabase('integrate_convertkit_wpforms_sequences_last_queried'); + $I->dontSeeOptionInDatabase('integrate_convertkit_wpforms_tags'); + $I->dontSeeOptionInDatabase('integrate_convertkit_wpforms_tags_last_queried'); + } + /** * 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 diff --git a/tests/Integration/APITest.php b/tests/Integration/APITest.php index 81ab917..3b58015 100644 --- a/tests/Integration/APITest.php +++ b/tests/Integration/APITest.php @@ -200,6 +200,77 @@ 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. + ); + + // Store the access token in the Plugin's settings. + wpforms_update_providers_options( + 'convertkit', + array( + 'access_token' => $result['oauth']['access_token'], + 'refresh_token' => $result['oauth']['refresh_token'], + 'token_expires' => $result['oauth']['expires_at'], + 'label' => 'Kit WordPress', + 'date' => time(), + ), + 'wpunittest1234' + ); + + // 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(); + + // Confirm the access token and refresh token are deleted from the Plugin's settings. + $providers = wpforms_get_providers_options(); + $account = reset( $providers['convertkit'] ); + $this->assertEmpty( $account['access_token'] ); + $this->assertEmpty( $account['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 \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. * diff --git a/tests/Support/Helper/WPForms.php b/tests/Support/Helper/WPForms.php index 1e34ce2..f9d2c64 100644 --- a/tests/Support/Helper/WPForms.php +++ b/tests/Support/Helper/WPForms.php @@ -17,10 +17,12 @@ class WPForms extends \Codeception\Module * @param EndToEndTester $I Tester. * @param bool|string $accessToken Access Token (if not specified, CONVERTKIT_OAUTH_ACCESS_TOKEN is used). * @param bool|string $refreshToken Refresh Token (if not specified, CONVERTKIT_OAUTH_REFRESH_TOKEN is used). + * @param bool|string $apiKey API Key (if not specified, CONVERTKIT_API_KEY is used). + * @param bool|string $apiSecret API Secret (if not specified, CONVERTKIT_API_SECRET is used). * @param string $accountID Kit Account ID. * @return string Account ID in WPForms. */ - public function setupWPFormsIntegration($I, $accessToken = false, $refreshToken = false, $accountID = false) + public function setupWPFormsIntegration($I, $accessToken = false, $refreshToken = false, $apiKey = false, $apiSecret = false, $accountID = false) { $accountID = 'kit-' . ( $accountID ? $accountID : $_ENV['CONVERTKIT_API_ACCOUNT_ID'] ); @@ -31,6 +33,8 @@ public function setupWPFormsIntegration($I, $accessToken = false, $refreshToken $accountID => [ 'access_token' => $accessToken ? $accessToken : $_ENV['CONVERTKIT_OAUTH_ACCESS_TOKEN'], 'refresh_token' => $refreshToken ? $refreshToken : $_ENV['CONVERTKIT_OAUTH_REFRESH_TOKEN'], + 'api_key' => $apiKey ? $apiKey : $_ENV['CONVERTKIT_API_KEY'], + 'api_secret' => $apiSecret ? $apiSecret : $_ENV['CONVERTKIT_API_SECRET'], 'label' => 'Kit', 'date' => strtotime('now'), ], From 5a5ca0d6f5f8205329ed120503f68ceadb0167af Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Fri, 10 Apr 2026 18:15:10 +0800 Subject: [PATCH 2/8] =?UTF-8?q?Don=E2=80=99t=20add=20`convertkit=5Fapi=5Fr?= =?UTF-8?q?evoke=5Ftokens`=20action?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit WPForms provides the ‘Disconnect’ functionality, and it will always remove the connection (which includes API Key, Secret, Tokens etc). There’s no need to go and remove these settings via listening to the `convertkit_api_revoke_tokens` action. Updates tests to confirm token revokation works and WPForms truly removes the credentials upstream. --- includes/functions.php | 75 --------------------- tests/EndToEnd/general/IntegrationsCest.php | 40 ++++------- tests/Integration/APITest.php | 19 ------ 3 files changed, 13 insertions(+), 121 deletions(-) diff --git a/includes/functions.php b/includes/functions.php index 5477ee8..e322f5c 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -148,84 +148,9 @@ function integrate_convertkit_wpforms_maybe_delete_credentials( $result, $client } -/** - * 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 1.9.2 - * - * @param string $client_id OAuth Client ID used for the Access and Refresh Tokens. - * @param string $revoked_access_token Revoked Access Token. - */ -function integrate_convertkit_wpforms_delete_credentials( $client_id, $revoked_access_token ) { - - error_log( 'integrate_convertkit_wpforms_delete_credentials: ' . $client_id . ' - ' . $revoked_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(); - - error_log( print_r( $providers, true ) ); - - // Bail if no ConvertKit providers were registered. - if ( ! array_key_exists( 'convertkit', $providers ) ) { - return; - } - - error_log( print_r( $providers['convertkit'], true ) ); - - // Iterate through providers to find the specific connection containing the now revoked Access Token. - foreach ( $providers['convertkit'] as $id => $settings ) { - // Skip if this isn't the connection. - if ( $settings['access_token'] !== $revoked_access_token ) { - continue; - } - - error_log( 'id: ' . $id ); - - // Remove the invalid tokens from the connection. - // Keep the connection so the user doesn't lose settings on WPForms Forms. - // They can use the Reconnect link at WPForms > Settings > Integrations > Kit > Reconnect. - wpforms_update_providers_options( - 'convertkit', - array( - 'access_token' => '', - 'refresh_token' => '', - 'token_expires' => 0, - 'api_key' => '', - 'api_secret' => '', - 'label' => $settings['label'], - 'date' => time(), - ), - $id - ); - - // Clear any existing scheduled WordPress Cron event for this connection. - wp_clear_scheduled_hook( - 'integrate_convertkit_wpforms_refresh_token', - array( - $id, - ) - ); - - // Break out of the loop now the credentials have been removed. - break; - } - -} - // Update Access Token when refreshed by the API class. add_action( 'convertkit_api_refresh_token', 'integrate_convertkit_wpforms_maybe_update_credentials', 10, 3 ); -// Delete credentials when the user revokes the access and refresh tokens. -add_action( 'convertkit_api_revoke_tokens', 'integrate_convertkit_wpforms_delete_credentials', 10, 2 ); - // 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', 'integrate_convertkit_wpforms_maybe_delete_credentials', 10, 3 ); diff --git a/tests/EndToEnd/general/IntegrationsCest.php b/tests/EndToEnd/general/IntegrationsCest.php index d188af6..f7d926e 100644 --- a/tests/EndToEnd/general/IntegrationsCest.php +++ b/tests/EndToEnd/general/IntegrationsCest.php @@ -176,19 +176,19 @@ public function testInvalidCredentials(EndToEndTester $I) */ 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' + apiSecret: 'fakeAPISecret', + accountID: $accountID ); - $providers = $I->grabOptionFromDatabase('wpforms_providers'); - var_dump($providers); - die(); - // Load WPForms > Settings > Integrations. $I->amOnAdminPage('admin.php?page=wpforms-settings&view=integrations'); @@ -207,32 +207,18 @@ public function testCredentialsAndResourcesAreDeletedOnDisconnect(EndToEndTester $I->wait(3); $I->dontSee('Connected on:'); - // Check connection's credentials are removed from the settings. + // 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); - - var_dump($providers); - - // Get first integration for Kit, and confirm it has the expected array structure and values. - $account = reset( $providers['convertkit'] ); - var_dump($account); - die(); - - $I->assertEquals('', $account['access_token']); - $I->assertEquals('', $account['refresh_token']); - $I->assertEquals('', $account['token_expires']); - $I->assertEquals('', $account['api_key']); - $I->assertEquals('', $account['api_secret']); + $I->assertCount(0, $providers['convertkit']); // Check cached resources are removed from the database on disconnection. - $I->dontSeeOptionInDatabase('integrate_convertkit_wpforms_custom_fields'); - $I->dontSeeOptionInDatabase('integrate_convertkit_wpforms_custom_fields_last_queried'); - $I->dontSeeOptionInDatabase('integrate_convertkit_wpforms_forms'); - $I->dontSeeOptionInDatabase('integrate_convertkit_wpforms_forms_last_queried'); - $I->dontSeeOptionInDatabase('integrate_convertkit_wpforms_sequences'); - $I->dontSeeOptionInDatabase('integrate_convertkit_wpforms_sequences_last_queried'); - $I->dontSeeOptionInDatabase('integrate_convertkit_wpforms_tags'); - $I->dontSeeOptionInDatabase('integrate_convertkit_wpforms_tags_last_queried'); + $I->dontSeeOptionInDatabase('integrate_convertkit_wpforms_custom_fields_' . $accountID); + $I->dontSeeOptionInDatabase('integrate_convertkit_wpforms_forms_' . $accountID); + $I->dontSeeOptionInDatabase('integrate_convertkit_wpforms_sequences_' . $accountID); + $I->dontSeeOptionInDatabase('integrate_convertkit_wpforms_tags_' . $accountID); } /** diff --git a/tests/Integration/APITest.php b/tests/Integration/APITest.php index 3b58015..40acb43 100644 --- a/tests/Integration/APITest.php +++ b/tests/Integration/APITest.php @@ -221,19 +221,6 @@ public function testCredentialsDeletedAndInvalidWhenRevoked() 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. - wpforms_update_providers_options( - 'convertkit', - array( - 'access_token' => $result['oauth']['access_token'], - 'refresh_token' => $result['oauth']['refresh_token'], - 'token_expires' => $result['oauth']['expires_at'], - 'label' => 'Kit WordPress', - 'date' => time(), - ), - 'wpunittest1234' - ); - // Initialize the API with the access token and refresh token. $api = new \Integrate_ConvertKit_WPForms_API( $_ENV['CONVERTKIT_OAUTH_CLIENT_ID'], @@ -248,12 +235,6 @@ public function testCredentialsDeletedAndInvalidWhenRevoked() // Revoke the access and refresh tokens. $api->revoke_tokens(); - // Confirm the access token and refresh token are deleted from the Plugin's settings. - $providers = wpforms_get_providers_options(); - $account = reset( $providers['convertkit'] ); - $this->assertEmpty( $account['access_token'] ); - $this->assertEmpty( $account['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. From 70474ab963c6bbdcc3d2b1758ab8408eedac5ace Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Fri, 10 Apr 2026 18:55:31 +0800 Subject: [PATCH 3/8] =?UTF-8?q?Tests:=20Delete,=20don=E2=80=99t=20disconne?= =?UTF-8?q?ct,=20accounts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/EndToEnd/forms/FormCest.php | 8 +++---- tests/EndToEnd/general/IntegrationsCest.php | 5 +---- tests/Support/Helper/WPForms.php | 25 +++++---------------- 3 files changed, 9 insertions(+), 29 deletions(-) diff --git a/tests/EndToEnd/forms/FormCest.php b/tests/EndToEnd/forms/FormCest.php index ba53690..dee7cf9 100644 --- a/tests/EndToEnd/forms/FormCest.php +++ b/tests/EndToEnd/forms/FormCest.php @@ -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); } /** diff --git a/tests/EndToEnd/general/IntegrationsCest.php b/tests/EndToEnd/general/IntegrationsCest.php index f7d926e..808f37d 100644 --- a/tests/EndToEnd/general/IntegrationsCest.php +++ b/tests/EndToEnd/general/IntegrationsCest.php @@ -215,10 +215,7 @@ public function testCredentialsAndResourcesAreDeletedOnDisconnect(EndToEndTester $I->assertCount(0, $providers['convertkit']); // Check cached resources are removed from the database on disconnection. - $I->dontSeeOptionInDatabase('integrate_convertkit_wpforms_custom_fields_' . $accountID); - $I->dontSeeOptionInDatabase('integrate_convertkit_wpforms_forms_' . $accountID); - $I->dontSeeOptionInDatabase('integrate_convertkit_wpforms_sequences_' . $accountID); - $I->dontSeeOptionInDatabase('integrate_convertkit_wpforms_tags_' . $accountID); + $I->dontSeeCachedResourcesInDatabase($I, $this->accountID); } /** diff --git a/tests/Support/Helper/WPForms.php b/tests/Support/Helper/WPForms.php index f9d2c64..3608579 100644 --- a/tests/Support/Helper/WPForms.php +++ b/tests/Support/Helper/WPForms.php @@ -565,32 +565,17 @@ public function createPageWithWPFormsShortcode($I, $formID) } /** - * Disconnects the given account ID via the UI in WPForms > Settings > Integrations. + * Removes the given account ID from the WPForms provider settings. * * @since 1.7.0 * * @param EndToEndTester $I Tester. * @param string $accountID Account ID. */ - public function disconnectAccount($I, $accountID) + public function removeProviderConnection($I, $accountID) { - // Login as the Administrator, if we're not already logged in. - if ( ! $I->amLoggedInAsAdmin($I) ) { - $I->doLoginAsAdmin($I); - } - - // Click Disconnect. - $I->amOnAdminPage('admin.php?page=wpforms-settings&view=integrations'); - $I->click('#wpforms-integration-convertkit'); - $I->waitForElementVisible('#wpforms-integration-convertkit .wpforms-settings-provider-accounts-list span.remove a[data-key="' . $accountID . '"]'); - $I->click('#wpforms-integration-convertkit .wpforms-settings-provider-accounts-list span.remove a[data-key="' . $accountID . '"]'); - - // Confirm that we want to disconnect. - $I->waitForElementVisible('.jconfirm-box'); - $I->click('.jconfirm-box button.btn-confirm'); - - // Confirm connection is no longer listed. - $I->wait(5); - $I->dontSeeElementInDOM('a[data-key="' . $accountID . '"]'); + $providers = $I->grabOptionFromDatabase('wpforms_providers'); + unset($providers['convertkit'][ $accountID ]); + $I->haveOptionInDatabase('wpforms_providers', $providers); } } From a110c61cfb6f008f0064345d43c1d9786bc760be Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Fri, 10 Apr 2026 19:11:41 +0800 Subject: [PATCH 4/8] Revoke and Remove Tokens on Uninstall --- .github/workflows/tests.yml | 1 + tests/EndToEnd/uninstall/UninstallCest.php | 102 +++++++++++++++++++++ tests/Support/Helper/Plugin.php | 13 +++ tests/Support/Helper/ThirdPartyPlugin.php | 33 +++++++ uninstall.php | 83 +++++++++++++++++ 5 files changed, 232 insertions(+) create mode 100644 tests/EndToEnd/uninstall/UninstallCest.php create mode 100644 uninstall.php diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 1bc543f..f5ca0aa 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -59,6 +59,7 @@ jobs: 'EndToEnd/forms', 'EndToEnd/general', 'EndToEnd/recommendations', + 'EndToEnd/uninstall', 'Integration' ] diff --git a/tests/EndToEnd/uninstall/UninstallCest.php b/tests/EndToEnd/uninstall/UninstallCest.php new file mode 100644 index 0000000..48aa1d1 --- /dev/null +++ b/tests/EndToEnd/uninstall/UninstallCest.php @@ -0,0 +1,102 @@ +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'] ); + } +} \ No newline at end of file diff --git a/tests/Support/Helper/Plugin.php b/tests/Support/Helper/Plugin.php index 2aece54..57fab38 100644 --- a/tests/Support/Helper/Plugin.php +++ b/tests/Support/Helper/Plugin.php @@ -35,6 +35,19 @@ public function deactivateConvertKitPlugin($I) $I->deactivateThirdPartyPlugin($I, 'integrate-convertkit-wpforms'); } + /** + * Helper method to delete the Kit Plugin, checking + * it deleted and no errors were output. + * + * @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. diff --git a/tests/Support/Helper/ThirdPartyPlugin.php b/tests/Support/Helper/ThirdPartyPlugin.php index edf0ac5..9ca7f73 100644 --- a/tests/Support/Helper/ThirdPartyPlugin.php +++ b/tests/Support/Helper/ThirdPartyPlugin.php @@ -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. * diff --git a/uninstall.php b/uninstall.php new file mode 100644 index 0000000..b8ea19d --- /dev/null +++ b/uninstall.php @@ -0,0 +1,83 @@ + Delete. + * + * @package CKWC + * @author ConvertKit + */ + +// If uninstall.php is not called by WordPress, die. +if ( ! defined( 'WP_UNINSTALL_PLUGIN' ) ) { + die; +} + +// Only WordPress and PHP methods can be used. Plugin classes and methods +// are not reliably available due to the Plugin being deactivated and going +// through deletion now. + +// Get providers. +$providers = get_option( 'wpforms_providers' ); + +// Bail if no providers exist. +if ( ! $providers ) { + return; +} + +// Bail if no Kit connections exist. +if ( ! array_key_exists( 'convertkit', $providers ) ) { + return; +} + +// Iterate through each connection, revoking the tokens. +foreach ( $providers['convertkit'] as $account_id =>$connection ) { + // Revoke Access Token. + if ( array_key_exists( 'access_token', $connection ) && ! empty( $connection['access_token'] ) ) { + wp_remote_post( + 'https://api.kit.com/v4/oauth/revoke', + array( + 'headers' => array( + 'Accept' => 'application/json', + 'Content-Type' => 'application/json', + ), + 'body' => wp_json_encode( + array( + 'client_id' => 'L0kyADsB3WP5zO5MvUpXQU64gIntQg9BBAIme17r_7A', + 'token' => $connection['access_token'], + ) + ), + 'timeout' => 5, + ) + ); + } + + // Revoke Refresh Token. + if ( array_key_exists( 'refresh_token', $connection ) && ! empty( $connection['refresh_token'] ) ) { + wp_remote_post( + 'https://api.kit.com/v4/oauth/revoke', + array( + 'headers' => array( + 'Accept' => 'application/json', + 'Content-Type' => 'application/json', + ), + 'body' => wp_json_encode( + array( + 'client_id' => 'L0kyADsB3WP5zO5MvUpXQU64gIntQg9BBAIme17r_7A', + 'token' => $connection['refresh_token'], + ) + ), + 'timeout' => 5, + ) + ); + } + + // Remove credentials from settings. + $providers['convertkit'][ $account_id ]['access_token'] = ''; + $providers['convertkit'][ $account_id ]['refresh_token'] = ''; + $providers['convertkit'][ $account_id ]['token_expires'] = ''; + $providers['convertkit'][ $account_id ]['api_key'] = ''; + $providers['convertkit'][ $account_id ]['api_secret'] = ''; +} + +// Save settings. +update_option( 'wpforms_providers', $providers ); \ No newline at end of file From 7f2facbf92a9d8aeeac44b33ab4b2635b48c068a Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Fri, 10 Apr 2026 19:13:04 +0800 Subject: [PATCH 5/8] Fix test --- tests/EndToEnd/general/IntegrationsCest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/EndToEnd/general/IntegrationsCest.php b/tests/EndToEnd/general/IntegrationsCest.php index 808f37d..5d38354 100644 --- a/tests/EndToEnd/general/IntegrationsCest.php +++ b/tests/EndToEnd/general/IntegrationsCest.php @@ -215,7 +215,7 @@ public function testCredentialsAndResourcesAreDeletedOnDisconnect(EndToEndTester $I->assertCount(0, $providers['convertkit']); // Check cached resources are removed from the database on disconnection. - $I->dontSeeCachedResourcesInDatabase($I, $this->accountID); + $I->dontSeeCachedResourcesInDatabase($I, $accountID); } /** From 4fc10cea34d6a650662e91c59cd92a565cda0296 Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Fri, 10 Apr 2026 19:18:29 +0800 Subject: [PATCH 6/8] Run UninstallCest --- .github/workflows/tests.yml | 8 +- tests/EndToEnd/uninstall/UninstallCest.php | 15 ++-- uninstall.php | 92 +++++++++++----------- 3 files changed, 56 insertions(+), 59 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index f5ca0aa..35775d9 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -52,15 +52,11 @@ jobs: fail-fast: false matrix: wp-versions: [ 'latest' ] #[ '6.1.1', 'latest' ] - php-versions: [ '8.1', '8.2', '8.3', '8.4' ] #[ '7.3', '7.4', '8.0', '8.1' ] + php-versions: [ '8.1' ] #[ '7.3', '7.4', '8.0', '8.1' ] # Folder names within the 'tests' folder to run tests in parallel. test-groups: [ - 'EndToEnd/forms', - 'EndToEnd/general', - 'EndToEnd/recommendations', - 'EndToEnd/uninstall', - 'Integration' + 'EndToEnd/uninstall' ] # Steps to install, configure and run tests diff --git a/tests/EndToEnd/uninstall/UninstallCest.php b/tests/EndToEnd/uninstall/UninstallCest.php index 48aa1d1..83aa463 100644 --- a/tests/EndToEnd/uninstall/UninstallCest.php +++ b/tests/EndToEnd/uninstall/UninstallCest.php @@ -46,7 +46,7 @@ public function testPluginDeletionRevokesAndRemovesTokens(EndToEndTester $I) $tokens = json_decode(wp_remote_retrieve_body($result), true)['oauth']; // Store the tokens and API keys in the Plugin's settings. - $I->setupConvertKitPlugin( + $I->setupWPFormsIntegration( $I, accessToken: $tokens['access_token'], refreshToken: $tokens['refresh_token'], @@ -62,11 +62,12 @@ public function testPluginDeletionRevokesAndRemovesTokens(EndToEndTester $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']); + $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( @@ -99,4 +100,4 @@ public function testPluginDeletionRevokesAndRemovesTokens(EndToEndTester $I) $I->assertArrayHasKey( 'error', $data ); $I->assertEquals( 'invalid_grant', $data['error'] ); } -} \ No newline at end of file +} diff --git a/uninstall.php b/uninstall.php index b8ea19d..84f2cce 100644 --- a/uninstall.php +++ b/uninstall.php @@ -30,54 +30,54 @@ } // Iterate through each connection, revoking the tokens. -foreach ( $providers['convertkit'] as $account_id =>$connection ) { - // Revoke Access Token. - if ( array_key_exists( 'access_token', $connection ) && ! empty( $connection['access_token'] ) ) { - wp_remote_post( - 'https://api.kit.com/v4/oauth/revoke', - array( - 'headers' => array( - 'Accept' => 'application/json', - 'Content-Type' => 'application/json', - ), - 'body' => wp_json_encode( - array( - 'client_id' => 'L0kyADsB3WP5zO5MvUpXQU64gIntQg9BBAIme17r_7A', - 'token' => $connection['access_token'], - ) - ), - 'timeout' => 5, - ) - ); - } +foreach ( $providers['convertkit'] as $account_id => $connection ) { + // Revoke Access Token. + if ( array_key_exists( 'access_token', $connection ) && ! empty( $connection['access_token'] ) ) { + wp_remote_post( + 'https://api.kit.com/v4/oauth/revoke', + array( + 'headers' => array( + 'Accept' => 'application/json', + 'Content-Type' => 'application/json', + ), + 'body' => wp_json_encode( + array( + 'client_id' => 'L0kyADsB3WP5zO5MvUpXQU64gIntQg9BBAIme17r_7A', + 'token' => $connection['access_token'], + ) + ), + 'timeout' => 5, + ) + ); + } - // Revoke Refresh Token. - if ( array_key_exists( 'refresh_token', $connection ) && ! empty( $connection['refresh_token'] ) ) { - wp_remote_post( - 'https://api.kit.com/v4/oauth/revoke', - array( - 'headers' => array( - 'Accept' => 'application/json', - 'Content-Type' => 'application/json', - ), - 'body' => wp_json_encode( - array( - 'client_id' => 'L0kyADsB3WP5zO5MvUpXQU64gIntQg9BBAIme17r_7A', - 'token' => $connection['refresh_token'], - ) - ), - 'timeout' => 5, - ) - ); - } + // Revoke Refresh Token. + if ( array_key_exists( 'refresh_token', $connection ) && ! empty( $connection['refresh_token'] ) ) { + wp_remote_post( + 'https://api.kit.com/v4/oauth/revoke', + array( + 'headers' => array( + 'Accept' => 'application/json', + 'Content-Type' => 'application/json', + ), + 'body' => wp_json_encode( + array( + 'client_id' => 'L0kyADsB3WP5zO5MvUpXQU64gIntQg9BBAIme17r_7A', + 'token' => $connection['refresh_token'], + ) + ), + 'timeout' => 5, + ) + ); + } - // Remove credentials from settings. - $providers['convertkit'][ $account_id ]['access_token'] = ''; - $providers['convertkit'][ $account_id ]['refresh_token'] = ''; - $providers['convertkit'][ $account_id ]['token_expires'] = ''; - $providers['convertkit'][ $account_id ]['api_key'] = ''; - $providers['convertkit'][ $account_id ]['api_secret'] = ''; + // Remove credentials from settings. + $providers['convertkit'][ $account_id ]['access_token'] = ''; + $providers['convertkit'][ $account_id ]['refresh_token'] = ''; + $providers['convertkit'][ $account_id ]['token_expires'] = ''; + $providers['convertkit'][ $account_id ]['api_key'] = ''; + $providers['convertkit'][ $account_id ]['api_secret'] = ''; } // Save settings. -update_option( 'wpforms_providers', $providers ); \ No newline at end of file +update_option( 'wpforms_providers', $providers ); From 7cabf01cc891ac932bce0b8c584549204b198865 Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Fri, 10 Apr 2026 19:29:13 +0800 Subject: [PATCH 7/8] Reinstate all tests --- .github/workflows/tests.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 35775d9..f5ca0aa 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -52,11 +52,15 @@ jobs: fail-fast: false matrix: wp-versions: [ 'latest' ] #[ '6.1.1', 'latest' ] - php-versions: [ '8.1' ] #[ '7.3', '7.4', '8.0', '8.1' ] + php-versions: [ '8.1', '8.2', '8.3', '8.4' ] #[ '7.3', '7.4', '8.0', '8.1' ] # Folder names within the 'tests' folder to run tests in parallel. test-groups: [ - 'EndToEnd/uninstall' + 'EndToEnd/forms', + 'EndToEnd/general', + 'EndToEnd/recommendations', + 'EndToEnd/uninstall', + 'Integration' ] # Steps to install, configure and run tests From 4538844d4e7499f098960be7be85f664b1ba604c Mon Sep 17 00:00:00 2001 From: Tim Carr Date: Mon, 13 Apr 2026 11:13:45 +0800 Subject: [PATCH 8/8] Fix docblock comment --- tests/Support/Helper/Plugin.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/Support/Helper/Plugin.php b/tests/Support/Helper/Plugin.php index 57fab38..4492308 100644 --- a/tests/Support/Helper/Plugin.php +++ b/tests/Support/Helper/Plugin.php @@ -36,8 +36,7 @@ public function deactivateConvertKitPlugin($I) } /** - * Helper method to delete the Kit Plugin, checking - * it deleted and no errors were output. + * Helper method to delete the Kit Plugin. * * @since 1.9.2 *