diff --git a/admin/class-convertkit-mm-admin.php b/admin/class-convertkit-mm-admin.php index 6d81ac4..d46cd54 100644 --- a/admin/class-convertkit-mm-admin.php +++ b/admin/class-convertkit-mm-admin.php @@ -203,8 +203,7 @@ private function check_credentials() { // Remove from settings. $this->settings->delete_credentials(); - // Redirect to General screen, which will now show the ConvertKit_Settings_OAuth screen, because - // the Plugin has no access token. + // Reload settings screen, to reflect no credentials exist. wp_safe_redirect( add_query_arg( array( diff --git a/convertkit-membermouse.php b/convertkit-membermouse.php index 55166f7..24e5c44 100644 --- a/convertkit-membermouse.php +++ b/convertkit-membermouse.php @@ -54,6 +54,7 @@ // Load plugin files. require CONVERTKIT_MM_PATH . 'includes/class-convertkit-mm-actions.php'; +require CONVERTKIT_MM_PATH . 'includes/class-convertkit-mm-admin-notices.php'; require CONVERTKIT_MM_PATH . 'includes/class-convertkit-mm-api.php'; require CONVERTKIT_MM_PATH . 'includes/class-convertkit-mm-resource.php'; require CONVERTKIT_MM_PATH . 'includes/class-convertkit-mm-resource-custom-fields.php'; diff --git a/includes/class-convertkit-mm-admin-notices.php b/includes/class-convertkit-mm-admin-notices.php new file mode 100644 index 0000000..59c3f4d --- /dev/null +++ b/includes/class-convertkit-mm-admin-notices.php @@ -0,0 +1,196 @@ +key_prefix ); + if ( ! $notices ) { + return; + } + + // Output notices. + foreach ( $notices as $notice ) { + switch ( $notice ) { + case 'authorization_failed': + $api = new ConvertKit_MM_API( CONVERTKIT_MM_OAUTH_CLIENT_ID, CONVERTKIT_MM_OAUTH_CLIENT_REDIRECT_URI ); + $output = sprintf( + '%s %s', + esc_html__( 'Kit for MemberMouse: Authorization failed. Please', 'convertkit-mm' ), + sprintf( + '%s', + esc_url( $api->get_oauth_url( admin_url( 'options-general.php?page=convertkit-mm' ), get_site_url() ) ), + esc_html__( 'connect your Kit account.', 'convertkit-mm' ) + ) + ); + break; + + default: + $output = ''; + + /** + * Define the text to output in an admin error notice. + * + * @since 1.3.7 + * + * @param string $notice Admin notice name. + */ + $output = apply_filters( CONVERTKIT_MM_NAME . '_admin_notices_output_' . $notice, $output ); + break; + } + + // If no output defined, skip. + if ( empty( $output ) ) { + continue; + } + ?> +
+

+ +

+
+ exist() ) { + return update_option( $this->key_prefix, array( $notice ) ); + } + + // Fetch existing persistent notices. + $notices = $this->get(); + + // Add notice to existing notices. + $notices[] = $notice; + + // Remove any duplicate notices. + $notices = array_values( array_unique( $notices ) ); + + // Update and return. + return update_option( $this->key_prefix, $notices ); + + } + + /** + * Returns all notices stored in the options table. + * + * @since 1.3.7 + * + * @return array + */ + public function get() { + + // Fetch all notices from the options table. + return get_option( $this->key_prefix ); + + } + + /** + * Whether any persistent notices are stored in the option table. + * + * @since 1.3.7 + * + * @return bool + */ + public function exist() { + + if ( ! $this->get() ) { + return false; + } + + return true; + + } + + /** + * Delete all persistent notices. + * + * @since 1.3.7 + * + * @param string $notice Notice name. + * @return bool Success + */ + public function delete( $notice ) { + + // If no persistent notices exist, there's nothing to delete. + if ( ! $this->exist() ) { + return false; + } + + // Fetch existing persistent notices. + $notices = $this->get(); + + // Remove notice from existing notices. + $index = array_search( $notice, $notices, true ); + if ( $index !== false ) { + unset( $notices[ $index ] ); + } + + // Update and return. + return update_option( $this->key_prefix, $notices ); + + } + +} diff --git a/includes/class-convertkit-mm-api.php b/includes/class-convertkit-mm-api.php index a80d00e..510cca9 100644 --- a/includes/class-convertkit-mm-api.php +++ b/includes/class-convertkit-mm-api.php @@ -1,15 +1,15 @@ last_queried = get_option( $this->settings_name . '_last_queried' ); + $this->resources = get_option( $this->settings_name ); + + } + + /** + * Fetches resources (custom fields, forms, sequences or tags) from the API, storing them in the options table + * with a last queried timestamp. + * + * If the refresh results in a 401, removes the access and refresh tokens from the settings. + * + * @since 1.3.7 + * + * @return WP_Error|array + */ + public function refresh() { + + // Call parent refresh method. + $result = parent::refresh(); + + // If an error occured, maybe delete credentials from the Plugin's settings + // if the error is a 401 unauthorized. + if ( is_wp_error( $result ) ) { + convertkit_mm_maybe_delete_credentials( $result ); + } + + return $result; } diff --git a/includes/class-convertkit-mm-settings.php b/includes/class-convertkit-mm-settings.php index 3d5ee07..8791d9e 100644 --- a/includes/class-convertkit-mm-settings.php +++ b/includes/class-convertkit-mm-settings.php @@ -49,10 +49,6 @@ public function __construct() { $this->settings = array_merge( $this->get_defaults(), $settings ); } - // 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 ); - } /** @@ -122,6 +118,9 @@ public function has_api_key() { */ public function get_access_token() { + // Reload settings from options table, to ensure we have the latest tokens. + $this->refresh_settings(); + // Return Access Token from settings. return $this->settings['access_token']; @@ -149,6 +148,9 @@ public function has_access_token() { */ public function get_refresh_token() { + // Reload settings from options table, to ensure we have the latest tokens. + $this->refresh_settings(); + // Return Refresh Token from settings. return $this->settings['refresh_token']; @@ -287,16 +289,13 @@ public function get_bundle_cancellation_mapping( $id ) { * * @since 1.3.0 * - * @param array $result New Access Token, Refresh Token and Expiry. - * @param string $client_id OAuth Client ID used for the Access and Refresh Tokens. + * @param array $result New Access Token, Refresh Token and Expiry. */ - public function update_credentials( $result, $client_id ) { + public function update_credentials( $result ) { - // 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 !== CONVERTKIT_MM_OAUTH_CLIENT_ID ) { - return; - } + // Remove any existing persistent notice. + $admin_notices = new ConvertKit_MM_Admin_Notices(); + $admin_notices->delete( 'authorization_failed' ); $this->save( array( @@ -329,6 +328,9 @@ public function delete_credentials() { ) ); + // Clear any existing scheduled WordPress Cron event. + wp_clear_scheduled_hook( 'convertkit_mm_refresh_token' ); + } /** @@ -410,7 +412,25 @@ public function save( $settings ) { update_option( self::SETTINGS_NAME, array_merge( $this->get(), $settings ) ); // Reload settings in class, to reflect changes. - $this->settings = get_option( self::SETTINGS_NAME ); + $this->refresh_settings(); + + } + + /** + * Reloads settings from the options table so this instance has the latest values. + * + * @since 1.3.7 + */ + private function refresh_settings() { + + $settings = get_option( self::SETTINGS_NAME ); + + if ( ! $settings ) { + $this->settings = $this->get_defaults(); + return; + } + + $this->settings = array_merge( $this->get_defaults(), $settings ); } diff --git a/includes/class-convertkit-mm.php b/includes/class-convertkit-mm.php index 382c3ab..a2be708 100644 --- a/includes/class-convertkit-mm.php +++ b/includes/class-convertkit-mm.php @@ -55,7 +55,7 @@ class ConvertKit_MM { public function __construct() { // Initialize. - add_action( 'init', array( $this, 'init' ) ); + add_action( 'init', array( $this, 'init' ), 1 ); } @@ -85,7 +85,8 @@ private function initialize_admin() { return; } - $this->classes['admin'] = new ConvertKit_MM_Admin(); + $this->classes['admin'] = new ConvertKit_MM_Admin(); + $this->classes['admin_notices'] = new ConvertKit_MM_Admin_Notices(); /** * Initialize integration classes for the WordPress Administration interface. diff --git a/includes/convertkit-mm-functions.php b/includes/convertkit-mm-functions.php index 3f3aa31..d2c3830 100644 --- a/includes/convertkit-mm-functions.php +++ b/includes/convertkit-mm-functions.php @@ -31,3 +31,68 @@ function convertkit_mm_log( $log, $message ) { fclose( $log ); // phpcs:ignore WordPress.WP.AlternativeFunctions } + +/** + * 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.7 + * + * @param array $result New Access Token, Refresh Token and Expiry. + * @param string $client_id OAuth Client ID used for the Access and Refresh Tokens. + */ +function convertkit_mm_maybe_update_credentials( $result, $client_id ) { + + // Don't save these credentials if they're not for this Client ID. + // They're for another Kit Plugin that uses OAuth. + if ( $client_id !== CONVERTKIT_MM_OAUTH_CLIENT_ID ) { + return; + } + + $settings = new ConvertKit_MM_Settings(); + $settings->update_credentials( $result ); + +} + +/** + * 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 either: + * - The access token is invalid + * - The access token expired, and refreshing failed + * + * @since 1.3.7 + * + * @param WP_Error $result Error result. + * @param string $client_id OAuth Client ID used for the Access and Refresh Tokens. + */ +function convertkit_mm_maybe_delete_credentials( $result, $client_id ) { + + // Don't save these credentials if they're not for this Client ID. + // They're for another Kit Plugin that uses OAuth. + if ( $client_id !== CONVERTKIT_MM_OAUTH_CLIENT_ID ) { + return; + } + + // If the error isn't a 401, don't delete credentials. + // This could be e.g. a temporary network error, rate limit or similar. + if ( $result->get_error_data( 'convertkit_api_error' ) !== 401 ) { + return; + } + + // Persist an error notice in the WordPress Administration until the user fixes the problem. + $admin_notices = new ConvertKit_MM_Admin_Notices(); + $admin_notices->add( 'authorization_failed' ); + + $settings = new ConvertKit_MM_Settings(); + $settings->delete_credentials(); + +} + +// Update Access Token when refreshed by the API class. +add_action( 'convertkit_api_get_access_token', 'convertkit_mm_maybe_update_credentials', 10, 2 ); +add_action( 'convertkit_api_refresh_token', 'convertkit_mm_maybe_update_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', 'convertkit_mm_maybe_delete_credentials', 10, 2 ); diff --git a/tests/EndToEnd/general/SettingsCest.php b/tests/EndToEnd/general/SettingsCest.php index e344e98..f73c22c 100644 --- a/tests/EndToEnd/general/SettingsCest.php +++ b/tests/EndToEnd/general/SettingsCest.php @@ -90,6 +90,9 @@ public function testInvalidCredentials(EndToEndTester $I) $I->see('Connect'); $I->dontSee('Disconnect'); $I->dontSeeElementInDOM('input#submit'); + + // Check that a notice is displayed that the API credentials are invalid. + $I->seeErrorNotice($I, 'Kit for MemberMouse: Authorization failed. Please connect your Kit account.'); } /** @@ -122,6 +125,15 @@ public function testValidCredentials(EndToEndTester $I) $I->see('Disconnect'); $I->seeElementInDOM('input#submit'); + // Navigate to the WordPress Admin. + $I->amOnAdminPage('index.php'); + + // Check that no notice is displayed that the API credentials are invalid. + $I->dontSeeErrorNotice($I, 'Kit for MemberMouse: Authorization failed. Please connect your Kit account.'); + + // Go to the Plugin's Settings Screen. + $I->amOnAdminPage('options-general.php?page=convertkit-mm'); + // Disconnect the Plugin connection to ConvertKit. $I->click('Disconnect'); diff --git a/tests/Integration/ResourceCustomFieldsNoDataTest.php b/tests/Integration/ResourceCustomFieldsNoDataTest.php new file mode 100644 index 0000000..112e253 --- /dev/null +++ b/tests/Integration/ResourceCustomFieldsNoDataTest.php @@ -0,0 +1,162 @@ +settings = new \ConvertKit_MM_Settings(); + update_option( + $this->settings::SETTINGS_NAME, + [ + 'access_token' => $_ENV['CONVERTKIT_OAUTH_ACCESS_TOKEN_NO_DATA'], + 'refresh_token' => $_ENV['CONVERTKIT_OAUTH_REFRESH_TOKEN_NO_DATA'], + ] + ); + + // Initialize the resource class we want to test. + $this->resource = new \ConvertKit_MM_Resource_Custom_Fields(); + + // Confirm initialization didn't result in an error. + $this->assertNotInstanceOf(\WP_Error::class, $this->resource->resources); + + // Initialize the resource class, fetching resources from the API and caching them in the options table. + $result = $this->resource->init(); + + // Confirm calling init() didn't result in an error. + $this->assertNotInstanceOf(\WP_Error::class, $result); + } + + /** + * Performs actions after each test. + * + * @since 1.3.7 + */ + public function tearDown(): void + { + // Delete Credentials and Resources from Plugin's settings. + delete_option($this->settings::SETTINGS_NAME); + delete_option($this->resource->settings_name); + delete_option($this->resource->settings_name . '_last_queried'); + + // Destroy the resource class we tested. + unset($this->resource); + + // Deactivate Plugin. + deactivate_plugins('convertkit/wp-convertkit.php'); + + parent::tearDown(); + } + + /** + * Test that the refresh() function performs as expected. + * + * @since 1.3.7 + */ + public function testRefresh() + { + // Confirm that no resources exist in the stored options table data. + $result = $this->resource->refresh(); + $this->assertNotInstanceOf(\WP_Error::class, $result); + $this->assertIsArray($result); + $this->assertCount(0, $result); + } + + /** + * Test that the expiry timestamp is set and returns the expected value. + * + * @since 1.3.7 + */ + public function testExpiry() + { + // Define the expected expiry date based on the resource class' $cache_duration setting. + $expectedExpiryDate = date('Y-m-d', time() + $this->resource->cache_duration); + + // Fetch the actual expiry date set when the resource class was initialized. + $expiryDate = date('Y-m-d', $this->resource->last_queried + $this->resource->cache_duration); + + // Confirm both dates match. + $this->assertEquals($expectedExpiryDate, $expiryDate); + } + + /** + * Test that the get() function performs as expected. + * + * @since 1.3.7 + */ + public function testGet() + { + // Confirm that no resources exist in the stored options table data. + $result = $this->resource->get(); + $this->assertNotInstanceOf(\WP_Error::class, $result); + $this->assertIsArray($result); + $this->assertCount(0, $result); + } + + /** + * Test that the count() function returns the number of resources. + * + * @since 1.3.7 + */ + public function testCount() + { + $result = $this->resource->get(); + $this->assertEquals($this->resource->count(), count($result)); + } + + /** + * Test that the exist() function performs as expected. + * + * @since 1.3.7 + */ + public function testExist() + { + // Confirm that the function returns false, because resources do not exist. + $result = $this->resource->exist(); + $this->assertSame($result, false); + } +} diff --git a/tests/Integration/ResourceCustomFieldsTest.php b/tests/Integration/ResourceCustomFieldsTest.php new file mode 100644 index 0000000..db22ec0 --- /dev/null +++ b/tests/Integration/ResourceCustomFieldsTest.php @@ -0,0 +1,242 @@ +settings = new \ConvertKit_MM_Settings(); + update_option( + $this->settings::SETTINGS_NAME, + [ + 'access_token' => $_ENV['CONVERTKIT_OAUTH_ACCESS_TOKEN'], + 'refresh_token' => $_ENV['CONVERTKIT_OAUTH_REFRESH_TOKEN'], + ] + ); + + // Initialize the resource class we want to test. + $this->resource = new \ConvertKit_MM_Resource_Custom_Fields(); + + // Confirm initialization didn't result in an error. + $this->assertNotInstanceOf(\WP_Error::class, $this->resource->resources); + + // Initialize the resource class, fetching resources from the API and caching them in the options table. + $result = $this->resource->init(); + + // Confirm calling init() didn't result in an error. + $this->assertNotInstanceOf(\WP_Error::class, $result); + } + + /** + * Performs actions after each test. + * + * @since 1.3.7 + */ + public function tearDown(): void + { + // Delete Credentials and Resources from Plugin's settings. + delete_option($this->settings::SETTINGS_NAME); + delete_option($this->resource->settings_name); + delete_option($this->resource->settings_name . '_last_queried'); + + // Destroy the resource class we tested. + unset($this->resource); + + // Deactivate Plugin. + deactivate_plugins('convertkit/wp-convertkit.php'); + + parent::tearDown(); + } + + /** + * Test that the refresh() function performs as expected. + * + * @since 1.3.7 + */ + public function testRefresh() + { + // Confirm that the data is stored in the options table and includes some expected keys. + $result = $this->resource->refresh(); + $this->assertIsArray($result); + $this->assertArrayHasKey('id', reset($result)); + $this->assertArrayHasKey('name', reset($result)); + } + + /** + * Test that the expiry timestamp is set and returns the expected value. + * + * @since 1.3.7 + */ + public function testExpiry() + { + // Define the expected expiry date based on the resource class' $cache_duration setting. + $expectedExpiryDate = date('Y-m-d', time() + $this->resource->cache_duration); + + // Fetch the actual expiry date set when the resource class was initialized. + $expiryDate = date('Y-m-d', $this->resource->last_queried + $this->resource->cache_duration); + + // Confirm both dates match. + $this->assertEquals($expectedExpiryDate, $expiryDate); + } + + /** + * Tests that the get() function returns resources in alphabetical ascending order + * by default. + * + * @since 1.3.7 + */ + public function testGet() + { + // Call resource class' get() function. + $result = $this->resource->get(); + + // Assert result is an array. + $this->assertIsArray($result); + + // Assert top level array keys are preserved. + $this->assertArrayHasKey(array_key_first($this->resource->resources), $result); + $this->assertArrayHasKey(array_key_last($this->resource->resources), $result); + + // Assert resource within results has expected array keys. + $this->assertArrayHasKey('id', reset($result)); + $this->assertArrayHasKey('name', reset($result)); + $this->assertArrayHasKey('key', reset($result)); + $this->assertArrayHasKey('label', reset($result)); + + // Assert order of data is in ascending alphabetical order. + $this->assertEquals('Billing Address', reset($result)[ $this->resource->order_by ]); + $this->assertEquals('URL', end($result)[ $this->resource->order_by ]); + } + + /** + * Tests that the get() function returns resources in alphabetical descending order + * when a valid order_by and order properties are defined. + * + * @since 1.3.7 + */ + public function testGetWithValidOrderByAndOrder() + { + // Define order_by and order. + $this->resource->order_by = 'key'; + $this->resource->order = 'desc'; + + // Call resource class' get() function. + $result = $this->resource->get(); + + // Assert result is an array. + $this->assertIsArray($result); + + // Assert top level array keys are preserved. + $this->assertArrayHasKey(array_key_first($this->resource->resources), $result); + $this->assertArrayHasKey(array_key_last($this->resource->resources), $result); + + // Assert resource within results has expected array keys. + $this->assertArrayHasKey('id', reset($result)); + $this->assertArrayHasKey('name', reset($result)); + $this->assertArrayHasKey('key', reset($result)); + $this->assertArrayHasKey('label', reset($result)); + + // Assert order of data is in descending alphabetical order. + $this->assertEquals('url', reset($result)[ $this->resource->order_by ]); + $this->assertEquals('billing_address', end($result)[ $this->resource->order_by ]); + } + + /** + * Tests that the get() function returns resources in their original order + * when populated with Forms and an invalid order_by value is specified. + * + * @since 1.3.7 + */ + public function testGetWithInvalidOrderBy() + { + // Define order_by with an invalid value (i.e. an array key that does not exist). + $this->resource->order_by = 'invalid_key'; + + // Call resource class' get() function. + $result = $this->resource->get(); + + // Assert result is an array. + $this->assertIsArray($result); + + // Assert top level array keys are preserved. + $this->assertArrayHasKey(array_key_first($this->resource->resources), $result); + $this->assertArrayHasKey(array_key_last($this->resource->resources), $result); + + // Assert resource within results has expected array keys. + $this->assertArrayHasKey('id', reset($result)); + $this->assertArrayHasKey('name', reset($result)); + $this->assertArrayHasKey('key', reset($result)); + $this->assertArrayHasKey('label', reset($result)); + + // Assert order of data has not changed. + $this->assertEquals('URL', reset($result)['label']); + $this->assertEquals('Notes', end($result)['label']); + } + + /** + * Test that the count() function returns the number of resources. + * + * @since 1.3.7 + */ + public function testCount() + { + $result = $this->resource->get(); + $this->assertEquals($this->resource->count(), count($result)); + } + + /** + * Test that the exist() function performs as expected. + * + * @since 1.3.7 + */ + public function testExist() + { + // Confirm that the function returns true, because resources exist. + $result = $this->resource->exist(); + $this->assertSame($result, true); + } +} diff --git a/tests/Integration/ResourceTagsNoDataTest.php b/tests/Integration/ResourceTagsNoDataTest.php new file mode 100644 index 0000000..7ded369 --- /dev/null +++ b/tests/Integration/ResourceTagsNoDataTest.php @@ -0,0 +1,162 @@ +settings = new \ConvertKit_MM_Settings(); + update_option( + $this->settings::SETTINGS_NAME, + [ + 'access_token' => $_ENV['CONVERTKIT_OAUTH_ACCESS_TOKEN_NO_DATA'], + 'refresh_token' => $_ENV['CONVERTKIT_OAUTH_REFRESH_TOKEN_NO_DATA'], + ] + ); + + // Initialize the resource class we want to test. + $this->resource = new \ConvertKit_MM_Resource_Tags(); + + // Confirm initialization didn't result in an error. + $this->assertNotInstanceOf(\WP_Error::class, $this->resource->resources); + + // Initialize the resource class, fetching resources from the API and caching them in the options table. + $result = $this->resource->init(); + + // Confirm calling init() didn't result in an error. + $this->assertNotInstanceOf(\WP_Error::class, $result); + } + + /** + * Performs actions after each test. + * + * @since 1.3.7 + */ + public function tearDown(): void + { + // Delete Credentials and Resources from Plugin's settings. + delete_option($this->settings::SETTINGS_NAME); + delete_option($this->resource->settings_name); + delete_option($this->resource->settings_name . '_last_queried'); + + // Destroy the resource class we tested. + unset($this->resource); + + // Deactivate Plugin. + deactivate_plugins('convertkit/wp-convertkit.php'); + + parent::tearDown(); + } + + /** + * Test that the refresh() function performs as expected. + * + * @since 1.3.7 + */ + public function testRefresh() + { + // Confirm that the data is stored in the options table and includes some expected keys. + $result = $this->resource->refresh(); + $this->assertNotInstanceOf(\WP_Error::class, $result); + $this->assertIsArray($result); + $this->assertCount(0, $result); + } + + /** + * Test that the expiry timestamp is set and returns the expected value. + * + * @since 1.3.7 + */ + public function testExpiry() + { + // Define the expected expiry date based on the resource class' $cache_duration setting. + $expectedExpiryDate = date('Y-m-d', time() + $this->resource->cache_duration); + + // Fetch the actual expiry date set when the resource class was initialized. + $expiryDate = date('Y-m-d', $this->resource->last_queried + $this->resource->cache_duration); + + // Confirm both dates match. + $this->assertEquals($expectedExpiryDate, $expiryDate); + } + + /** + * Test that the get() function performs as expected. + * + * @since 1.3.7 + */ + public function testGet() + { + // Confirm that the data is fetched from the options table when using get(), and includes some expected keys. + $result = $this->resource->get(); + $this->assertNotInstanceOf(\WP_Error::class, $result); + $this->assertIsArray($result); + $this->assertCount(0, $result); + } + + /** + * Test that the count() function returns the number of resources. + * + * @since 1.3.7 + */ + public function testCount() + { + $result = $this->resource->get(); + $this->assertEquals($this->resource->count(), count($result)); + } + + /** + * Test that the exist() function performs as expected. + * + * @since 1.3.7 + */ + public function testExist() + { + // Confirm that the function returns true, because resources exist. + $result = $this->resource->exist(); + $this->assertSame($result, false); + } +} diff --git a/tests/Integration/ResourceTagsTest.php b/tests/Integration/ResourceTagsTest.php new file mode 100644 index 0000000..a5a0806 --- /dev/null +++ b/tests/Integration/ResourceTagsTest.php @@ -0,0 +1,236 @@ +settings = new \ConvertKit_MM_Settings(); + update_option( + $this->settings::SETTINGS_NAME, + [ + 'access_token' => $_ENV['CONVERTKIT_OAUTH_ACCESS_TOKEN'], + 'refresh_token' => $_ENV['CONVERTKIT_OAUTH_REFRESH_TOKEN'], + ] + ); + + // Initialize the resource class we want to test. + $this->resource = new \ConvertKit_MM_Resource_Tags(); + + // Confirm initialization didn't result in an error. + $this->assertNotInstanceOf(\WP_Error::class, $this->resource->resources); + + // Initialize the resource class, fetching resources from the API and caching them in the options table. + $result = $this->resource->init(); + + // Confirm calling init() didn't result in an error. + $this->assertNotInstanceOf(\WP_Error::class, $result); + } + + /** + * Performs actions after each test. + * + * @since 1.3.7 + */ + public function tearDown(): void + { + // Delete Credentials and Resources from Plugin's settings. + delete_option($this->settings::SETTINGS_NAME); + delete_option($this->resource->settings_name); + delete_option($this->resource->settings_name . '_last_queried'); + + // Destroy the resource class we tested. + unset($this->resource); + + // Deactivate Plugin. + deactivate_plugins('convertkit/wp-convertkit.php'); + + parent::tearDown(); + } + + /** + * Test that the refresh() function performs as expected. + * + * @since 1.3.7 + */ + public function testRefresh() + { + // Confirm that the data is stored in the options table and includes some expected keys. + $result = $this->resource->refresh(); + $this->assertIsArray($result); + $this->assertArrayHasKey('id', reset($result)); + $this->assertArrayHasKey('name', reset($result)); + } + + /** + * Test that the expiry timestamp is set and returns the expected value. + * + * @since 1.3.7 + */ + public function testExpiry() + { + // Define the expected expiry date based on the resource class' $cache_duration setting. + $expectedExpiryDate = date('Y-m-d', time() + $this->resource->cache_duration); + + // Fetch the actual expiry date set when the resource class was initialized. + $expiryDate = date('Y-m-d', $this->resource->last_queried + $this->resource->cache_duration); + + // Confirm both dates match. + $this->assertEquals($expectedExpiryDate, $expiryDate); + } + + /** + * Tests that the get() function returns resources in alphabetical ascending order + * by default. + * + * @since 1.3.7 + */ + public function testGet() + { + // Call resource class' get() function. + $result = $this->resource->get(); + + // Assert result is an array. + $this->assertIsArray($result); + + // Assert top level array keys are preserved. + $this->assertArrayHasKey(array_key_first($this->resource->resources), $result); + $this->assertArrayHasKey(array_key_last($this->resource->resources), $result); + + // Assert resource within results has expected array keys. + $this->assertArrayHasKey('id', reset($result)); + $this->assertArrayHasKey('name', reset($result)); + + // Assert order of data is in ascending alphabetical order. + $this->assertEquals('gravityforms-tag-1', reset($result)[ $this->resource->order_by ]); + $this->assertEquals('wpforms', end($result)[ $this->resource->order_by ]); + } + + /** + * Tests that the get() function returns resources in alphabetical descending order + * when a valid order_by and order properties are defined. + * + * @since 1.3.7 + */ + public function testGetWithValidOrderByAndOrder() + { + // Define order_by and order. + $this->resource->order_by = 'name'; + $this->resource->order = 'desc'; + + // Call resource class' get() function. + $result = $this->resource->get(); + + // Assert result is an array. + $this->assertIsArray($result); + + // Assert top level array keys are preserved. + $this->assertArrayHasKey(array_key_first($this->resource->resources), $result); + $this->assertArrayHasKey(array_key_last($this->resource->resources), $result); + + // Assert resource within results has expected array keys. + $this->assertArrayHasKey('id', reset($result)); + $this->assertArrayHasKey('name', reset($result)); + + // Assert order of data is in ascending alphabetical order. + $this->assertEquals('wpforms', reset($result)[ $this->resource->order_by ]); + $this->assertEquals('gravityforms-tag-1', end($result)[ $this->resource->order_by ]); + } + + /** + * Tests that the get() function returns resources in their original order + * when populated with Forms and an invalid order_by value is specified. + * + * @since 1.3.7 + */ + public function testGetWithInvalidOrderBy() + { + // Define order_by with an invalid value (i.e. an array key that does not exist). + $this->resource->order_by = 'invalid_key'; + + // Call resource class' get() function. + $result = $this->resource->get(); + + // Assert result is an array. + $this->assertIsArray($result); + + // Assert top level array keys are preserved. + $this->assertArrayHasKey(array_key_first($this->resource->resources), $result); + $this->assertArrayHasKey(array_key_last($this->resource->resources), $result); + + // Assert resource within results has expected array keys. + $this->assertArrayHasKey('id', reset($result)); + $this->assertArrayHasKey('name', reset($result)); + + // Assert order of data has not changed. + $this->assertEquals('wpforms', reset($result)['name']); + $this->assertEquals('wordpress', end($result)['name']); + } + + /** + * Test that the count() function returns the number of resources. + * + * @since 1.3.7 + */ + public function testCount() + { + $result = $this->resource->get(); + $this->assertEquals($this->resource->count(), count($result)); + } + + /** + * Test that the exist() function performs as expected. + * + * @since 1.3.7 + */ + public function testExist() + { + // Confirm that the function returns true, because resources exist. + $result = $this->resource->exist(); + $this->assertSame($result, true); + } +} diff --git a/tests/Support/Helper/Plugin.php b/tests/Support/Helper/Plugin.php index 6787fe3..1ccdc31 100644 --- a/tests/Support/Helper/Plugin.php +++ b/tests/Support/Helper/Plugin.php @@ -83,5 +83,14 @@ public function resetConvertKitPlugin($I) { // Plugin Settings. $I->dontHaveOptionInDatabase('convertkit-mm-options'); + + // Resources. + $I->dontHaveOptionInDatabase('convertkit-mm-custom-fields'); + $I->dontHaveOptionInDatabase('convertkit-mm-custom-fields_last_queried'); + $I->dontHaveOptionInDatabase('convertkit-mm-tags'); + $I->dontHaveOptionInDatabase('convertkit-mm-tags_last_queried'); + + // Persistent notices. + $I->dontHaveOptionInDatabase('convertkit-mm-admin-notices'); } }