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');
}
}