diff --git a/.env.dist.testing b/.env.dist.testing
new file mode 100644
index 0000000..ccdc27b
--- /dev/null
+++ b/.env.dist.testing
@@ -0,0 +1,20 @@
+TEST_SITE_DB_DSN=mysql:host=localhost;dbname=test
+TEST_SITE_DB_HOST=localhost
+TEST_SITE_DB_NAME=test
+TEST_SITE_DB_USER=root
+TEST_SITE_DB_PASSWORD=root
+TEST_SITE_TABLE_PREFIX=wp_
+TEST_SITE_ADMIN_USERNAME=admin
+TEST_SITE_ADMIN_PASSWORD=password
+TEST_SITE_WP_ADMIN_PATH=/wp-admin
+WP_ROOT_FOLDER="/home/runner/work/convertkit-membermouse/convertkit-membermouse/wordpress"
+TEST_DB_NAME=test
+TEST_DB_HOST=localhost
+TEST_DB_USER=root
+TEST_DB_PASSWORD=root
+TEST_TABLE_PREFIX=wp_
+TEST_SITE_WP_URL=http://127.0.0.1
+TEST_SITE_WP_DOMAIN=127.0.0.1
+TEST_SITE_ADMIN_EMAIL=wordpress@convertkit.local
+TEST_SITE_HTTP_USER_AGENT=HeadlessChrome
+TEST_SITE_HTTP_USER_AGENT_MOBILE=HeadlessChromeMobile
diff --git a/.env.example b/.env.example
new file mode 100644
index 0000000..11823e2
--- /dev/null
+++ b/.env.example
@@ -0,0 +1,24 @@
+TEST_SITE_DB_DSN=mysql:host=localhost;dbname=test
+TEST_SITE_DB_HOST=localhost
+TEST_SITE_DB_NAME=test
+TEST_SITE_DB_USER=root
+TEST_SITE_DB_PASSWORD=root
+TEST_SITE_TABLE_PREFIX=wp_
+TEST_SITE_ADMIN_USERNAME=admin
+TEST_SITE_ADMIN_PASSWORD=password
+TEST_SITE_WP_ADMIN_PATH=/wp-admin
+WP_ROOT_FOLDER="/Users/tim/Local Sites/convertkit-github/app/public"
+TEST_DB_NAME=test
+TEST_DB_HOST=localhost
+TEST_DB_USER=root
+TEST_DB_PASSWORD=root
+TEST_TABLE_PREFIX=wp_
+TEST_SITE_WP_URL=http://convertkit.local
+TEST_SITE_WP_DOMAIN=convertkit.local
+TEST_SITE_ADMIN_EMAIL=wordpress@convertkit.local
+TEST_SITE_HTTP_USER_AGENT=HeadlessChrome
+TEST_SITE_HTTP_USER_AGENT_MOBILE=HeadlessChromeMobile
+CONVERTKIT_API_KEY_NO_DATA=
+CONVERTKIT_API_SECRET_NO_DATA=
+CONVERTKIT_API_KEY=
+CONVERTKIT_API_SECRET=
\ No newline at end of file
diff --git a/.github/workflows/coding-standards.yml b/.github/workflows/coding-standards.yml
index bd998dc..3064255 100644
--- a/.github/workflows/coding-standards.yml
+++ b/.github/workflows/coding-standards.yml
@@ -111,6 +111,11 @@ jobs:
working-directory: ${{ env.PLUGIN_DIR }}
run: composer dump-autoload
+ # Run Coding Standards on Tests.
+ - name: Run Coding Standards on Tests
+ working-directory: ${{ env.PLUGIN_DIR }}
+ run: php vendor/bin/phpcs -q --standard=phpcs.tests.xml --report=checkstyle ./tests | cs2pr
+
# Run WordPress Coding Standards on Plugin.
- name: Run WordPress Coding Standards
working-directory: ${{ env.PLUGIN_DIR }}
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
new file mode 100644
index 0000000..e8a1f6c
--- /dev/null
+++ b/.github/workflows/tests.yml
@@ -0,0 +1,208 @@
+name: Run Tests
+
+# When to run tests.
+on:
+ pull_request:
+ types:
+ - opened
+ - synchronize
+ push:
+ branches:
+ - main
+
+jobs:
+ tests:
+ # Name.
+ name: ${{ matrix.test-groups }} / WordPress ${{ matrix.wp-versions }} / PHP ${{ matrix.php-versions }}
+
+ # Virtual Environment to use.
+ # @see: https://github.com/actions/virtual-environments
+ runs-on: ubuntu-20.04
+
+ # Environment Variables.
+ # Accessible by using ${{ env.NAME }}
+ # Use ${{ secrets.NAME }} to include any GitHub Secrets in ${{ env.NAME }}
+ # The base folder will always be /home/runner/work/github-repo-name/github-repo-name
+ env:
+ ROOT_DIR: /home/runner/work/convertkit-membermouse/convertkit-membermouse/wordpress
+ PLUGIN_DIR: /home/runner/work/convertkit-membermouse/convertkit-membermouse/wordpress/wp-content/plugins/convertkit-membermouse
+ DB_NAME: test
+ DB_USER: root
+ DB_PASS: root
+ DB_HOST: localhost
+ INSTALL_PLUGINS: "https://hub.membermouse.com/download.php" # Don't include this repository's Plugin here.
+ CONVERTKIT_API_KEY: ${{ secrets.CONVERTKIT_API_KEY }} # ConvertKit API Key, stored in the repository's Settings > Secrets
+ CONVERTKIT_API_SECRET: ${{ secrets.CONVERTKIT_API_SECRET }} # ConvertKit API Secret, stored in the repository's Settings > Secrets
+ CONVERTKIT_API_KEY_NO_DATA: ${{ secrets.CONVERTKIT_API_KEY_NO_DATA }} # ConvertKit API Key for ConvertKit account with no data, stored in the repository's Settings > Secrets
+ CONVERTKIT_API_SECRET_NO_DATA: ${{ secrets.CONVERTKIT_API_SECRET_NO_DATA }} # ConvertKit API Secret for ConvertKit account with no data, stored in the repository's Settings > Secrets
+
+ # Defines the WordPress and PHP Versions matrix to run tests on
+ # WooCommerce 5.9.0 requires WordPress 5.6 or greater, so we do not test on earlier versions
+ # If testing older WordPress versions, ensure they are e.g. 5.7.4, 5.6.6 that have the X3 SSL fix: https://core.trac.wordpress.org/ticket/54207
+ # For PHP, make sure that an nginx configuration file exists for the required PHP version in this repository at tests/nginx/php-x.x.conf
+ strategy:
+ fail-fast: false
+ matrix:
+ wp-versions: [ 'latest' ] #[ 'latest' ]
+ php-versions: [ '7.4', '8.0', '8.1', '8.2', '8.3' ] #[ '7.3', '7.4', '8.0', '8.1' ]
+
+ # Folder names within the 'tests' folder to run tests in parallel.
+ test-groups: [
+ 'acceptance/general',
+ ]
+
+ # Steps to install, configure and run tests
+ steps:
+ - name: Define Test Group Name
+ id: test-group
+ uses: mad9000/actions-find-and-replace-string@5
+ with:
+ source: ${{ matrix.test-groups }}
+ find: '/'
+ replace: '-'
+ replaceAll: true
+
+ - name: Start MySQL
+ run: sudo systemctl start mysql.service
+
+ - name: Create MySQL Database
+ run: |
+ mysql -e 'CREATE DATABASE test;' -u${{ env.DB_USER }} -p${{ env.DB_PASS }}
+ mysql -e 'SHOW DATABASES;' -u${{ env.DB_USER }} -p${{ env.DB_PASS }}
+
+ # WordPress won't be able to connect to the DB if we don't perform this step.
+ - name: Permit MySQL Password Auth for MySQL 8.0
+ run: mysql -e "ALTER USER '${{ env.DB_USER }}'@'${{ env.DB_HOST }}' IDENTIFIED WITH mysql_native_password BY '${{ env.DB_PASS }}';" -u${{ env.DB_USER }} -p${{ env.DB_PASS }}
+
+ # Some workflows checkout WordPress from GitHub, but that seems to bring a bunch of uncompiled files with it.
+ # Instead download from wordpress.org stable.
+ - name: Download WordPress
+ run: wget https://wordpress.org/wordpress-${{ matrix.wp-versions }}.tar.gz
+
+ - name: Extract WordPress
+ run: tar xfz wordpress-${{ matrix.wp-versions }}.tar.gz
+
+ # Checkout (copy) this repository's Plugin to this VM.
+ - name: Checkout Plugin
+ uses: actions/checkout@v4
+ with:
+ path: ${{ env.PLUGIN_DIR }}
+
+ # We install WP-CLI, as it provides useful commands to setup and install WordPress through the command line.
+ - name: Install WP-CLI
+ run: |
+ curl -O https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar
+ chmod +x wp-cli.phar
+ sudo mv wp-cli.phar /usr/local/bin/wp-cli
+
+ - name: Setup wp-config.php
+ working-directory: ${{ env.ROOT_DIR }}
+ run: wp-cli config create --dbname=${{ env.DB_NAME }} --dbuser=${{ env.DB_USER }} --dbpass=${{ env.DB_PASS }} --dbhost=${{ env.DB_HOST }} --locale=en_DB
+
+ - name: Install WordPress
+ working-directory: ${{ env.ROOT_DIR }}
+ run: wp-cli core install --url=127.0.0.1 --title=ConvertKit --admin_user=admin --admin_password=password --admin_email=wordpress@convertkit.local
+
+ # env.INSTALL_PLUGINS is a list of Plugin slugs, space separated e.g. contact-form-7 woocommerce.
+ - name: Install Free Third Party WordPress Plugins
+ working-directory: ${{ env.ROOT_DIR }}
+ run: wp-cli plugin install ${{ env.INSTALL_PLUGINS }}
+
+ # WP_DEBUG = true is required so all PHP errors are output and caught by tests (E_ALL).
+ - name: Enable WP_DEBUG
+ working-directory: ${{ env.ROOT_DIR }}
+ run: |
+ wp-cli config set WP_DEBUG true --raw
+
+ # FS_METHOD = direct is required for WP_Filesystem to operate without suppressed PHP fopen() errors that trip up tests.
+ - name: Enable FS_METHOD
+ working-directory: ${{ env.ROOT_DIR }}
+ run: |
+ wp-cli config set FS_METHOD direct
+
+ # This step is deliberately after WordPress installation and configuration, as enabling PHP 8.x before using WP-CLI results
+ # in the workflow failing due to incompatibilities between WP-CLI and PHP 8.x.
+ # By installing PHP at this stage, we can still run our tests against e.g. PHP 8.x.
+ - name: Install PHP
+ uses: shivammathur/setup-php@v2
+ with:
+ php-version: ${{ matrix.php-versions }}
+ coverage: xdebug
+
+ # Make sure that an nginx configuration file exists in this repository at tests/nginx/php-x.x.conf.
+ # Refer to an existing .conf file in this repository if you need to create a new one e.g. for a new PHP version.
+ - name: Copy nginx configuration file
+ run: sudo cp ${{ env.PLUGIN_DIR }}/tests/nginx/php-${{ matrix.php-versions }}.conf /etc/nginx/conf.d/php-${{ matrix.php-versions }}.conf
+
+ - name: Test nginx
+ run: sudo nginx -t
+
+ - name: Start nginx
+ run: sudo systemctl start nginx.service
+
+ - name: Install chromedriver
+ uses: nanasess/setup-chromedriver@master
+
+ - name: Start chromedriver
+ run: |
+ export DISPLAY=:99
+ chromedriver --url-base=/wd/hub &
+ sudo Xvfb -ac :99 -screen 0 1280x1024x24 > /dev/null 2>&1 & # optional
+
+ # Write any secrets, such as API keys, to the .env.dist.testing file now.
+ # Make sure your committed .env.dist.testing file ends with a newline.
+ # The formatting of the contents to include a blank newline is deliberate.
+ - name: Define GitHub Secrets in .env.dist.testing
+ uses: DamianReeves/write-file-action@v1.2
+ with:
+ path: ${{ env.PLUGIN_DIR }}/.env.dist.testing
+ contents: |
+
+ CONVERTKIT_API_KEY=${{ env.CONVERTKIT_API_KEY }}
+ CONVERTKIT_API_SECRET=${{ env.CONVERTKIT_API_SECRET }}
+ CONVERTKIT_API_KEY_NO_DATA=${{ env.CONVERTKIT_API_KEY_NO_DATA }}
+ CONVERTKIT_API_SECRET_NO_DATA=${{ env.CONVERTKIT_API_SECRET_NO_DATA }}
+ write-mode: append
+
+ # Installs wp-browser, Codeception, PHP CodeSniffer and anything else needed to run tests.
+ - name: Run Composer
+ working-directory: ${{ env.PLUGIN_DIR }}
+ run: composer update
+
+ - name: Build PHP Autoloader
+ working-directory: ${{ env.PLUGIN_DIR }}
+ run: composer dump-autoload
+
+ # This ensures the Plugin's log file can be written to.
+ # We don't recursively do this, as it'll prevent Codeception from writing to the /tests/_output directory.
+ - name: Set Permissions for Plugin Directory
+ run: |
+ sudo chmod g+w ${{ env.PLUGIN_DIR }}
+ sudo chown www-data:www-data ${{ env.PLUGIN_DIR }}
+
+ # Build Codeception Tests.
+ - name: Build Tests
+ working-directory: ${{ env.PLUGIN_DIR }}
+ run: php vendor/bin/codecept build
+
+ # Run Codeception Acceptance Tests.
+ - name: Run tests/${{ matrix.test-groups }}
+ working-directory: ${{ env.PLUGIN_DIR }}
+ run: php vendor/bin/codecept run tests/${{ matrix.test-groups }} --fail-fast
+
+ # Artifacts are data generated by this workflow that we want to access, such as log files, screenshots, HTML output.
+ # The if: failure() directive means that this will run when the workflow fails e.g. if a test fails, which is needed
+ # because we want to see why a test failed.
+ - name: Upload Test Results to Artifact
+ if: failure()
+ uses: actions/upload-artifact@v4
+ with:
+ name: test-results-${{ steps.test-group.outputs.value }}-${{ matrix.php-versions }}
+ path: ${{ env.PLUGIN_DIR }}/tests/_output/
+
+ - name: Upload Plugin Log File to Artifact
+ if: failure()
+ uses: actions/upload-artifact@v4
+ with:
+ name: log-${{ steps.test-group.outputs.value }}-${{ matrix.php-versions }}.txt
+ path: ${{ env.PLUGIN_DIR }}/log/log.txt
\ No newline at end of file
diff --git a/codeception.dist.yml b/codeception.dist.yml
new file mode 100644
index 0000000..e05dd29
--- /dev/null
+++ b/codeception.dist.yml
@@ -0,0 +1,22 @@
+paths:
+ tests: tests
+ output: tests/_output
+ data: tests/_data
+ support: tests/_support
+ envs: tests/_envs
+settings:
+ error_level: E_ALL & ~E_STRICT & ~E_DEPRECATED
+actor_suffix: Tester
+extensions:
+ enabled:
+ - Codeception\Extension\RunFailed
+ commands:
+ - Codeception\Command\GenerateWPUnit
+ - Codeception\Command\GenerateWPRestApi
+ - Codeception\Command\GenerateWPRestController
+ - Codeception\Command\GenerateWPRestPostTypeController
+ - Codeception\Command\GenerateWPAjax
+ - Codeception\Command\GenerateWPCanonical
+ - Codeception\Command\GenerateWPXMLRPC
+params:
+ - .env.dist.testing
\ No newline at end of file
diff --git a/composer.json b/composer.json
index 7f34bcc..62fc3a7 100644
--- a/composer.json
+++ b/composer.json
@@ -3,9 +3,6 @@
"description": "ConvertKit WordPress Plugin",
"type": "project",
"license": "GPLv3",
- "require": {
- "convertkit/convertkit-wordpress-libraries": "1.4.2"
- },
"require-dev": {
"lucatume/wp-browser": "<3.5",
"codeception/module-asserts": "^1.3",
diff --git a/phpcs.tests.xml b/phpcs.tests.xml
new file mode 100644
index 0000000..881d54b
--- /dev/null
+++ b/phpcs.tests.xml
@@ -0,0 +1,58 @@
+
+
+ Coding Standards for Tests
+
+
+ tests
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tests/_data/.gitkeep b/tests/_data/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/tests/_data/dump.sql b/tests/_data/dump.sql
new file mode 100644
index 0000000..90a00fe
--- /dev/null
+++ b/tests/_data/dump.sql
@@ -0,0 +1,364 @@
+-- Adminer 4.8.1 MySQL 8.0.16 dump
+
+SET NAMES utf8;
+SET time_zone = '+00:00';
+SET foreign_key_checks = 0;
+SET sql_mode = 'NO_AUTO_VALUE_ON_ZERO';
+
+SET NAMES utf8mb4;
+
+DROP TABLE IF EXISTS `wp_commentmeta`;
+CREATE TABLE `wp_commentmeta` (
+ `meta_id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
+ `comment_id` bigint(20) unsigned NOT NULL DEFAULT '0',
+ `meta_key` varchar(255) COLLATE utf8mb4_unicode_520_ci DEFAULT NULL,
+ `meta_value` longtext COLLATE utf8mb4_unicode_520_ci,
+ PRIMARY KEY (`meta_id`),
+ KEY `comment_id` (`comment_id`),
+ KEY `meta_key` (`meta_key`(191))
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci;
+
+
+DROP TABLE IF EXISTS `wp_comments`;
+CREATE TABLE `wp_comments` (
+ `comment_ID` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
+ `comment_post_ID` bigint(20) unsigned NOT NULL DEFAULT '0',
+ `comment_author` tinytext COLLATE utf8mb4_unicode_520_ci NOT NULL,
+ `comment_author_email` varchar(100) COLLATE utf8mb4_unicode_520_ci NOT NULL DEFAULT '',
+ `comment_author_url` varchar(200) COLLATE utf8mb4_unicode_520_ci NOT NULL DEFAULT '',
+ `comment_author_IP` varchar(100) COLLATE utf8mb4_unicode_520_ci NOT NULL DEFAULT '',
+ `comment_date` datetime NOT NULL DEFAULT '0000-00-00 00:00:00',
+ `comment_date_gmt` datetime NOT NULL DEFAULT '0000-00-00 00:00:00',
+ `comment_content` text COLLATE utf8mb4_unicode_520_ci NOT NULL,
+ `comment_karma` int(11) NOT NULL DEFAULT '0',
+ `comment_approved` varchar(20) COLLATE utf8mb4_unicode_520_ci NOT NULL DEFAULT '1',
+ `comment_agent` varchar(255) COLLATE utf8mb4_unicode_520_ci NOT NULL DEFAULT '',
+ `comment_type` varchar(20) COLLATE utf8mb4_unicode_520_ci NOT NULL DEFAULT 'comment',
+ `comment_parent` bigint(20) unsigned NOT NULL DEFAULT '0',
+ `user_id` bigint(20) unsigned NOT NULL DEFAULT '0',
+ PRIMARY KEY (`comment_ID`),
+ KEY `comment_post_ID` (`comment_post_ID`),
+ KEY `comment_approved_date_gmt` (`comment_approved`,`comment_date_gmt`),
+ KEY `comment_date_gmt` (`comment_date_gmt`),
+ KEY `comment_parent` (`comment_parent`),
+ KEY `comment_author_email` (`comment_author_email`(10))
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci;
+
+INSERT INTO `wp_comments` (`comment_ID`, `comment_post_ID`, `comment_author`, `comment_author_email`, `comment_author_url`, `comment_author_IP`, `comment_date`, `comment_date_gmt`, `comment_content`, `comment_karma`, `comment_approved`, `comment_agent`, `comment_type`, `comment_parent`, `user_id`) VALUES
+(1, 1, 'A WordPress Commenter', 'wapuu@wordpress.example', 'https://wordpress.org/', '', '2023-07-03 13:38:12', '2023-07-03 13:38:12', 'Hi, this is a comment.\nTo get started with moderating, editing, and deleting comments, please visit the Comments screen in the dashboard.\nCommenter avatars come from Gravatar.', 0, '1', '', 'comment', 0, 0);
+
+DROP TABLE IF EXISTS `wp_links`;
+CREATE TABLE `wp_links` (
+ `link_id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
+ `link_url` varchar(255) COLLATE utf8mb4_unicode_520_ci NOT NULL DEFAULT '',
+ `link_name` varchar(255) COLLATE utf8mb4_unicode_520_ci NOT NULL DEFAULT '',
+ `link_image` varchar(255) COLLATE utf8mb4_unicode_520_ci NOT NULL DEFAULT '',
+ `link_target` varchar(25) COLLATE utf8mb4_unicode_520_ci NOT NULL DEFAULT '',
+ `link_description` varchar(255) COLLATE utf8mb4_unicode_520_ci NOT NULL DEFAULT '',
+ `link_visible` varchar(20) COLLATE utf8mb4_unicode_520_ci NOT NULL DEFAULT 'Y',
+ `link_owner` bigint(20) unsigned NOT NULL DEFAULT '1',
+ `link_rating` int(11) NOT NULL DEFAULT '0',
+ `link_updated` datetime NOT NULL DEFAULT '0000-00-00 00:00:00',
+ `link_rel` varchar(255) COLLATE utf8mb4_unicode_520_ci NOT NULL DEFAULT '',
+ `link_notes` mediumtext COLLATE utf8mb4_unicode_520_ci NOT NULL,
+ `link_rss` varchar(255) COLLATE utf8mb4_unicode_520_ci NOT NULL DEFAULT '',
+ PRIMARY KEY (`link_id`),
+ KEY `link_visible` (`link_visible`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci;
+
+
+DROP TABLE IF EXISTS `wp_options`;
+CREATE TABLE `wp_options` (
+ `option_id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
+ `option_name` varchar(191) COLLATE utf8mb4_unicode_520_ci NOT NULL DEFAULT '',
+ `option_value` longtext COLLATE utf8mb4_unicode_520_ci NOT NULL,
+ `autoload` varchar(20) COLLATE utf8mb4_unicode_520_ci NOT NULL DEFAULT 'yes',
+ PRIMARY KEY (`option_id`),
+ UNIQUE KEY `option_name` (`option_name`),
+ KEY `autoload` (`autoload`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci;
+
+INSERT INTO `wp_options` (`option_id`, `option_name`, `option_value`, `autoload`) VALUES
+(1, 'siteurl', 'http://convertkit.local', 'yes'),
+(2, 'home', 'http://convertkit.local', 'yes'),
+(3, 'blogname', 'convertkit', 'yes'),
+(4, 'blogdescription', 'Just another WordPress site', 'yes'),
+(5, 'users_can_register', '0', 'yes'),
+(6, 'admin_email', 'dev-email@flywheel.local', 'yes'),
+(7, 'start_of_week', '1', 'yes'),
+(8, 'use_balanceTags', '0', 'yes'),
+(9, 'use_smilies', '1', 'yes'),
+(10, 'require_name_email', '1', 'yes'),
+(11, 'comments_notify', '1', 'yes'),
+(12, 'posts_per_rss', '10', 'yes'),
+(13, 'rss_use_excerpt', '0', 'yes'),
+(14, 'mailserver_url', 'mail.example.com', 'yes'),
+(15, 'mailserver_login', 'login@example.com', 'yes'),
+(16, 'mailserver_pass', 'password', 'yes'),
+(17, 'mailserver_port', '110', 'yes'),
+(18, 'default_category', '1', 'yes'),
+(19, 'default_comment_status', 'open', 'yes'),
+(20, 'default_ping_status', 'open', 'yes'),
+(21, 'default_pingback_flag', '1', 'yes'),
+(22, 'posts_per_page', '10', 'yes'),
+(23, 'date_format', 'F j, Y', 'yes'),
+(24, 'time_format', 'g:i a', 'yes'),
+(25, 'links_updated_date_format', 'F j, Y g:i a', 'yes'),
+(26, 'comment_moderation', '0', 'yes'),
+(27, 'moderation_notify', '1', 'yes'),
+(28, 'permalink_structure', '/%postname%/', 'yes'),
+(29, 'rewrite_rules', 'a:93:{s:11:\"^wp-json/?$\";s:22:\"index.php?rest_route=/\";s:14:\"^wp-json/(.*)?\";s:33:\"index.php?rest_route=/$matches[1]\";s:21:\"^index.php/wp-json/?$\";s:22:\"index.php?rest_route=/\";s:24:\"^index.php/wp-json/(.*)?\";s:33:\"index.php?rest_route=/$matches[1]\";s:17:\"^wp-sitemap\\.xml$\";s:23:\"index.php?sitemap=index\";s:17:\"^wp-sitemap\\.xsl$\";s:36:\"index.php?sitemap-stylesheet=sitemap\";s:23:\"^wp-sitemap-index\\.xsl$\";s:34:\"index.php?sitemap-stylesheet=index\";s:48:\"^wp-sitemap-([a-z]+?)-([a-z\\d_-]+?)-(\\d+?)\\.xml$\";s:75:\"index.php?sitemap=$matches[1]&sitemap-subtype=$matches[2]&paged=$matches[3]\";s:34:\"^wp-sitemap-([a-z]+?)-(\\d+?)\\.xml$\";s:47:\"index.php?sitemap=$matches[1]&paged=$matches[2]\";s:47:\"category/(.+?)/feed/(feed|rdf|rss|rss2|atom)/?$\";s:52:\"index.php?category_name=$matches[1]&feed=$matches[2]\";s:42:\"category/(.+?)/(feed|rdf|rss|rss2|atom)/?$\";s:52:\"index.php?category_name=$matches[1]&feed=$matches[2]\";s:23:\"category/(.+?)/embed/?$\";s:46:\"index.php?category_name=$matches[1]&embed=true\";s:35:\"category/(.+?)/page/?([0-9]{1,})/?$\";s:53:\"index.php?category_name=$matches[1]&paged=$matches[2]\";s:17:\"category/(.+?)/?$\";s:35:\"index.php?category_name=$matches[1]\";s:44:\"tag/([^/]+)/feed/(feed|rdf|rss|rss2|atom)/?$\";s:42:\"index.php?tag=$matches[1]&feed=$matches[2]\";s:39:\"tag/([^/]+)/(feed|rdf|rss|rss2|atom)/?$\";s:42:\"index.php?tag=$matches[1]&feed=$matches[2]\";s:20:\"tag/([^/]+)/embed/?$\";s:36:\"index.php?tag=$matches[1]&embed=true\";s:32:\"tag/([^/]+)/page/?([0-9]{1,})/?$\";s:43:\"index.php?tag=$matches[1]&paged=$matches[2]\";s:14:\"tag/([^/]+)/?$\";s:25:\"index.php?tag=$matches[1]\";s:45:\"type/([^/]+)/feed/(feed|rdf|rss|rss2|atom)/?$\";s:50:\"index.php?post_format=$matches[1]&feed=$matches[2]\";s:40:\"type/([^/]+)/(feed|rdf|rss|rss2|atom)/?$\";s:50:\"index.php?post_format=$matches[1]&feed=$matches[2]\";s:21:\"type/([^/]+)/embed/?$\";s:44:\"index.php?post_format=$matches[1]&embed=true\";s:33:\"type/([^/]+)/page/?([0-9]{1,})/?$\";s:51:\"index.php?post_format=$matches[1]&paged=$matches[2]\";s:15:\"type/([^/]+)/?$\";s:33:\"index.php?post_format=$matches[1]\";s:12:\"robots\\.txt$\";s:18:\"index.php?robots=1\";s:13:\"favicon\\.ico$\";s:19:\"index.php?favicon=1\";s:48:\".*wp-(atom|rdf|rss|rss2|feed|commentsrss2)\\.php$\";s:18:\"index.php?feed=old\";s:20:\".*wp-app\\.php(/.*)?$\";s:19:\"index.php?error=403\";s:18:\".*wp-register.php$\";s:23:\"index.php?register=true\";s:32:\"feed/(feed|rdf|rss|rss2|atom)/?$\";s:27:\"index.php?&feed=$matches[1]\";s:27:\"(feed|rdf|rss|rss2|atom)/?$\";s:27:\"index.php?&feed=$matches[1]\";s:8:\"embed/?$\";s:21:\"index.php?&embed=true\";s:20:\"page/?([0-9]{1,})/?$\";s:28:\"index.php?&paged=$matches[1]\";s:41:\"comments/feed/(feed|rdf|rss|rss2|atom)/?$\";s:42:\"index.php?&feed=$matches[1]&withcomments=1\";s:36:\"comments/(feed|rdf|rss|rss2|atom)/?$\";s:42:\"index.php?&feed=$matches[1]&withcomments=1\";s:17:\"comments/embed/?$\";s:21:\"index.php?&embed=true\";s:44:\"search/(.+)/feed/(feed|rdf|rss|rss2|atom)/?$\";s:40:\"index.php?s=$matches[1]&feed=$matches[2]\";s:39:\"search/(.+)/(feed|rdf|rss|rss2|atom)/?$\";s:40:\"index.php?s=$matches[1]&feed=$matches[2]\";s:20:\"search/(.+)/embed/?$\";s:34:\"index.php?s=$matches[1]&embed=true\";s:32:\"search/(.+)/page/?([0-9]{1,})/?$\";s:41:\"index.php?s=$matches[1]&paged=$matches[2]\";s:14:\"search/(.+)/?$\";s:23:\"index.php?s=$matches[1]\";s:47:\"author/([^/]+)/feed/(feed|rdf|rss|rss2|atom)/?$\";s:50:\"index.php?author_name=$matches[1]&feed=$matches[2]\";s:42:\"author/([^/]+)/(feed|rdf|rss|rss2|atom)/?$\";s:50:\"index.php?author_name=$matches[1]&feed=$matches[2]\";s:23:\"author/([^/]+)/embed/?$\";s:44:\"index.php?author_name=$matches[1]&embed=true\";s:35:\"author/([^/]+)/page/?([0-9]{1,})/?$\";s:51:\"index.php?author_name=$matches[1]&paged=$matches[2]\";s:17:\"author/([^/]+)/?$\";s:33:\"index.php?author_name=$matches[1]\";s:69:\"([0-9]{4})/([0-9]{1,2})/([0-9]{1,2})/feed/(feed|rdf|rss|rss2|atom)/?$\";s:80:\"index.php?year=$matches[1]&monthnum=$matches[2]&day=$matches[3]&feed=$matches[4]\";s:64:\"([0-9]{4})/([0-9]{1,2})/([0-9]{1,2})/(feed|rdf|rss|rss2|atom)/?$\";s:80:\"index.php?year=$matches[1]&monthnum=$matches[2]&day=$matches[3]&feed=$matches[4]\";s:45:\"([0-9]{4})/([0-9]{1,2})/([0-9]{1,2})/embed/?$\";s:74:\"index.php?year=$matches[1]&monthnum=$matches[2]&day=$matches[3]&embed=true\";s:57:\"([0-9]{4})/([0-9]{1,2})/([0-9]{1,2})/page/?([0-9]{1,})/?$\";s:81:\"index.php?year=$matches[1]&monthnum=$matches[2]&day=$matches[3]&paged=$matches[4]\";s:39:\"([0-9]{4})/([0-9]{1,2})/([0-9]{1,2})/?$\";s:63:\"index.php?year=$matches[1]&monthnum=$matches[2]&day=$matches[3]\";s:56:\"([0-9]{4})/([0-9]{1,2})/feed/(feed|rdf|rss|rss2|atom)/?$\";s:64:\"index.php?year=$matches[1]&monthnum=$matches[2]&feed=$matches[3]\";s:51:\"([0-9]{4})/([0-9]{1,2})/(feed|rdf|rss|rss2|atom)/?$\";s:64:\"index.php?year=$matches[1]&monthnum=$matches[2]&feed=$matches[3]\";s:32:\"([0-9]{4})/([0-9]{1,2})/embed/?$\";s:58:\"index.php?year=$matches[1]&monthnum=$matches[2]&embed=true\";s:44:\"([0-9]{4})/([0-9]{1,2})/page/?([0-9]{1,})/?$\";s:65:\"index.php?year=$matches[1]&monthnum=$matches[2]&paged=$matches[3]\";s:26:\"([0-9]{4})/([0-9]{1,2})/?$\";s:47:\"index.php?year=$matches[1]&monthnum=$matches[2]\";s:43:\"([0-9]{4})/feed/(feed|rdf|rss|rss2|atom)/?$\";s:43:\"index.php?year=$matches[1]&feed=$matches[2]\";s:38:\"([0-9]{4})/(feed|rdf|rss|rss2|atom)/?$\";s:43:\"index.php?year=$matches[1]&feed=$matches[2]\";s:19:\"([0-9]{4})/embed/?$\";s:37:\"index.php?year=$matches[1]&embed=true\";s:31:\"([0-9]{4})/page/?([0-9]{1,})/?$\";s:44:\"index.php?year=$matches[1]&paged=$matches[2]\";s:13:\"([0-9]{4})/?$\";s:26:\"index.php?year=$matches[1]\";s:27:\".?.+?/attachment/([^/]+)/?$\";s:32:\"index.php?attachment=$matches[1]\";s:37:\".?.+?/attachment/([^/]+)/trackback/?$\";s:37:\"index.php?attachment=$matches[1]&tb=1\";s:57:\".?.+?/attachment/([^/]+)/feed/(feed|rdf|rss|rss2|atom)/?$\";s:49:\"index.php?attachment=$matches[1]&feed=$matches[2]\";s:52:\".?.+?/attachment/([^/]+)/(feed|rdf|rss|rss2|atom)/?$\";s:49:\"index.php?attachment=$matches[1]&feed=$matches[2]\";s:52:\".?.+?/attachment/([^/]+)/comment-page-([0-9]{1,})/?$\";s:50:\"index.php?attachment=$matches[1]&cpage=$matches[2]\";s:33:\".?.+?/attachment/([^/]+)/embed/?$\";s:43:\"index.php?attachment=$matches[1]&embed=true\";s:16:\"(.?.+?)/embed/?$\";s:41:\"index.php?pagename=$matches[1]&embed=true\";s:20:\"(.?.+?)/trackback/?$\";s:35:\"index.php?pagename=$matches[1]&tb=1\";s:40:\"(.?.+?)/feed/(feed|rdf|rss|rss2|atom)/?$\";s:47:\"index.php?pagename=$matches[1]&feed=$matches[2]\";s:35:\"(.?.+?)/(feed|rdf|rss|rss2|atom)/?$\";s:47:\"index.php?pagename=$matches[1]&feed=$matches[2]\";s:28:\"(.?.+?)/page/?([0-9]{1,})/?$\";s:48:\"index.php?pagename=$matches[1]&paged=$matches[2]\";s:35:\"(.?.+?)/comment-page-([0-9]{1,})/?$\";s:48:\"index.php?pagename=$matches[1]&cpage=$matches[2]\";s:24:\"(.?.+?)(?:/([0-9]+))?/?$\";s:47:\"index.php?pagename=$matches[1]&page=$matches[2]\";s:27:\"[^/]+/attachment/([^/]+)/?$\";s:32:\"index.php?attachment=$matches[1]\";s:37:\"[^/]+/attachment/([^/]+)/trackback/?$\";s:37:\"index.php?attachment=$matches[1]&tb=1\";s:57:\"[^/]+/attachment/([^/]+)/feed/(feed|rdf|rss|rss2|atom)/?$\";s:49:\"index.php?attachment=$matches[1]&feed=$matches[2]\";s:52:\"[^/]+/attachment/([^/]+)/(feed|rdf|rss|rss2|atom)/?$\";s:49:\"index.php?attachment=$matches[1]&feed=$matches[2]\";s:52:\"[^/]+/attachment/([^/]+)/comment-page-([0-9]{1,})/?$\";s:50:\"index.php?attachment=$matches[1]&cpage=$matches[2]\";s:33:\"[^/]+/attachment/([^/]+)/embed/?$\";s:43:\"index.php?attachment=$matches[1]&embed=true\";s:16:\"([^/]+)/embed/?$\";s:37:\"index.php?name=$matches[1]&embed=true\";s:20:\"([^/]+)/trackback/?$\";s:31:\"index.php?name=$matches[1]&tb=1\";s:40:\"([^/]+)/feed/(feed|rdf|rss|rss2|atom)/?$\";s:43:\"index.php?name=$matches[1]&feed=$matches[2]\";s:35:\"([^/]+)/(feed|rdf|rss|rss2|atom)/?$\";s:43:\"index.php?name=$matches[1]&feed=$matches[2]\";s:28:\"([^/]+)/page/?([0-9]{1,})/?$\";s:44:\"index.php?name=$matches[1]&paged=$matches[2]\";s:35:\"([^/]+)/comment-page-([0-9]{1,})/?$\";s:44:\"index.php?name=$matches[1]&cpage=$matches[2]\";s:24:\"([^/]+)(?:/([0-9]+))?/?$\";s:43:\"index.php?name=$matches[1]&page=$matches[2]\";s:16:\"[^/]+/([^/]+)/?$\";s:32:\"index.php?attachment=$matches[1]\";s:26:\"[^/]+/([^/]+)/trackback/?$\";s:37:\"index.php?attachment=$matches[1]&tb=1\";s:46:\"[^/]+/([^/]+)/feed/(feed|rdf|rss|rss2|atom)/?$\";s:49:\"index.php?attachment=$matches[1]&feed=$matches[2]\";s:41:\"[^/]+/([^/]+)/(feed|rdf|rss|rss2|atom)/?$\";s:49:\"index.php?attachment=$matches[1]&feed=$matches[2]\";s:41:\"[^/]+/([^/]+)/comment-page-([0-9]{1,})/?$\";s:50:\"index.php?attachment=$matches[1]&cpage=$matches[2]\";s:22:\"[^/]+/([^/]+)/embed/?$\";s:43:\"index.php?attachment=$matches[1]&embed=true\";}', 'yes'),
+(30, 'hack_file', '0', 'yes'),
+(31, 'blog_charset', 'UTF-8', 'yes'),
+(32, 'moderation_keys', '', 'no'),
+(33, 'active_plugins', 'a:0:{}', 'yes'),
+(34, 'category_base', '', 'yes'),
+(35, 'ping_sites', 'http://rpc.pingomatic.com/', 'yes'),
+(36, 'comment_max_links', '2', 'yes'),
+(37, 'gmt_offset', '0', 'yes'),
+(38, 'default_email_category', '1', 'yes'),
+(39, 'recently_edited', '', 'no'),
+(40, 'template', 'twentytwentythree', 'yes'),
+(41, 'stylesheet', 'twentytwentythree', 'yes'),
+(42, 'comment_registration', '0', 'yes'),
+(43, 'html_type', 'text/html', 'yes'),
+(44, 'use_trackback', '0', 'yes'),
+(45, 'default_role', 'subscriber', 'yes'),
+(46, 'db_version', '57155', 'yes'),
+(47, 'uploads_use_yearmonth_folders', '1', 'yes'),
+(48, 'upload_path', '', 'yes'),
+(49, 'blog_public', '1', 'yes'),
+(50, 'default_link_category', '2', 'yes'),
+(51, 'show_on_front', 'posts', 'yes'),
+(52, 'tag_base', '', 'yes'),
+(53, 'show_avatars', '1', 'yes'),
+(54, 'avatar_rating', 'G', 'yes'),
+(55, 'upload_url_path', '', 'yes'),
+(56, 'thumbnail_size_w', '150', 'yes'),
+(57, 'thumbnail_size_h', '150', 'yes'),
+(58, 'thumbnail_crop', '1', 'yes'),
+(59, 'medium_size_w', '300', 'yes'),
+(60, 'medium_size_h', '300', 'yes'),
+(61, 'avatar_default', 'mystery', 'yes'),
+(62, 'large_size_w', '1024', 'yes'),
+(63, 'large_size_h', '1024', 'yes'),
+(64, 'image_default_link_type', 'none', 'yes'),
+(65, 'image_default_size', '', 'yes'),
+(66, 'image_default_align', '', 'yes'),
+(67, 'close_comments_for_old_posts', '0', 'yes'),
+(68, 'close_comments_days_old', '14', 'yes'),
+(69, 'thread_comments', '1', 'yes'),
+(70, 'thread_comments_depth', '5', 'yes'),
+(71, 'page_comments', '0', 'yes'),
+(72, 'comments_per_page', '50', 'yes'),
+(73, 'default_comments_page', 'newest', 'yes'),
+(74, 'comment_order', 'asc', 'yes'),
+(75, 'sticky_posts', 'a:0:{}', 'yes'),
+(76, 'widget_categories', 'a:0:{}', 'yes'),
+(77, 'widget_text', 'a:0:{}', 'yes'),
+(78, 'widget_rss', 'a:0:{}', 'yes'),
+(79, 'uninstall_plugins', 'a:0:{}', 'no'),
+(80, 'timezone_string', '', 'yes'),
+(81, 'page_for_posts', '0', 'yes'),
+(82, 'page_on_front', '0', 'yes'),
+(83, 'default_post_format', '0', 'yes'),
+(84, 'link_manager_enabled', '0', 'yes'),
+(85, 'finished_splitting_shared_terms', '1', 'yes'),
+(86, 'site_icon', '0', 'yes'),
+(87, 'medium_large_size_w', '768', 'yes'),
+(88, 'medium_large_size_h', '0', 'yes'),
+(89, 'wp_page_for_privacy_policy', '3', 'yes'),
+(90, 'show_comments_cookies_opt_in', '1', 'yes'),
+(91, 'admin_email_lifespan', '1712414097', 'yes'),
+(92, 'disallowed_keys', '', 'no'),
+(93, 'comment_previously_approved', '1', 'yes'),
+(94, 'auto_plugin_theme_update_emails', 'a:0:{}', 'no'),
+(95, 'auto_update_core_dev', 'enabled', 'yes'),
+(96, 'auto_update_core_minor', 'enabled', 'yes'),
+(97, 'auto_update_core_major', 'enabled', 'yes'),
+(98, 'wp_force_deactivated_plugins', 'a:0:{}', 'yes'),
+(99, 'initial_db_version', '57155', 'yes'),
+(100, 'wp_user_roles', 'a:5:{s:13:\"administrator\";a:2:{s:4:\"name\";s:13:\"Administrator\";s:12:\"capabilities\";a:61:{s:13:\"switch_themes\";b:1;s:11:\"edit_themes\";b:1;s:16:\"activate_plugins\";b:1;s:12:\"edit_plugins\";b:1;s:10:\"edit_users\";b:1;s:10:\"edit_files\";b:1;s:14:\"manage_options\";b:1;s:17:\"moderate_comments\";b:1;s:17:\"manage_categories\";b:1;s:12:\"manage_links\";b:1;s:12:\"upload_files\";b:1;s:6:\"import\";b:1;s:15:\"unfiltered_html\";b:1;s:10:\"edit_posts\";b:1;s:17:\"edit_others_posts\";b:1;s:20:\"edit_published_posts\";b:1;s:13:\"publish_posts\";b:1;s:10:\"edit_pages\";b:1;s:4:\"read\";b:1;s:8:\"level_10\";b:1;s:7:\"level_9\";b:1;s:7:\"level_8\";b:1;s:7:\"level_7\";b:1;s:7:\"level_6\";b:1;s:7:\"level_5\";b:1;s:7:\"level_4\";b:1;s:7:\"level_3\";b:1;s:7:\"level_2\";b:1;s:7:\"level_1\";b:1;s:7:\"level_0\";b:1;s:17:\"edit_others_pages\";b:1;s:20:\"edit_published_pages\";b:1;s:13:\"publish_pages\";b:1;s:12:\"delete_pages\";b:1;s:19:\"delete_others_pages\";b:1;s:22:\"delete_published_pages\";b:1;s:12:\"delete_posts\";b:1;s:19:\"delete_others_posts\";b:1;s:22:\"delete_published_posts\";b:1;s:20:\"delete_private_posts\";b:1;s:18:\"edit_private_posts\";b:1;s:18:\"read_private_posts\";b:1;s:20:\"delete_private_pages\";b:1;s:18:\"edit_private_pages\";b:1;s:18:\"read_private_pages\";b:1;s:12:\"delete_users\";b:1;s:12:\"create_users\";b:1;s:17:\"unfiltered_upload\";b:1;s:14:\"edit_dashboard\";b:1;s:14:\"update_plugins\";b:1;s:14:\"delete_plugins\";b:1;s:15:\"install_plugins\";b:1;s:13:\"update_themes\";b:1;s:14:\"install_themes\";b:1;s:11:\"update_core\";b:1;s:10:\"list_users\";b:1;s:12:\"remove_users\";b:1;s:13:\"promote_users\";b:1;s:18:\"edit_theme_options\";b:1;s:13:\"delete_themes\";b:1;s:6:\"export\";b:1;}}s:6:\"editor\";a:2:{s:4:\"name\";s:6:\"Editor\";s:12:\"capabilities\";a:34:{s:17:\"moderate_comments\";b:1;s:17:\"manage_categories\";b:1;s:12:\"manage_links\";b:1;s:12:\"upload_files\";b:1;s:15:\"unfiltered_html\";b:1;s:10:\"edit_posts\";b:1;s:17:\"edit_others_posts\";b:1;s:20:\"edit_published_posts\";b:1;s:13:\"publish_posts\";b:1;s:10:\"edit_pages\";b:1;s:4:\"read\";b:1;s:7:\"level_7\";b:1;s:7:\"level_6\";b:1;s:7:\"level_5\";b:1;s:7:\"level_4\";b:1;s:7:\"level_3\";b:1;s:7:\"level_2\";b:1;s:7:\"level_1\";b:1;s:7:\"level_0\";b:1;s:17:\"edit_others_pages\";b:1;s:20:\"edit_published_pages\";b:1;s:13:\"publish_pages\";b:1;s:12:\"delete_pages\";b:1;s:19:\"delete_others_pages\";b:1;s:22:\"delete_published_pages\";b:1;s:12:\"delete_posts\";b:1;s:19:\"delete_others_posts\";b:1;s:22:\"delete_published_posts\";b:1;s:20:\"delete_private_posts\";b:1;s:18:\"edit_private_posts\";b:1;s:18:\"read_private_posts\";b:1;s:20:\"delete_private_pages\";b:1;s:18:\"edit_private_pages\";b:1;s:18:\"read_private_pages\";b:1;}}s:6:\"author\";a:2:{s:4:\"name\";s:6:\"Author\";s:12:\"capabilities\";a:10:{s:12:\"upload_files\";b:1;s:10:\"edit_posts\";b:1;s:20:\"edit_published_posts\";b:1;s:13:\"publish_posts\";b:1;s:4:\"read\";b:1;s:7:\"level_2\";b:1;s:7:\"level_1\";b:1;s:7:\"level_0\";b:1;s:12:\"delete_posts\";b:1;s:22:\"delete_published_posts\";b:1;}}s:11:\"contributor\";a:2:{s:4:\"name\";s:11:\"Contributor\";s:12:\"capabilities\";a:5:{s:10:\"edit_posts\";b:1;s:4:\"read\";b:1;s:7:\"level_1\";b:1;s:7:\"level_0\";b:1;s:12:\"delete_posts\";b:1;}}s:10:\"subscriber\";a:2:{s:4:\"name\";s:10:\"Subscriber\";s:12:\"capabilities\";a:2:{s:4:\"read\";b:1;s:7:\"level_0\";b:1;}}}', 'yes'),
+(101, 'fresh_site', '1', 'yes'),
+(102, 'user_count', '1', 'no'),
+(103, 'widget_block', 'a:6:{i:2;a:1:{s:7:\"content\";s:19:\"\";}i:3;a:1:{s:7:\"content\";s:154:\"
Recent Posts
\";}i:4;a:1:{s:7:\"content\";s:227:\"Recent Comments
\";}i:5;a:1:{s:7:\"content\";s:146:\"Archives
\";}i:6;a:1:{s:7:\"content\";s:150:\"Categories
\";}s:12:\"_multiwidget\";i:1;}', 'yes'),
+(104, 'sidebars_widgets', 'a:4:{s:19:\"wp_inactive_widgets\";a:0:{}s:9:\"sidebar-1\";a:3:{i:0;s:7:\"block-2\";i:1;s:7:\"block-3\";i:2;s:7:\"block-4\";}s:9:\"sidebar-2\";a:2:{i:0;s:7:\"block-5\";i:1;s:7:\"block-6\";}s:13:\"array_version\";i:3;}', 'yes'),
+(105, 'cron', 'a:9:{i:1696862175;a:1:{s:28:\"wp_update_comment_type_batch\";a:1:{s:32:\"40cd750bba9870f18aada2478b24840a\";a:2:{s:8:\"schedule\";b:0;s:4:\"args\";a:0:{}}}}i:1696865698;a:1:{s:34:\"wp_privacy_delete_old_export_files\";a:1:{s:32:\"40cd750bba9870f18aada2478b24840a\";a:3:{s:8:\"schedule\";s:6:\"hourly\";s:4:\"args\";a:0:{}s:8:\"interval\";i:3600;}}}i:1696905298;a:3:{s:16:\"wp_version_check\";a:1:{s:32:\"40cd750bba9870f18aada2478b24840a\";a:3:{s:8:\"schedule\";s:10:\"twicedaily\";s:4:\"args\";a:0:{}s:8:\"interval\";i:43200;}}s:17:\"wp_update_plugins\";a:1:{s:32:\"40cd750bba9870f18aada2478b24840a\";a:3:{s:8:\"schedule\";s:10:\"twicedaily\";s:4:\"args\";a:0:{}s:8:\"interval\";i:43200;}}s:16:\"wp_update_themes\";a:1:{s:32:\"40cd750bba9870f18aada2478b24840a\";a:3:{s:8:\"schedule\";s:10:\"twicedaily\";s:4:\"args\";a:0:{}s:8:\"interval\";i:43200;}}}i:1696905315;a:1:{s:21:\"wp_update_user_counts\";a:1:{s:32:\"40cd750bba9870f18aada2478b24840a\";a:3:{s:8:\"schedule\";s:10:\"twicedaily\";s:4:\"args\";a:0:{}s:8:\"interval\";i:43200;}}}i:1696948498;a:2:{s:30:\"wp_site_health_scheduled_check\";a:1:{s:32:\"40cd750bba9870f18aada2478b24840a\";a:3:{s:8:\"schedule\";s:6:\"weekly\";s:4:\"args\";a:0:{}s:8:\"interval\";i:604800;}}s:32:\"recovery_mode_clean_expired_keys\";a:1:{s:32:\"40cd750bba9870f18aada2478b24840a\";a:3:{s:8:\"schedule\";s:5:\"daily\";s:4:\"args\";a:0:{}s:8:\"interval\";i:86400;}}}i:1696948515;a:2:{s:19:\"wp_scheduled_delete\";a:1:{s:32:\"40cd750bba9870f18aada2478b24840a\";a:3:{s:8:\"schedule\";s:5:\"daily\";s:4:\"args\";a:0:{}s:8:\"interval\";i:86400;}}s:25:\"delete_expired_transients\";a:1:{s:32:\"40cd750bba9870f18aada2478b24840a\";a:3:{s:8:\"schedule\";s:5:\"daily\";s:4:\"args\";a:0:{}s:8:\"interval\";i:86400;}}}i:1696948516;a:1:{s:30:\"wp_scheduled_auto_draft_delete\";a:1:{s:32:\"40cd750bba9870f18aada2478b24840a\";a:3:{s:8:\"schedule\";s:5:\"daily\";s:4:\"args\";a:0:{}s:8:\"interval\";i:86400;}}}i:1697466926;a:1:{s:30:\"wp_delete_temp_updater_backups\";a:1:{s:32:\"40cd750bba9870f18aada2478b24840a\";a:3:{s:8:\"schedule\";s:6:\"weekly\";s:4:\"args\";a:0:{}s:8:\"interval\";i:604800;}}}s:7:\"version\";i:2;}', 'yes'),
+(106, 'widget_pages', 'a:1:{s:12:\"_multiwidget\";i:1;}', 'yes'),
+(107, 'widget_calendar', 'a:1:{s:12:\"_multiwidget\";i:1;}', 'yes'),
+(108, 'widget_archives', 'a:1:{s:12:\"_multiwidget\";i:1;}', 'yes'),
+(109, 'widget_media_audio', 'a:1:{s:12:\"_multiwidget\";i:1;}', 'yes'),
+(110, 'widget_media_image', 'a:1:{s:12:\"_multiwidget\";i:1;}', 'yes'),
+(111, 'widget_media_gallery', 'a:1:{s:12:\"_multiwidget\";i:1;}', 'yes'),
+(112, 'widget_media_video', 'a:1:{s:12:\"_multiwidget\";i:1;}', 'yes'),
+(113, 'widget_meta', 'a:1:{s:12:\"_multiwidget\";i:1;}', 'yes'),
+(114, 'widget_search', 'a:1:{s:12:\"_multiwidget\";i:1;}', 'yes'),
+(115, 'widget_recent-posts', 'a:1:{s:12:\"_multiwidget\";i:1;}', 'yes'),
+(116, 'widget_recent-comments', 'a:1:{s:12:\"_multiwidget\";i:1;}', 'yes'),
+(117, 'widget_tag_cloud', 'a:1:{s:12:\"_multiwidget\";i:1;}', 'yes'),
+(118, 'widget_nav_menu', 'a:1:{s:12:\"_multiwidget\";i:1;}', 'yes'),
+(119, 'widget_custom_html', 'a:1:{s:12:\"_multiwidget\";i:1;}', 'yes'),
+(120, 'nonce_key', ':@mS9DsY,vFI=iAKiwy;g$hm^l d4nE(_%Bd+>v2K`d;Km2Me;mO9[f_-Q Zh=[Q', 'no'),
+(121, 'nonce_salt', '/?#2ig*c_e_}-[mel-$?%U;;@}oC9TF%|l /Df%)V@kV~|$Lo[bKcL,W{y-]4%S>', 'no'),
+(122, 'recovery_keys', 'a:0:{}', 'yes'),
+(123, 'theme_mods_twentytwentythree', 'a:1:{s:18:\"custom_css_post_id\";i:-1;}', 'yes'),
+(124, 'db_upgraded', '', 'yes'),
+(125, 'can_compress_scripts', '1', 'yes'),
+(126, 'WishListMemberOptions_Migrated', '1', 'yes'),
+(127, 'widget_wishlistwidget', 'a:1:{s:12:\"_multiwidget\";i:1;}', 'yes'),
+(128, 'WishListMemberOptions_MigrateLevelData', '1', 'yes'),
+(129, 'WishListMemberOptions_MigrateContentLevelData', '1', 'yes');
+
+DROP TABLE IF EXISTS `wp_postmeta`;
+CREATE TABLE `wp_postmeta` (
+ `meta_id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
+ `post_id` bigint(20) unsigned NOT NULL DEFAULT '0',
+ `meta_key` varchar(255) COLLATE utf8mb4_unicode_520_ci DEFAULT NULL,
+ `meta_value` longtext COLLATE utf8mb4_unicode_520_ci,
+ PRIMARY KEY (`meta_id`),
+ KEY `post_id` (`post_id`),
+ KEY `meta_key` (`meta_key`(191))
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci;
+
+DROP TABLE IF EXISTS `wp_posts`;
+CREATE TABLE `wp_posts` (
+ `ID` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
+ `post_author` bigint(20) unsigned NOT NULL DEFAULT '0',
+ `post_date` datetime NOT NULL DEFAULT '0000-00-00 00:00:00',
+ `post_date_gmt` datetime NOT NULL DEFAULT '0000-00-00 00:00:00',
+ `post_content` longtext COLLATE utf8mb4_unicode_520_ci NOT NULL,
+ `post_title` text COLLATE utf8mb4_unicode_520_ci NOT NULL,
+ `post_excerpt` text COLLATE utf8mb4_unicode_520_ci NOT NULL,
+ `post_status` varchar(20) COLLATE utf8mb4_unicode_520_ci NOT NULL DEFAULT 'publish',
+ `comment_status` varchar(20) COLLATE utf8mb4_unicode_520_ci NOT NULL DEFAULT 'open',
+ `ping_status` varchar(20) COLLATE utf8mb4_unicode_520_ci NOT NULL DEFAULT 'open',
+ `post_password` varchar(255) COLLATE utf8mb4_unicode_520_ci NOT NULL DEFAULT '',
+ `post_name` varchar(200) COLLATE utf8mb4_unicode_520_ci NOT NULL DEFAULT '',
+ `to_ping` text COLLATE utf8mb4_unicode_520_ci NOT NULL,
+ `pinged` text COLLATE utf8mb4_unicode_520_ci NOT NULL,
+ `post_modified` datetime NOT NULL DEFAULT '0000-00-00 00:00:00',
+ `post_modified_gmt` datetime NOT NULL DEFAULT '0000-00-00 00:00:00',
+ `post_content_filtered` longtext COLLATE utf8mb4_unicode_520_ci NOT NULL,
+ `post_parent` bigint(20) unsigned NOT NULL DEFAULT '0',
+ `guid` varchar(255) COLLATE utf8mb4_unicode_520_ci NOT NULL DEFAULT '',
+ `menu_order` int(11) NOT NULL DEFAULT '0',
+ `post_type` varchar(20) COLLATE utf8mb4_unicode_520_ci NOT NULL DEFAULT 'post',
+ `post_mime_type` varchar(100) COLLATE utf8mb4_unicode_520_ci NOT NULL DEFAULT '',
+ `comment_count` bigint(20) NOT NULL DEFAULT '0',
+ PRIMARY KEY (`ID`),
+ KEY `post_name` (`post_name`(191)),
+ KEY `type_status_date` (`post_type`,`post_status`,`post_date`,`ID`),
+ KEY `post_parent` (`post_parent`),
+ KEY `post_author` (`post_author`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci;
+
+DROP TABLE IF EXISTS `wp_term_relationships`;
+CREATE TABLE `wp_term_relationships` (
+ `object_id` bigint(20) unsigned NOT NULL DEFAULT '0',
+ `term_taxonomy_id` bigint(20) unsigned NOT NULL DEFAULT '0',
+ `term_order` int(11) NOT NULL DEFAULT '0',
+ PRIMARY KEY (`object_id`,`term_taxonomy_id`),
+ KEY `term_taxonomy_id` (`term_taxonomy_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci;
+
+INSERT INTO `wp_term_relationships` (`object_id`, `term_taxonomy_id`, `term_order`) VALUES
+(1, 1, 0);
+
+DROP TABLE IF EXISTS `wp_term_taxonomy`;
+CREATE TABLE `wp_term_taxonomy` (
+ `term_taxonomy_id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
+ `term_id` bigint(20) unsigned NOT NULL DEFAULT '0',
+ `taxonomy` varchar(32) COLLATE utf8mb4_unicode_520_ci NOT NULL DEFAULT '',
+ `description` longtext COLLATE utf8mb4_unicode_520_ci NOT NULL,
+ `parent` bigint(20) unsigned NOT NULL DEFAULT '0',
+ `count` bigint(20) NOT NULL DEFAULT '0',
+ PRIMARY KEY (`term_taxonomy_id`),
+ UNIQUE KEY `term_id_taxonomy` (`term_id`,`taxonomy`),
+ KEY `taxonomy` (`taxonomy`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci;
+
+INSERT INTO `wp_term_taxonomy` (`term_taxonomy_id`, `term_id`, `taxonomy`, `description`, `parent`, `count`) VALUES
+(1, 1, 'category', '', 0, 1);
+
+DROP TABLE IF EXISTS `wp_termmeta`;
+CREATE TABLE `wp_termmeta` (
+ `meta_id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
+ `term_id` bigint(20) unsigned NOT NULL DEFAULT '0',
+ `meta_key` varchar(255) COLLATE utf8mb4_unicode_520_ci DEFAULT NULL,
+ `meta_value` longtext COLLATE utf8mb4_unicode_520_ci,
+ PRIMARY KEY (`meta_id`),
+ KEY `term_id` (`term_id`),
+ KEY `meta_key` (`meta_key`(191))
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci;
+
+
+DROP TABLE IF EXISTS `wp_terms`;
+CREATE TABLE `wp_terms` (
+ `term_id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
+ `name` varchar(200) COLLATE utf8mb4_unicode_520_ci NOT NULL DEFAULT '',
+ `slug` varchar(200) COLLATE utf8mb4_unicode_520_ci NOT NULL DEFAULT '',
+ `term_group` bigint(10) NOT NULL DEFAULT '0',
+ PRIMARY KEY (`term_id`),
+ KEY `slug` (`slug`(191)),
+ KEY `name` (`name`(191))
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci;
+
+INSERT INTO `wp_terms` (`term_id`, `name`, `slug`, `term_group`) VALUES
+(1, 'Uncategorized', 'uncategorized', 0);
+
+DROP TABLE IF EXISTS `wp_usermeta`;
+CREATE TABLE `wp_usermeta` (
+ `umeta_id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
+ `user_id` bigint(20) unsigned NOT NULL DEFAULT '0',
+ `meta_key` varchar(255) COLLATE utf8mb4_unicode_520_ci DEFAULT NULL,
+ `meta_value` longtext COLLATE utf8mb4_unicode_520_ci,
+ PRIMARY KEY (`umeta_id`),
+ KEY `user_id` (`user_id`),
+ KEY `meta_key` (`meta_key`(191))
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci;
+
+INSERT INTO `wp_usermeta` (`umeta_id`, `user_id`, `meta_key`, `meta_value`) VALUES
+(1, 1, 'nickname', 'admin'),
+(2, 1, 'first_name', ''),
+(3, 1, 'last_name', ''),
+(4, 1, 'description', ''),
+(5, 1, 'rich_editing', 'true'),
+(6, 1, 'syntax_highlighting', 'true'),
+(7, 1, 'comment_shortcuts', 'false'),
+(8, 1, 'admin_color', 'fresh'),
+(9, 1, 'use_ssl', '0'),
+(10, 1, 'show_admin_bar_front', 'true'),
+(11, 1, 'locale', ''),
+(12, 1, 'wp_capabilities', 'a:1:{s:13:\"administrator\";b:1;}'),
+(13, 1, 'wp_user_level', '10'),
+(14, 1, 'dismissed_wp_pointers', ''),
+(15, 1, 'show_welcome_panel', '1'),
+(16, 1, 'session_tokens', 'a:1:{s:64:\"d1edb8c7d17dc41fa6de9833631a6381dca0306f20dfd4b64947e6b8818dd16e\";a:4:{s:10:\"expiration\";i:1676810217;s:2:\"ip\";s:9:\"127.0.0.1\";s:2:\"ua\";s:117:\"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36\";s:5:\"login\";i:1676637417;}}'),
+(17, 1, 'wp_user-settings', 'unfold=1&ampmfold=o&ampeditor=html&amplibraryContent=browse&ampsiteorigin_panels_setting_tab=widgets&libraryContent=browse&editor=tinymce&libraryContent=browse&editor=tinymce&siteorigin_panels_setting_tab=welcome'),
+(18, 1, 'wp_user-settings-time', '1676637417'),
+(19, 1, 'wp_dashboard_quick_press_last_post_id', '1'),
+(20, 1, 'edit_page_per_page', '100'),
+(21, 1, 'edit_post_per_page', '100');
+
+DROP TABLE IF EXISTS `wp_users`;
+CREATE TABLE `wp_users` (
+ `ID` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
+ `user_login` varchar(60) COLLATE utf8mb4_unicode_520_ci NOT NULL DEFAULT '',
+ `user_pass` varchar(255) COLLATE utf8mb4_unicode_520_ci NOT NULL DEFAULT '',
+ `user_nicename` varchar(50) COLLATE utf8mb4_unicode_520_ci NOT NULL DEFAULT '',
+ `user_email` varchar(100) COLLATE utf8mb4_unicode_520_ci NOT NULL DEFAULT '',
+ `user_url` varchar(100) COLLATE utf8mb4_unicode_520_ci NOT NULL DEFAULT '',
+ `user_registered` datetime NOT NULL DEFAULT '0000-00-00 00:00:00',
+ `user_activation_key` varchar(255) COLLATE utf8mb4_unicode_520_ci NOT NULL DEFAULT '',
+ `user_status` int(11) NOT NULL DEFAULT '0',
+ `display_name` varchar(250) COLLATE utf8mb4_unicode_520_ci NOT NULL DEFAULT '',
+ PRIMARY KEY (`ID`),
+ KEY `user_login_key` (`user_login`),
+ KEY `user_nicename` (`user_nicename`),
+ KEY `user_email` (`user_email`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci;
+
+INSERT INTO `wp_users` (`ID`, `user_login`, `user_pass`, `user_nicename`, `user_email`, `user_url`, `user_registered`, `user_activation_key`, `user_status`, `display_name`) VALUES
+(1, 'admin', '$P$BPKHO1xSCwu6j57sJB/p7JndeBdRVd.', 'admin', 'dev-email@flywheel.local', 'http://convertkit.local', '2023-07-03 13:38:12', '', 0, 'admin');
+
+-- 2023-02-17 12:37:57
\ No newline at end of file
diff --git a/tests/_support/AcceptanceTester.php b/tests/_support/AcceptanceTester.php
new file mode 100644
index 0000000..a17e0fd
--- /dev/null
+++ b/tests/_support/AcceptanceTester.php
@@ -0,0 +1,27 @@
+{yourFunctionName}.
+ *
+ * @since 1.2.0
+ */
+class ConvertKitAPI extends \Codeception\Module
+{
+ /**
+ * Check the given email address exists as a subscriber, and optionally
+ * checks that the first name and custom fields contain the expected data.
+ *
+ * @since 1.2.0
+ *
+ * @param AcceptanceTester $I AcceptanceTester.
+ * @param string $emailAddress Email Address.
+ * @param bool|string $firstName First Name.
+ * @param bool|array $customFields Custom Fields.
+ */
+ public function apiCheckSubscriberExists($I, $emailAddress, $firstName = false, $customFields = false)
+ {
+ // Run request.
+ $results = $this->apiRequest(
+ 'subscribers',
+ 'GET',
+ [
+ 'email_address' => $emailAddress,
+ 'include_total_count' => true,
+
+ // Some test email addresses might bounce, so we want to check all subscriber states.
+ 'status' => 'all',
+ ]
+ );
+
+ // Check at least one subscriber was returned and it matches the email address.
+ $I->assertGreaterThan(0, $results['pagination']['total_count']);
+ $I->assertEquals($emailAddress, $results['subscribers'][0]['email_address']);
+
+ // If a first name was provided, check it matches.
+ if ($firstName) {
+ $I->assertEquals($firstName, $results['subscribers'][0]['first_name']);
+ }
+
+ // If custom fields are provided, check they exist.
+ if ($customFields) {
+ foreach ($customFields as $customField => $customFieldValue) {
+ $I->assertEquals($results['subscribers'][0]['fields'][ $customField ], $customFieldValue);
+ }
+ }
+ }
+
+ /**
+ * Check the given email address does not exists as a subscriber.
+ *
+ * @since 1.2.0
+ *
+ * @param AcceptanceTester $I AcceptanceTester.
+ * @param string $emailAddress Email Address.
+ */
+ public function apiCheckSubscriberDoesNotExist($I, $emailAddress)
+ {
+ // Run request.
+ $results = $this->apiRequest(
+ 'subscribers',
+ 'GET',
+ [
+ 'email_address' => $emailAddress,
+ 'include_total_count' => true,
+
+ // Some test email addresses might bounce, so we want to check all subscriber states.
+ 'status' => 'all',
+ ]
+ );
+
+ // Check no subscribers are returned by this request.
+ $I->assertEquals(0, $results['pagination']['total_count']);
+ }
+
+ /**
+ * Checks if the given email address has the given tag.
+ *
+ * @since 1.2.0
+ *
+ * @param AcceptanceTester $I AcceptanceTester.
+ * @param string $emailAddress Email Address.
+ * @param string $tagID Tag ID.
+ */
+ public function apiCheckSubscriberHasTag($I, $emailAddress, $tagID)
+ {
+ // Get subscriber ID by email.
+ $subscriberID = $this->apiGetSubscriberIDByEmail($emailAddress);
+
+ // Get subscriber tags.
+ $subscriberTags = $this->apiGetSubscriberTags($subscriberID);
+
+ $subscriberTagged = false;
+ foreach ($subscriberTags as $tag) {
+ if ( (int) $tag['id'] === (int) $tagID) {
+ $subscriberTagged = true;
+ break;
+ }
+ }
+
+ // Check that the Subscriber is tagged.
+ $I->assertTrue($subscriberTagged);
+ }
+
+ /**
+ * Checks if the given email address does not have the given tag.
+ *
+ * @since 1.2.0
+ *
+ * @param AcceptanceTester $I AcceptanceTester.
+ * @param string $emailAddress Email Address.
+ * @param string $tagID Tag ID.
+ */
+ public function apiCheckSubscriberDoesNotHaveTag($I, $emailAddress, $tagID)
+ {
+ // Get subscriber ID by email.
+ $subscriberID = $this->apiGetSubscriberIDByEmail($emailAddress);
+
+ // Get subscriber tags.
+ $subscriberTags = $this->apiGetSubscriberTags($subscriberID);
+
+ $subscriberTagged = false;
+ foreach ($subscriberTags as $tag) {
+ if ( (int) $tag['id'] === (int) $tagID) {
+ $subscriberTagged = true;
+ break;
+ }
+ }
+
+ // Check that the Subscriber is not tagged.
+ $I->assertFalse($subscriberTagged);
+ }
+
+ /**
+ * Checks if the given email address has no tags in ConvertKit.
+ *
+ * @since 1.2.0
+ *
+ * @param AcceptanceTester $I AcceptanceTester.
+ * @param string $emailAddress Email Address.
+ */
+ public function apiCheckSubscriberHasNoTags($I, $emailAddress)
+ {
+ // Get subscriber ID by email.
+ $subscriberID = $this->apiGetSubscriberIDByEmail($emailAddress);
+
+ // Get subscriber tags.
+ $subscriberTags = $this->apiGetSubscriberTags($subscriberID);
+
+ // Confirm no tags exist.
+ $I->assertCount(0, $subscriberTags);
+ }
+
+ /**
+ * Returns the subscriber ID for the given email address from the API.
+ *
+ * @since 1.2.0
+ *
+ * @param string $emailAddress Subscriber Email Address.
+ * @return array
+ */
+ public function apiGetSubscriberIDByEmail($emailAddress)
+ {
+ $subscriber = $this->apiRequest(
+ 'subscribers',
+ 'GET',
+ [
+ 'email_address' => $emailAddress,
+ 'include_total_count' => true,
+
+ // Some test email addresses might bounce, so we want to check all subscriber states.
+ 'status' => 'all',
+ ]
+ );
+
+ return $subscriber['subscribers'][0]['id'];
+ }
+
+ /**
+ * Returns all tags for the given subscriber ID from the API.
+ *
+ * @since 1.2.0
+ *
+ * @param int $subscriberID Subscriber ID.
+ * @return array
+ */
+ public function apiGetSubscriberTags($subscriberID)
+ {
+ $tags = $this->apiRequest('subscribers/' . $subscriberID . '/tags');
+ return $tags['tags'];
+ }
+
+ /**
+ * Sends a request to the ConvertKit API, typically used to read an endpoint to confirm
+ * that data in an Acceptance Test was added/edited/deleted successfully.
+ *
+ * @since 1.2.0
+ *
+ * @param string $endpoint Endpoint.
+ * @param string $method Method (GET|POST|PUT).
+ * @param array $params Endpoint Parameters.
+ */
+ public function apiRequest($endpoint, $method = 'GET', $params = array())
+ {
+ // Send request.
+ $client = new \GuzzleHttp\Client();
+ switch ($method) {
+ case 'GET':
+ $result = $client->request(
+ $method,
+ 'https://api.convertkit.com/v4/' . $endpoint . '?' . http_build_query($params),
+ [
+ 'headers' => [
+ 'Authorization' => 'Bearer ' . $_ENV['CONVERTKIT_OAUTH_ACCESS_TOKEN'],
+ 'timeout' => 5,
+ ],
+ ]
+ );
+ break;
+
+ default:
+ $result = $client->request(
+ $method,
+ 'https://api.convertkit.com/v4/' . $endpoint,
+ [
+ 'headers' => [
+ 'Accept' => 'application/json',
+ 'Content-Type' => 'application/json; charset=utf-8',
+ 'Authorization' => 'Bearer ' . $_ENV['CONVERTKIT_OAUTH_ACCESS_TOKEN'],
+ 'timeout' => 5,
+ ],
+ 'body' => (string) json_encode($params), // phpcs:ignore WordPress.WP.AlternativeFunctions
+ ]
+ );
+ break;
+ }
+
+ // Return JSON decoded response.
+ return json_decode($result->getBody()->getContents(), true);
+ }
+}
diff --git a/tests/_support/Helper/Acceptance/Email.php b/tests/_support/Helper/Acceptance/Email.php
new file mode 100644
index 0000000..444896c
--- /dev/null
+++ b/tests/_support/Helper/Acceptance/Email.php
@@ -0,0 +1,25 @@
+{yourFunctionName}.
+ *
+ * @since 1.2.0
+ */
+class Email extends \Codeception\Module
+{
+ /**
+ * Generates a unique email address for use in a test, comprising of a prefix,
+ * date + time and PHP version number.
+ *
+ * This ensures that if tests are run in parallel, the same email address
+ * isn't used for two tests across parallel testing runs.
+ *
+ * @since 1.2.0
+ */
+ public function generateEmailAddress()
+ {
+ return 'wordpress-' . uniqid() . '-' . date( 'Y-m-d-H-i-s' ) . '-php-' . PHP_VERSION_ID . '@n7studios.com';
+ }
+}
diff --git a/tests/_support/Helper/Acceptance/Plugin.php b/tests/_support/Helper/Acceptance/Plugin.php
new file mode 100644
index 0000000..e62fb89
--- /dev/null
+++ b/tests/_support/Helper/Acceptance/Plugin.php
@@ -0,0 +1,37 @@
+{yourFunctionName}.
+ *
+ * @since 1.2.0
+ */
+class Plugin extends \Codeception\Module
+{
+ /**
+ * Helper method to activate the ConvertKit Plugin, checking
+ * it activated and no errors were output.
+ *
+ * @since 1.2.0
+ *
+ * @param AcceptanceTester $I AcceptanceTester.
+ */
+ public function activateConvertKitPlugin($I)
+ {
+ $I->activateThirdPartyPlugin($I, 'convertkit-membermouse');
+ }
+
+ /**
+ * Helper method to deactivate the ConvertKit Plugin, checking
+ * it activated and no errors were output.
+ *
+ * @since 1.2.0
+ *
+ * @param AcceptanceTester $I AcceptanceTester.
+ */
+ public function deactivateConvertKitPlugin($I)
+ {
+ $I->deactivateThirdPartyPlugin($I, 'convertkit-membermouse');
+ }
+}
diff --git a/tests/_support/Helper/Acceptance/ThirdPartyPlugin.php b/tests/_support/Helper/Acceptance/ThirdPartyPlugin.php
new file mode 100644
index 0000000..5e35dda
--- /dev/null
+++ b/tests/_support/Helper/Acceptance/ThirdPartyPlugin.php
@@ -0,0 +1,66 @@
+{yourFunctionName}.
+ *
+ * @since 1.9.6
+ */
+class ThirdPartyPlugin extends \Codeception\Module
+{
+ /**
+ * Helper method to activate a third party Plugin, checking
+ * it activated and no errors were output.
+ *
+ * @since 1.2.0
+ *
+ * @param AcceptanceTester $I AcceptanceTester.
+ * @param string $name Plugin Slug.
+ */
+ public function activateThirdPartyPlugin($I, $name)
+ {
+ // Login as the Administrator.
+ $I->loginAsAdmin();
+
+ // Go to the Plugins screen in the WordPress Administration interface.
+ $I->amOnPluginsPage();
+
+ // Activate the Plugin.
+ $I->activatePlugin($name);
+
+ // Go to the Plugins screen again; this prevents any Plugin that loads a wizard-style screen from
+ // causing seePluginActivated() to fail.
+ $I->amOnPluginsPage();
+
+ // Check that no PHP warnings or notices were output.
+ $I->checkNoWarningsAndNoticesOnScreen($I);
+ }
+
+ /**
+ * Helper method to activate a third party Plugin, checking
+ * it activated and no errors were output.
+ *
+ * @since 1.2.0
+ *
+ * @param AcceptanceTester $I Acceptance Tester.
+ * @param string $name Plugin Slug.
+ */
+ public function deactivateThirdPartyPlugin($I, $name)
+ {
+ // Login as the Administrator.
+ $I->loginAsAdmin();
+
+ // Go to the Plugins screen in the WordPress Administration interface.
+ $I->amOnPluginsPage();
+
+ // Deactivate the Plugin.
+ $I->deactivatePlugin($name);
+
+ // Wait for notice to display.
+ $I->waitForElementVisible('div.updated');
+
+ // Check that the Plugin deactivated successfully.
+ $I->seePluginDeactivated($name);
+ }
+}
diff --git a/tests/_support/Helper/Acceptance/Xdebug.php b/tests/_support/Helper/Acceptance/Xdebug.php
new file mode 100644
index 0000000..53f0e94
--- /dev/null
+++ b/tests/_support/Helper/Acceptance/Xdebug.php
@@ -0,0 +1,25 @@
+{yourFunctionName}.
+ *
+ * @since 1.2.0
+ */
+class Xdebug extends \Codeception\Module
+{
+ /**
+ * Helper method to assert that there are non PHP errors, warnings or notices output
+ *
+ * @since 1.2.0
+ *
+ * @param AcceptanceTester $I Acceptance Tester.
+ */
+ public function checkNoWarningsAndNoticesOnScreen($I)
+ {
+ // Check that no Xdebug errors exist.
+ $I->dontSeeElement('.xdebug-error');
+ $I->dontSeeElement('.xe-notice');
+ }
+}
diff --git a/tests/_support/Helper/Functional.php b/tests/_support/Helper/Functional.php
new file mode 100644
index 0000000..2e4e491
--- /dev/null
+++ b/tests/_support/Helper/Functional.php
@@ -0,0 +1,11 @@
+activateConvertKitPlugin($I);
+ $I->activateThirdPartyPlugin($I, 'membermouse-platform');
+
+ // Go to the Plugin's Settings > General Screen.
+ $I->amOnAdminPage('options-general.php?page=convertkit-mm');
+
+ // Check that no PHP warnings or notices were output.
+ $I->checkNoWarningsAndNoticesOnScreen($I);
+
+ $I->deactivateConvertKitPlugin($I);
+ $I->deactivateThirdPartyPlugin($I, 'membermouse-platform');
+ }
+
+ /**
+ * Test that activating the Plugin, without activating the MemberMouse Plugin, works
+ * with no errors.
+ *
+ * @since 1.2.0
+ *
+ * @param AcceptanceTester $I Tester.
+ */
+ public function testPluginActivationDeactivationWithoutMemberMouse(AcceptanceTester $I)
+ {
+ $I->activateConvertKitPlugin($I);
+
+ // Go to the Plugin's Settings > General Screen.
+ $I->amOnAdminPage('options-general.php?page=convertkit-mm');
+
+ // Check that no PHP warnings or notices were output.
+ $I->checkNoWarningsAndNoticesOnScreen($I);
+
+ $I->deactivateConvertKitPlugin($I);
+ }
+}
diff --git a/tests/functional.suite.yml b/tests/functional.suite.yml
new file mode 100644
index 0000000..140252e
--- /dev/null
+++ b/tests/functional.suite.yml
@@ -0,0 +1,40 @@
+# Codeception Test Suite Configuration
+#
+# Suite for functional tests
+# Emulate web requests and make WordPress process them
+
+actor: FunctionalTester
+modules:
+ enabled:
+ - WPDb
+ - WPBrowser
+ # - WPFilesystem
+ - Asserts
+ - \Helper\Functional
+ config:
+ WPDb:
+ dsn: '%TEST_SITE_DB_DSN%'
+ user: '%TEST_SITE_DB_USER%'
+ password: '%TEST_SITE_DB_PASSWORD%'
+ dump: 'tests/_data/dump.sql'
+ populate: true
+ cleanup: true
+ waitlock: 10
+ url: '%TEST_SITE_WP_URL%'
+ urlReplacement: true
+ tablePrefix: '%TEST_SITE_TABLE_PREFIX%'
+ WPBrowser:
+ url: '%TEST_SITE_WP_URL%'
+ adminUsername: '%TEST_SITE_ADMIN_USERNAME%'
+ adminPassword: '%TEST_SITE_ADMIN_PASSWORD%'
+ adminPath: '%TEST_SITE_WP_ADMIN_PATH%'
+ headers:
+ X_TEST_REQUEST: 1
+ X_WPBROWSER_REQUEST: 1
+
+ WPFilesystem:
+ wpRootFolder: '%WP_ROOT_FOLDER%'
+ plugins: '/wp-content/plugins'
+ mu-plugins: '/wp-content/mu-plugins'
+ themes: '/wp-content/themes'
+ uploads: '/wp-content/uploads'
\ No newline at end of file
diff --git a/tests/nginx/php-7.4.conf b/tests/nginx/php-7.4.conf
new file mode 100644
index 0000000..94ec299
--- /dev/null
+++ b/tests/nginx/php-7.4.conf
@@ -0,0 +1,13 @@
+server {
+ listen 80;
+ root /home/runner/work/convertkit-membermouse/convertkit-membermouse/wordpress;
+ server_name 127.0.0.1;
+ index index.php;
+ location / {
+ try_files $uri $uri/ /index.php?$args;
+ }
+ location ~ \.php$ {
+ include snippets/fastcgi-php.conf;
+ fastcgi_pass unix:/var/run/php/php7.4-fpm.sock;
+ }
+}
\ No newline at end of file
diff --git a/tests/nginx/php-8.0.conf b/tests/nginx/php-8.0.conf
new file mode 100644
index 0000000..e56ef39
--- /dev/null
+++ b/tests/nginx/php-8.0.conf
@@ -0,0 +1,13 @@
+server {
+ listen 80;
+ root /home/runner/work/convertkit-membermouse/convertkit-membermouse/wordpress;
+ server_name 127.0.0.1;
+ index index.php;
+ location / {
+ try_files $uri $uri/ /index.php?$args;
+ }
+ location ~ \.php$ {
+ include snippets/fastcgi-php.conf;
+ fastcgi_pass unix:/var/run/php/php8.0-fpm.sock;
+ }
+}
\ No newline at end of file
diff --git a/tests/nginx/php-8.1.conf b/tests/nginx/php-8.1.conf
new file mode 100644
index 0000000..3a33728
--- /dev/null
+++ b/tests/nginx/php-8.1.conf
@@ -0,0 +1,13 @@
+server {
+ listen 80;
+ root /home/runner/work/convertkit-membermouse/convertkit-membermouse/wordpress;
+ server_name 127.0.0.1;
+ index index.php;
+ location / {
+ try_files $uri $uri/ /index.php?$args;
+ }
+ location ~ \.php$ {
+ include snippets/fastcgi-php.conf;
+ fastcgi_pass unix:/var/run/php/php8.1-fpm.sock;
+ }
+}
\ No newline at end of file
diff --git a/tests/nginx/php-8.2.conf b/tests/nginx/php-8.2.conf
new file mode 100644
index 0000000..f071665
--- /dev/null
+++ b/tests/nginx/php-8.2.conf
@@ -0,0 +1,13 @@
+server {
+ listen 80;
+ root /home/runner/work/convertkit-membermouse/convertkit-membermouse/wordpress;
+ server_name 127.0.0.1;
+ index index.php;
+ location / {
+ try_files $uri $uri/ /index.php?$args;
+ }
+ location ~ \.php$ {
+ include snippets/fastcgi-php.conf;
+ fastcgi_pass unix:/var/run/php/php8.2-fpm.sock;
+ }
+}
\ No newline at end of file
diff --git a/tests/nginx/php-8.3.conf b/tests/nginx/php-8.3.conf
new file mode 100644
index 0000000..3e38366
--- /dev/null
+++ b/tests/nginx/php-8.3.conf
@@ -0,0 +1,13 @@
+server {
+ listen 80;
+ root /home/runner/work/convertkit-membermouse/convertkit-membermouse/wordpress;
+ server_name 127.0.0.1;
+ index index.php;
+ location / {
+ try_files $uri $uri/ /index.php?$args;
+ }
+ location ~ \.php$ {
+ include snippets/fastcgi-php.conf;
+ fastcgi_pass unix:/var/run/php/php8.3-fpm.sock;
+ }
+}
\ No newline at end of file
diff --git a/tests/unit.suite.yml b/tests/unit.suite.yml
new file mode 100644
index 0000000..46a00eb
--- /dev/null
+++ b/tests/unit.suite.yml
@@ -0,0 +1,10 @@
+# Codeception Test Suite Configuration
+#
+# Suite for unit tests not relying WordPress code.
+
+actor: UnitTester
+modules:
+ enabled:
+ - Asserts
+ - \Helper\Unit
+ step_decorators: ~
\ No newline at end of file
diff --git a/tests/wpunit.suite.yml b/tests/wpunit.suite.yml
new file mode 100644
index 0000000..fd88d3e
--- /dev/null
+++ b/tests/wpunit.suite.yml
@@ -0,0 +1,21 @@
+# Codeception Test Suite Configuration
+#
+# Suite for unit or integration tests that require WordPress functions and classes.
+
+actor: WpunitTester
+modules:
+ enabled:
+ - WPLoader
+ - \Helper\Wpunit
+ config:
+ WPLoader:
+ wpRootFolder: "%WP_ROOT_FOLDER%"
+ dbName: "%TEST_DB_NAME%"
+ dbHost: "%TEST_DB_HOST%"
+ dbUser: "%TEST_DB_USER%"
+ dbPassword: "%TEST_DB_PASSWORD%"
+ tablePrefix: "%TEST_TABLE_PREFIX%"
+ domain: "%TEST_SITE_WP_DOMAIN%"
+ adminEmail: "%TEST_SITE_ADMIN_EMAIL%"
+ title: "Test"
+ plugins: ['convertkit-membermouse/convertkit-membermouse.php'] # Change to the repository Plugin that we're testing.
\ No newline at end of file