diff --git a/.env.testing b/.env.testing
index 3d2af61..dc32f71 100644
--- a/.env.testing
+++ b/.env.testing
@@ -1,5 +1,6 @@
-TEST_SITE_DB_DSN=mysql:host=mysql;dbname=wordpress
-TEST_SITE_DB_HOST=mysql
+TEST_SITE_DB_DSN=mysql:host=codecept_db;dbname=wordpress
+TEST_SITE_DB_HOST=codecept_db
+TEST_SITE_DB_PORT=3306
TEST_SITE_DB_NAME=wordpress
TEST_SITE_DB_USER=wordpress
TEST_SITE_DB_PASSWORD=password
@@ -9,10 +10,10 @@ TEST_SITE_ADMIN_PASSWORD=password
TEST_SITE_WP_ADMIN_PATH=/wp-admin
WP_ROOT_FOLDER=/var/www/html
TEST_DB_NAME=wordpress
-TEST_DB_HOST=mysql
+TEST_DB_HOST=codecept_db
TEST_DB_USER=wordpress
TEST_DB_PASSWORD=password
TEST_TABLE_PREFIX=wp_
-TEST_SITE_WP_URL=localhost:8080
-TEST_SITE_WP_DOMAIN=localhost:8080
+TEST_SITE_WP_URL=http://localhost
+TEST_SITE_WP_DOMAIN=localhost
TEST_SITE_ADMIN_EMAIL=admin@example.com
diff --git a/.github/workflows/continous-integration.yml b/.github/workflows/continous-integration.yml
index bce3812..54b312f 100644
--- a/.github/workflows/continous-integration.yml
+++ b/.github/workflows/continous-integration.yml
@@ -50,13 +50,20 @@ jobs:
composer install
composer require codeception/module-asserts:* \
codeception/util-universalframework:* \
- codeception/module-rest:* \
+ codeception/module-cli:* \
+ codeception/module-db:* \
+ codeception/module-filesystem:* \
+ codeception/module-phpbrowser:* \
+ codeception/module-webdriver:* \
+ wp-cli/wp-cli-bundle \
lucatume/wp-browser:^3.1
- name: Run Codeception Tests w/ Docker.
env:
PHP_VERSION: ${{ matrix.php }}
- run: composer run-codeception -- -- --coverage --coverage-xml
+ run: |
+ composer run-codeception -- -- functional
+ composer run-codeception -- -- wpunit --coverage --coverage-xml
@@ -65,8 +72,7 @@ jobs:
COVERALLS_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
docker-compose run --rm \
- --workdir=/var/www/html/wp-content/plugins/wp-graphql-testcase \
--user $(id -u) \
-e COVERALLS_REPO_TOKEN=$COVERALLS_REPO_TOKEN \
- wordpress \
+ codeception_testing \
vendor/bin/php-coveralls -v
diff --git a/.gitignore b/.gitignore
index b033d2f..a08b9f5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,12 +4,13 @@
.vscode
# Composer files.
-vendor/
+vendor/*
# Local test configuration files.
codeception.yml
tests/*.suite.yml
-.env.testing.local
+.env.*
+!.env.testing
# Patchwork cache folder.
cache/*
@@ -21,6 +22,7 @@ composer.lock
# Some local vim files
tags
*.swp
+*.sql
# Release notes
.rel
diff --git a/codeception.dist.yml b/codeception.dist.yml
index 0af5afc..0b895ef 100644
--- a/codeception.dist.yml
+++ b/codeception.dist.yml
@@ -9,7 +9,8 @@ coverage:
enabled: true
include:
- src/*
- exclude:
+ exclude:
+ - src/Codeception/Module/*
- src/TestCase/WPGraphQLUnitTestCase.php
- src/Logger/PHPUnitLogger.php
show_only_summary: false
@@ -26,3 +27,4 @@ extensions:
- Codeception\Command\GenerateWPXMLRPC
params:
- .env.testing
+ - .env.docker
\ No newline at end of file
diff --git a/composer.json b/composer.json
index a508dc2..1d62007 100644
--- a/composer.json
+++ b/composer.json
@@ -17,24 +17,31 @@
"src/"
]
},
+ "repositories": [
+ {
+ "type": "composer",
+ "url": "https://wpackagist.org"
+ }
+ ],
"require": {
"php-extended/polyfill-php80-str-utils": "^1.3"
},
"require-dev": {
"composer/installers": "^1.9",
"johnpbloch/wordpress": "^6.1",
- "wp-graphql/wp-graphql": "^1.1.8",
"squizlabs/php_codesniffer": "^3.5",
"automattic/vipwpcs": "^2.3",
"wp-coding-standards/wpcs": "^2.3",
- "php-coveralls/php-coveralls": "2.4.3"
+ "php-coveralls/php-coveralls": "2.4.3",
+ "wpackagist-plugin/wp-graphql": "^1.26"
},
"scripts": {
- "cli": "docker-compose run --rm --workdir=/var/www/html/wp-content/plugins/wp-graphql-testcase --user $(id -u) wordpress wait_for_it $TEST_DB -s -t 300 --",
- "codeception": "codecept run wpunit --",
+ "run_phpunit_env": "docker-compose run --rm --workdir=/var/www/html/wp-content/plugins/wp-graphql-testcase --user $(id -u) wp_phpunit_testing wait-for-it $TEST_DB -s -t 300 --",
+ "run_codecept_env": "docker-compose run --rm --user $(id -u) codeception_testing wait-for-it $TEST_DB -s -t 300 --",
+ "codeception": "codecept run --",
"phpunit": "phpunit --",
- "run-codeception": "env TEST_DB=mysql:3306 composer cli vendor/bin/codecept run wpunit",
- "run-phpunit": "env TEST_DB=mysql_phpunit:3306 composer cli vendor/bin/phpunit"
+ "run-codeception": "env TEST_DB=codecept_db:3306 composer run_codecept_env vendor/bin/codecept run",
+ "run-phpunit": "env TEST_DB=phpunit_db:3306 composer run_phpunit_env vendor/bin/phpunit"
},
"extra": {
"wordpress-install-dir": "local/public",
@@ -45,13 +52,16 @@
"suggest": {
"codeception/module-asserts": "Needed for \\Tests\\WPGraphQL\\TestCase\\WPGraphQLTestcase to work.",
"codeception/util-universalframework": "Needed for \\Tests\\WPGraphQL\\TestCase\\WPGraphQLTestcase to work.",
- "codeception/module-rest": "Needed for \\Tests\\WPGraphQL\\TestCase\\WPGraphQLTestcase to work.",
"lucatume/wp-browser": "Needed for \\Tests\\WPGraphQL\\TestCase\\WPGraphQLTestcase to work.",
"phpunit/phpunit": "Needed for \\Tests\\WPGraphQL\\TestCase\\WPGraphQLUnitTestcase to work.",
"wp-phpunit/wp-phpunit": "Needed for \\Tests\\WPGraphQL\\TestCase\\WPGraphQLUnitTestcase to work.",
- "yoast/phpunit-polyfills": "Needed for \\Tests\\WPGraphQL\\TestCase\\WPGraphQLUnitTestcase to work."
+ "yoast/phpunit-polyfills": "Needed for \\Tests\\WPGraphQL\\TestCase\\WPGraphQLUnitTestcase to work.",
+ "guzzlehttp/guzzle": "Needed for \\Tests\\WPGraphQL\\Codeception\\Module\\WPGraphQL to work."
},
"config": {
+ "optimize-autoloader": true,
+ "process-timeout": 0,
+ "sort-packages": true,
"allow-plugins": {
"composer/installers": true,
"johnpbloch/wordpress-core-installer": true,
diff --git a/docker-compose.yml b/docker-compose.yml
index 2cb0395..8ad413e 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -2,40 +2,82 @@ version: '3'
services:
- mysql:
+ codecept_db:
image: mariadb:10.2
environment:
MYSQL_ROOT_PASSWORD: password
MYSQL_DATABASE: wordpress
MYSQL_USER: wordpress
MYSQL_PASSWORD: password
+ networks:
+ codecept:
+ aliases:
+ - codecept_db
- wordpress:
- image: wp-graphql/wordpress:${WP_VERSION:-latest}
+ codeception_testing:
+ image: wp-graphql/codeception-testing:${WP_VERSION:-latest}
build:
context: ./docker
+ dockerfile: codeception.Dockerfile
args:
PHP_VERSION: ${PHP_VERSION:-8.0}
depends_on:
- - mysql
- - mysql_phpunit
+ - codecept_db
ports:
- "8080:80"
volumes:
- ./local/public:/var/www/html # WP core files.
- .:/var/www/html/wp-content/plugins/wp-graphql-testcase
- - ./local/config/wp-config.php:/var/www/html/wp-config.php
- - ./local/config/wp-tests-config.php:/var/www/html/wp-tests-config.php
+ - ./local/config/.htaccess:/var/www/html/.htaccess
+ - ./local/config/enable-app-passwords.php:/var/www/html/wp-content/mu-plugins/enable-app-passwords.php
+ env_file: .env.testing
environment:
+ WORDPRESS_DOMAIN: localhost
COMPOSER_HOME: /tmp/.composer
APACHE_RUN_USER: "#1000" # Ensure Apache can write to the filesystem.
- WP_TESTS_DIR: /var/www/html/wp-content/plugins/wp-graphql-testcase/vendor/wp-phpunit/wp-phpunit
- WP_PHPUNIT__TESTS_CONFIG: /var/www/html/wp-tests-config.php
+ networks:
+ codecept:
+ aliases:
+ - codeception_testing
- mysql_phpunit:
+ phpunit_db:
image: mariadb:10.2
restart: always
environment:
MYSQL_ALLOW_EMPTY_PASSWORD: "yes"
MYSQL_DATABASE: "wordpress"
- MYSQL_ROOT_PASSWORD: ""
\ No newline at end of file
+ MYSQL_ROOT_PASSWORD: ""
+ networks:
+ phpunit:
+ aliases:
+ - phpunit_db
+
+ wp_phpunit_testing:
+ image: wp-graphql/wp-phpunit-testing:${WP_VERSION:-latest}
+ build:
+ context: ./docker
+ dockerfile: wp-phpunit.Dockerfile
+ args:
+ PHP_VERSION: ${PHP_VERSION:-8.0}
+ depends_on:
+ - phpunit_db
+ ports:
+ - "8081:80"
+ volumes:
+ - ./local/public:/var/www/html # WP core files.
+ - .:/var/www/html/wp-content/plugins/wp-graphql-testcase
+ - ./local/config/wp-tests-config.php:/var/www/html/wp-tests-config.php
+ env_file: .env.testing
+ environment:
+ COMPOSER_HOME: /tmp/.composer
+ APACHE_RUN_USER: "#1000" # Ensure Apache can write to the filesystem.
+ WP_TESTS_DIR: /var/www/html/wp-content/plugins/wp-graphql-testcase/vendor/wp-phpunit/wp-phpunit
+ WP_PHPUNIT__TESTS_CONFIG: /var/www/html/wp-tests-config.php
+ networks:
+ phpunit:
+ aliases:
+ - wp_phpunit_testing
+
+networks:
+ phpunit:
+ codecept:
\ No newline at end of file
diff --git a/docker/codeception.Dockerfile b/docker/codeception.Dockerfile
new file mode 100644
index 0000000..a765746
--- /dev/null
+++ b/docker/codeception.Dockerfile
@@ -0,0 +1,54 @@
+ARG PHP_VERSION=8.1
+
+FROM wordpress:php${PHP_VERSION}-apache
+
+# See: https://xdebug.org/docs/compat to match the Xdebug version with the PHP version.
+ARG XDEBUG_VERSION=3.3.1
+
+RUN apt-get update; \
+ apt-get install -y --no-install-recommends \
+ # WP-CLI dependencies.
+ bash less default-mysql-client git \
+ # MailHog dependencies.
+ msmtp;
+
+COPY php.ini /usr/local/etc/php/php.ini
+
+RUN pecl install "xdebug-${XDEBUG_VERSION}"; \
+ docker-php-ext-enable xdebug
+
+ENV XDEBUG_MODE=coverage
+
+# Install PDO MySQL driver.
+RUN docker-php-ext-install \
+ pdo_mysql
+
+ENV WP_ROOT_FOLDER="/var/www/html"
+ENV WORDPRESS_DB_HOST=${TEST_SITE_DB_HOST}
+ENV WORDPRESS_DB_PORT=${TEST_SITE_DB_PORT}
+ENV WORDPRESS_DB_USER=${TEST_SITE_DB_USER}
+ENV WORDPRESS_DB_PASSWORD=${TEST_SITE_DB_PASSWORD}
+ENV WORDPRESS_DB_NAME=${TEST_SITE_DB_NAME}
+ENV PLUGINS_DIR="${WP_ROOT_FOLDER}/wp-content/plugins"
+ENV PROJECT_DIR="${PLUGINS_DIR}/wp-graphql-testcase"
+
+WORKDIR $PROJECT_DIR
+
+# Set up Apache
+RUN echo 'ServerName localhost' >> /etc/apache2/apache2.conf
+RUN a2enmod rewrite
+
+ADD https://raw.githubusercontent.com/vishnubob/wait-for-it/master/wait-for-it.sh /usr/local/bin/wait-for-it
+RUN chmod 755 /usr/local/bin/wait-for-it
+
+ADD https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar /usr/local/bin/wp
+RUN chmod 755 /usr/local/bin/wp
+
+# Remove exec statement from base entrypoint script.
+RUN sed -i '$d' /usr/local/bin/docker-entrypoint.sh
+
+# Set up entrypoint
+COPY entrypoint.sh /usr/local/bin/app-entrypoint.sh
+RUN chmod 755 /usr/local/bin/app-entrypoint.sh
+ENTRYPOINT ["app-entrypoint.sh"]
+CMD ["apache2-foreground"]
\ No newline at end of file
diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh
new file mode 100644
index 0000000..38440a7
--- /dev/null
+++ b/docker/entrypoint.sh
@@ -0,0 +1,81 @@
+#!/bin/bash
+
+
+work_dir=$(pwd)
+
+cd "${WP_ROOT_FOLDER}" || exit
+
+# Run WordPress docker entrypoint.
+# shellcheck disable=SC1091
+. docker-entrypoint.sh 'apache2'
+
+set +u
+
+# Ensure mysql is loaded
+wait-for-it -s -t 300 "${TEST_SITE_DB_HOST}:${TEST_SITE_DB_PORT:-3306}" -- echo "Application database is operationally..."
+
+if [ -f "${WP_ROOT_FOLDER}/wp-config.php" ]; then
+ echo "Deleting old wp-config.php"
+ rm -rf "${WP_ROOT_FOLDER}/wp-config.php"
+fi
+
+echo "Creating wp-config.php..."
+wp config create \
+ --path="${WP_ROOT_FOLDER}" \
+ --dbname="${TEST_SITE_DB_NAME}" \
+ --dbuser="${TEST_SITE_DB_USER}" \
+ --dbpass="${TEST_SITE_DB_PASSWORD}" \
+ --dbhost="${TEST_SITE_DB_HOST}" \
+ --dbprefix="${TEST_TABLE_PREFIX}" \
+ --skip-check \
+ --quiet \
+ --allow-root
+
+# Install WP if not yet installed
+echo "Installing WordPress..."
+wp core install \
+ --path="${WP_ROOT_FOLDER}" \
+ --url="${TEST_SITE_WP_URL}" \
+ --title='Test' \
+ --admin_user="${TEST_SITE_ADMIN_USERNAME}" \
+ --admin_password="${TEST_SITE_ADMIN_PASSWORD}" \
+ --admin_email="${TEST_SITE_ADMIN_EMAIL}" \
+ --allow-root
+
+wp plugin activate wp-graphql --allow-root
+
+if [ -f "${PROJECT_DIR}/tests/codeception/_data/dump.sql" ]; then
+ rm -rf "${PROJECT_DIR}/tests/codeception/_data/dump.sql"
+fi
+
+wp user application-password delete 1 --all --allow-root
+
+app_user="admin"
+app_password=$(wp user application-password create 1 testing --porcelain --allow-root)
+
+echo "Creating .env.docker file..."
+echo TEST_SITE_ADMIN_APP_PASSWORD="$(echo -n "${app_user}:${app_password}" | base64)" > "$PROJECT_DIR/.env.docker"
+echo TEST_SITE_WP_DOMAIN="${TEST_SITE_WP_DOMAIN}" >> "$PROJECT_DIR/.env.docker"
+echo TEST_SITE_WP_URL="${TEST_SITE_WP_URL}" >> "$PROJECT_DIR/.env.docker"
+
+echo "Dumping app database..."
+wp db export "${PROJECT_DIR}/tests/codeception/_data/dump.sql" \
+ --dbuser="${TEST_SITE_DB_USER}" \
+ --dbpass="${TEST_SITE_DB_PASSWORD}" \
+ --skip-plugins \
+ --skip-themes \
+ --allow-root
+
+wp config set WP_SITEURL "${TEST_SITE_WP_URL}" --allow-root
+wp config set WP_HOME "${TEST_SITE_WP_URL}" --allow-root
+
+echo "Setting pretty permalinks..."
+wp rewrite structure '/%year%/%monthnum%/%postname%/' --allow-root
+
+service apache2 start
+
+echo "Running WordPress version: $(wp core version --allow-root) at $(wp option get home --allow-root)"
+
+cd "${work_dir}" || exit
+
+exec "$@"
\ No newline at end of file
diff --git a/docker/Dockerfile b/docker/wp-phpunit.Dockerfile
similarity index 87%
rename from docker/Dockerfile
rename to docker/wp-phpunit.Dockerfile
index e95ee7e..66a1613 100644
--- a/docker/Dockerfile
+++ b/docker/wp-phpunit.Dockerfile
@@ -23,5 +23,5 @@ ENV XDEBUG_MODE=coverage
RUN docker-php-ext-install \
pdo_mysql
-ADD https://raw.githubusercontent.com/vishnubob/wait-for-it/master/wait-for-it.sh /usr/local/bin/wait_for_it
-RUN chmod 755 /usr/local/bin/wait_for_it
\ No newline at end of file
+ADD https://raw.githubusercontent.com/vishnubob/wait-for-it/master/wait-for-it.sh /usr/local/bin/wait-for-it
+RUN chmod 755 /usr/local/bin/wait-for-it
\ No newline at end of file
diff --git a/local/config/.htaccess b/local/config/.htaccess
new file mode 100644
index 0000000..2130d24
--- /dev/null
+++ b/local/config/.htaccess
@@ -0,0 +1,15 @@
+# BEGIN WordPress
+# The directives (lines) between "BEGIN WordPress" and "END WordPress" are
+# dynamically generated, and should only be modified via WordPress filters.
+# Any changes to the directives between these markers will be overwritten.
+
+RewriteEngine On
+RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
+RewriteBase /
+RewriteRule ^index\.php$ - [L]
+RewriteCond %{REQUEST_FILENAME} !-f
+RewriteCond %{REQUEST_FILENAME} !-d
+RewriteRule . /index.php [L]
+
+
+# END WordPress
diff --git a/local/config/enable-app-passwords.php b/local/config/enable-app-passwords.php
new file mode 100644
index 0000000..334b4a0
--- /dev/null
+++ b/local/config/enable-app-passwords.php
@@ -0,0 +1,7 @@
+
src/TestCase/WPGraphQLTestCase.php
src/Logger/CodeceptLogger.php
+ src/Codeception
vendor/
local/
diff --git a/src/Codeception/Module/QueryAsserts.php b/src/Codeception/Module/QueryAsserts.php
new file mode 100644
index 0000000..7ebebca
--- /dev/null
+++ b/src/Codeception/Module/QueryAsserts.php
@@ -0,0 +1,193 @@
+logger = new Signal();
+ }
+
+ /**
+ * Copy of "GraphQLRelay\Relay::toGlobalId()" function.
+ *
+ * @param string $type The type of the object.
+ * @param string $id The ID of the object.
+ *
+ * @return string
+ */
+ public function asRelayId($type, $id) {
+ return base64_encode( $type . ':' . $id );
+ }
+
+ /**
+ * Returns an expected "Field" type data object.
+ *
+ * @param string $path Path to the data being tested.
+ * @param mixed $expected_value Expected value of the object being evaluted.
+ * @return array
+ */
+ public function expectField( string $path, $expected_value ) {
+ $type = $this->get_not() . 'FIELD';
+ return compact( 'type', 'path', 'expected_value' );
+ }
+
+ /**
+ * Returns an expected "Object" type data object.
+ *
+ * @param string $path Path to the data being tested.
+ * @param array $expected_value Expected value of the object being evaluted.
+ * @return array
+ */
+ public function expectObject( string $path, array $expected_value ) {
+ $type = $this->get_not() . 'OBJECT';
+ return compact( 'type', 'path', 'expected_value' );
+ }
+
+ /**
+ * Returns an expected "Node" type data object.
+ *
+ * @param string $path Path to the data being tested.
+ * @param array $expected_value Expected value of the node being evaluted.
+ * @param integer|null $expected_index Expected index of the node being evaluted.
+ * @return array
+ */
+ public function expectNode( string $path, array $expected_value, $expected_index = null ) {
+ $type = $this->get_not() . 'NODE';
+ return compact( 'type', 'path', 'expected_value', 'expected_index' );
+ }
+
+ /**
+ * Returns an expected "Edge" type data object.
+ *
+ * @param string $path Path to the data being tested.
+ * @param array $expected_value Expected value of the edge being evaluted.
+ * @param integer|null $expected_index Expected index of the edge being evaluted.
+ * @return array
+ */
+ public function expectEdge( string $path, array $expected_value, $expected_index = null ) {
+ $type = $this->get_not() . 'EDGE';
+ return compact( 'type', 'path', 'expected_value', 'expected_index' );
+ }
+
+ /**
+ * Triggers the "not" flag for the next expect*() call.
+ *
+ * @return WPGraphQLTestCommon
+ */
+ public function not() {
+ $this->not = '!';
+ return $this;
+ }
+
+ /**
+ * Clears the "not" flag and return the proper prefix.
+ *
+ * @return string
+ */
+ private function get_not() {
+ if ( ! $this->not ) {
+ return '';
+ }
+
+ $prefix = $this->not;
+ $this->not = null;
+ return $prefix;
+ }
+
+ /**
+ * Returns an expected "location" error data object.
+ *
+ * @param string $path Path to the data being tested.
+ * @return array
+ */
+ public function expectErrorPath( string $path ) {
+ $type = 'ERROR_PATH';
+ return compact( 'type', 'path' );
+ }
+
+ /**
+ * Returns an expected "Edge" type data object.
+ *
+ * @param string $path Path to the data being tested.
+ * @param int|null $search_type Expected index of the edge being evaluted.
+ * @return array
+ */
+ public function expectErrorMessage( string $needle, int $search_type = self::MESSAGE_EQUALS ) {
+ $type = 'ERROR_MESSAGE';
+ return compact( 'type', 'needle', 'search_type' );
+ }
+
+ /**
+ * Reports an error identified by $message if $response is not a valid GraphQL Response.
+ *
+ * @param array $response GraphQL query response object.
+ * @param string $message Error message.
+ * @return void
+ */
+ public function assertResponseIsValid( $response, $message = '' ) {
+ $this->assertThat(
+ $response,
+ new QueryConstraint( $this->logger ),
+ $message
+ );
+ }
+
+ /**
+ * Reports an error identified by $message if $response does not contain all data
+ * and specifications defined in the $expected array.
+ *
+ * @param array $response GraphQL query response.
+ * @param array $expected List of expected data objects.
+ * @param string $message Error message.
+ */
+ public function assertQuerySuccessful( array $response, array $expected = [], $message = '' ) {
+ $this->assertThat(
+ $response,
+ new QuerySuccessfulConstraint( $this->logger, $expected ),
+ $message
+ );
+ }
+
+ /**
+ * Reports an error identified by $message if $response does not contain the error
+ * specifications defined in the $expected array.
+ *
+ * @param array $response GraphQL query response.
+ * @param array $expected Expected error data.
+ * @param string $message Error message.
+ * @return void
+ */
+ public function assertQueryError( array $response, array $expected = [], $message = '' ) {
+ $this->assertThat(
+ $response,
+ new QueryErrorConstraint( $this->logger, $expected ),
+ $message
+ );
+ }
+}
\ No newline at end of file
diff --git a/src/Codeception/Module/WPGraphQL.php b/src/Codeception/Module/WPGraphQL.php
new file mode 100644
index 0000000..e6dfff5
--- /dev/null
+++ b/src/Codeception/Module/WPGraphQL.php
@@ -0,0 +1,367 @@
+
+ */
+ protected array $config = [
+ 'endpoint' => '',
+ 'auth_header' => '',
+ ];
+
+ protected array $requiredFields = [
+ 'endpoint',
+ ];
+
+ /** @var \GuzzleHttp\Client */
+ private $client = null;
+
+ /** @var \Tests\WPGraphQL\Logger\CodeceptLogger */
+ private $logger = null;
+
+ private function getHeaders() {
+ $headers = [ 'Content-Type' => 'application/json' ];
+ $auth_header = $this->config['auth_header'];
+ if ( ! empty( $auth_header ) ) {
+ $headers['Authorization'] = $auth_header;
+ }
+
+ return $headers;
+ }
+
+ /**
+ * Initializes the module
+ *
+ * @throws \Codeception\Exception\ModuleException Invalid endpoint.
+ *
+ * @return void
+ */
+ public function _before( TestInterface $test ) {
+ $endpoint = $this->config['endpoint'];
+ if ( empty( $endpoint ) ) {
+ throw new ModuleException( $this, 'Invalid endpoint.' );
+ }
+ $this->client = new \GuzzleHttp\Client(
+ [
+ 'base_uri' => $endpoint,
+ 'timeout' => 300,
+ ]
+ );
+ $this->logger = new \Tests\WPGraphQL\Logger\CodeceptLogger();
+ }
+
+ /**
+ * Sends a GET request to the GraphQL endpoint and returns a response
+ *
+ * @param string $query The GraphQL query to send.
+ * @param array $request_headers The headers to send with the request.
+ *
+ * @throws \Codeception\Exception\ModuleException Invalid endpoint | Invalid query.
+ *
+ * @return array
+ */
+ public function getRawRequest( $query, $request_headers = [] ) {
+ $endpoint = $this->config['endpoint'];
+ if ( empty( $endpoint ) ) {
+ throw new ModuleException( $this, 'Invalid endpoint.' );
+ }
+
+ if ( empty( $query ) ) {
+ throw new ModuleException( $this, 'Invalid query.' );
+ }
+
+ $headers = array_merge(
+ $this->getHeaders(),
+ $request_headers
+ );
+
+ $this->logger->logData( "GET request to {$endpoint} with query: {$query}" );
+ $this->logger->logData( "Headers: " . json_encode( $headers ) );
+
+ $response = $this->client->request( 'GET', "?query={$query}", [ 'headers' => $headers ] );
+
+ if ( empty( $response ) ) {
+ throw new ModuleException( $this, 'Invalid response.' );
+ }
+
+ $this->logger->logData( $response->getHeaders() );
+ $this->logger->logData( $response->getBody() );
+
+ return $response;
+ }
+
+ /**
+ * Sends a GET request to the GraphQL endpoint and returns the query results
+ *
+ * @param string $query The GraphQL query to send.
+ * @param array $request_headers The headers to send with the request.
+ *
+ * @throws \Codeception\Exception\ModuleException Invalid response | Empty response.
+ *
+ * @return array
+ */
+ public function getRequest( $query, $request_headers = [] ) {
+ $response = $this->getRawRequest( $query, $request_headers );
+ if ( $response->getStatusCode() !== 200 ) {
+ throw new ModuleException( $this, 'Invalid response.' );
+ }
+
+ if ( empty( $response->getBody() ) ) {
+ throw new ModuleException( $this, 'Empty response.' );
+ }
+
+ $queryResults = json_decode( $response->getBody(), true );
+
+ return $queryResults;
+ }
+
+ /**
+ * Sends a POST request to the GraphQL endpoint and return a response
+ *
+ * @param string $query The GraphQL query to send.
+ * @param array $variables The variables to send with the query.
+ * @param string|null $request_headers The headers to send with the request.
+ *
+ * @throws \Codeception\Exception\ModuleException Invalid endpoint.
+ *
+ * @return array
+ */
+ public function postRawRequest( $query, $variables = [], $request_headers = [] ) {
+ $endpoint = $this->config['endpoint'];
+ if ( empty( $endpoint ) ) {
+ throw new ModuleException( $this, 'Invalid endpoint.' );
+ }
+
+ if ( empty( $query ) ) {
+ throw new ModuleException( $this, 'Invalid query.' );
+ }
+
+ if ( ! is_array( $variables ) ) {
+ throw new ModuleException( $this, 'Invalid variables.' );
+ }
+
+ $headers = array_merge(
+ $this->getHeaders(),
+ $request_headers
+ );
+
+ $this->logger->logData( "GET request to {$endpoint} with query: {$query}" );
+ $this->logger->logData( "Variables: " . json_encode( $variables ) );
+ $this->logger->logData( "Headers: " . json_encode( $headers ) );
+
+ $response = $this->client->request(
+ 'POST',
+ '',
+ [
+ 'headers' => $headers,
+ 'body' => json_encode( [ 'query' => $query, 'variables' => $variables ] ),
+ ]
+ );
+
+ if ( empty( $response ) ) {
+ throw new ModuleException( $this, 'Invalid response.' );
+ }
+
+ $this->logger->logData( $response->getHeaders() );
+ $this->logger->logData( $response->getBody() );
+
+ return $response;
+ }
+
+ /**
+ * Sends POST request to the GraphQL endpoint and returns the query results
+ *
+ * @param string $query The GraphQL query to send.
+ * @param array $variables The variables to send with the query.
+ * @param string|null $request_headers The headers to send with the request.
+ *
+ * @throws \Codeception\Exception\ModuleException Invalid endpoint.
+ *
+ * @return array
+ */
+ public function postRequest( $query, $variables = [], $request_headers = [] ) {
+ $response = $this->postRawRequest( $query, $variables, $request_headers );
+ if ( $response->getStatusCode() !== 200 ) {
+ throw new ModuleException( $this, 'Invalid response.' );
+ }
+
+ if ( empty( $response->getBody() ) ) {
+ throw new ModuleException( $this, 'Empty response.' );
+ }
+
+ $queryResults = json_decode( $response->getBody(), true );
+
+ return $queryResults;
+ }
+
+ /**
+ * Sends a batch query request to the GraphQL endpoint and return a response
+ *
+ * @param object{'query': string, 'variables': array} $operations An array of operations to send.
+ * @param array $request_headers An array of headers to send with the request.
+ *
+ * @throws \Codeception\Exception\ModuleException Invalid endpoint.
+ *
+ * @return array
+ */
+ public function batchRawRequest( $operations, $request_headers = [] ) {
+ $endpoint = $this->config['endpoint'];
+ if ( empty( $endpoint ) ) {
+ throw new ModuleException( $this, 'Invalid endpoint.' );
+ }
+
+ if ( empty( $operations ) ) {
+ throw new ModuleException( $this, 'Invalid query.' );
+ }
+
+ $headers = array_merge(
+ $this->getHeaders(),
+ $request_headers
+ );
+
+ $response = $this->client->request(
+ 'POST',
+ '',
+ [
+ 'headers' => $headers,
+ 'body' => json_encode( $operations ),
+ ]
+ );
+
+ if ( empty( $response ) ) {
+ throw new ModuleException( $this, 'Invalid response.' );
+ }
+
+ $this->logger->logData( $response->getHeaders() );
+ $this->logger->logData( json_decode( $response->getBody() ) );
+
+ return $response;
+ }
+
+ /**
+ * Sends a batch query request to the GraphQL endpoint and returns the query results
+ *
+ * @param object{'query': string, 'variables': array} $operations An array of operations to send.
+ * @param array $request_headers An array of headers to send with the request.
+ *
+ * @throws \Codeception\Exception\ModuleException Invalid endpoint.
+ *
+ * @return array
+ */
+ public function batchRequest( $operations, $request_headers = [] ) {
+ $response = $this->batchRawRequest( $operations, $request_headers );
+ if ( $response->getStatusCode() !== 200 ) {
+ throw new ModuleException( $this, 'Invalid response.' );
+ }
+
+ if ( empty( $response->getBody() ) ) {
+ throw new ModuleException( $this, 'Empty response.' );
+ }
+
+ $queryResults = json_decode( $response->getBody(), true );
+
+ return $queryResults;
+ }
+
+ /**
+ * Sends a concurrent requests to the GraphQL endpoint and returns a response
+ *
+ * @param {'query': string, 'variables': array} $operations An array of operations to send.
+ * @param array $request_headers An array of headers to send with the request.
+ * @param int $stagger The time in milliseconds to stagger requests.
+ *
+ * @throws \Codeception\Exception\ModuleException Invalid endpoint.
+ *
+ * @return array
+ */
+ public function concurrentRawRequests( $operations, $request_headers = [], $stagger = 800 ) {
+ $endpoint = $this->config['endpoint'];
+ if ( empty( $endpoint ) ) {
+ throw new ModuleException( $this, 'Invalid endpoint.' );
+ }
+
+ if ( empty( $operations ) ) {
+ throw new ModuleException( $this, 'Invalid query.' );
+ }
+
+ $headers = array_merge(
+ $this->getHeaders(),
+ $request_headers
+ );
+
+ $promises = [];
+ foreach ( $operations as $index => $operation ) {
+ $body = json_encode( $operation );
+ $delay = $stagger * ($index + 1);
+ $connected = false;
+ $progress = function ( $downloadTotal, $downloadedBytes, $uploadTotal, $uploadedBytes ) use ( $index, &$connected ) {
+ if ( $uploadTotal === $uploadedBytes && 0 === $downloadTotal && ! $connected ) {
+ $this->logger->logData(
+ "Session mutation request $index connected @ "
+ . date( 'Y-m-d H:i:s', time() )
+ );
+ $connected = true;
+ }
+ };
+ $promises[] = $this->client->postAsync(
+ '',
+ [
+ 'body' => $body,
+ 'delay' => $delay,
+ 'progress' => $progress,
+ 'headers' => $headers,
+ ]
+ );
+ }
+
+ $responses = \GuzzleHttp\Promise\Utils::unwrap( $promises );
+ \GuzzleHttp\Promise\Utils::settle( $promises )->wait();
+
+ return $responses;
+ }
+
+ /**
+ * Sends a concurrent requests to the GraphQL endpoint and returns a response
+ *
+ * @param {'query': string, 'variables': array} $operations An array of operations to send.
+ * @param array $request_headers An array of headers to send with the request.
+ * @param int $stagger The time in milliseconds to stagger requests.
+ *
+ * @throws \Codeception\Exception\ModuleException Invalid endpoint.
+ *
+ * @return array
+ */
+ public function concurrentRequests( $operations, $request_headers = [], $stagger = 800 ) {
+ $responses = $this->concurrentRawRequests( $operations, $request_headers, $stagger );
+ if ( empty( $responses ) ) {
+ throw new ModuleException( $this, 'Invalid response.' );
+ }
+
+ $queryResults = [];
+ foreach ( $responses as $response ) {
+ if ( $response->getStatusCode() !== 200 ) {
+ throw new ModuleException( $this, 'Invalid response.' );
+ }
+
+ if ( empty( $response->getBody() ) ) {
+ throw new ModuleException( $this, 'Empty response.' );
+ }
+
+ $this->logger->logData( $response->getHeaders() );
+ $this->logger->logData( json_decode( $response->getBody() ) );
+
+ $queryResults[] = json_decode( $response->getBody(), true );
+ }
+
+ return $queryResults;
+ }
+}
\ No newline at end of file
diff --git a/src/Constraint/QueryConstraint.php b/src/Constraint/QueryConstraint.php
index cc7a9d0..ae85556 100644
--- a/src/Constraint/QueryConstraint.php
+++ b/src/Constraint/QueryConstraint.php
@@ -38,12 +38,19 @@ class QueryConstraint extends Constraint {
*/
private $actual = null;
+ /**
+ * Error message for assertion failure.
+ *
+ * @var string
+ */
+ protected $error_message = null;
+
/**
- * List of errors trigger during validation.
+ * List of reasons for assertion failure.
*
* @var string[]
*/
- private $error_messages = [];
+ protected $error_details = [];
/**
* Constructor
@@ -65,17 +72,17 @@ public function __construct($logger, array $expected = []) {
*/
protected function responseIsValid( $response, &$message = null ) {
if ( empty( $response ) ) {
- $this->error_messages[] = 'GraphQL query response is invalid.';
+ $this->error_details[] = 'GraphQL query response is invalid.';
return false;
}
if ( array_keys( $response ) === range( 0, count( $response ) - 1 ) ) {
- $this->error_messages[] = 'The GraphQL query response must be provided as an associative array.';
+ $this->error_details[] = 'The GraphQL query response must be provided as an associative array.';
return false;
}
if ( 0 === count( array_intersect( array_keys( $response ), [ 'data', 'errors' ] ) ) ) {
- $this->error_messages[] = 'A valid GraphQL query response must contain a "data" or "errors" object.';
+ $this->error_details[] = 'A valid GraphQL query response must contain a "data" or "errors" object.';
return false;
}
@@ -94,7 +101,7 @@ protected function expectedDataFound( array $response, array $expected_data, str
// Throw if "$expected_data" invalid.
if ( empty( $expected_data['type'] ) ) {
$this->logger->logData( [ 'INVALID_DATA_OBJECT' => $expected_data ] );
- $this->error_messages[] = "Invalid rule object provided for evaluation: \n\t " . json_encode( $expected_data, JSON_PRETTY_PRINT );
+ $this->error_details[] = "Invalid rule object provided for evaluation: \n\t " . json_encode( $expected_data, JSON_PRETTY_PRINT );
return false;
}
@@ -132,7 +139,7 @@ protected function expectedDataFound( array $response, array $expected_data, str
case $this->logger::NOT_FALSY:
// Fail if data found at path is a falsy value (null, false, []).
if ( empty( $actual_data ) && ! $reverse ) {
- $this->error_messages[] = sprintf(
+ $this->error_details[] = sprintf(
'Expected data at path "%s" not to be falsy value. "%s" Given',
$full_path,
is_array( $actual_data ) ? '[]' : (string) $actual_data
@@ -140,7 +147,7 @@ protected function expectedDataFound( array $response, array $expected_data, str
return false;
} elseif ( ! empty( $actual_data ) && $reverse ) {
- $this->error_messages[] = sprintf(
+ $this->error_details[] = sprintf(
'Expected data at path "%s" to be falsy value. "%s" Given',
$full_path,
is_array( $actual_data ) ? "\n\n" . json_encode( $actual_data, JSON_PRETTY_PRINT ) : $actual_data
@@ -155,7 +162,7 @@ protected function expectedDataFound( array $response, array $expected_data, str
case $this->logger::IS_FALSY:
// Fail if data found at path is not falsy value (null, false, 0, []).
if ( ! empty( $actual_data ) && ! $reverse ) {
- $this->error_messages[] = sprintf(
+ $this->error_details[] = sprintf(
'Expected data at path "%s" to be falsy value. "%s" Given',
$full_path,
is_array( $actual_data ) ? "\n\n" .json_encode( $actual_data, JSON_PRETTY_PRINT ) : $actual_data
@@ -163,7 +170,7 @@ protected function expectedDataFound( array $response, array $expected_data, str
return false;
} elseif ( empty( $actual_data ) && $reverse ) {
- $this->error_messages[] = sprintf(
+ $this->error_details[] = sprintf(
'Expected data at path "%s" not to be falsy value. "%s" Given',
$full_path,
is_array( $actual_data ) ? "\n\n" .json_encode( $actual_data, JSON_PRETTY_PRINT ) : $actual_data
@@ -179,7 +186,7 @@ protected function expectedDataFound( array $response, array $expected_data, str
default: // Check if "$expected_value" is not null if comparing to provided value.
// Fail if no data found at path.
if ( is_null( $actual_data ) && ! $reverse ) {
- $this->error_messages[] = sprintf( 'No data found at path "%s"', $full_path );
+ $this->error_details[] = sprintf( 'No data found at path "%s"', $full_path );
return false;
} elseif (
@@ -187,7 +194,7 @@ protected function expectedDataFound( array $response, array $expected_data, str
&& $reverse
&& $expected_value === $this->logger::NOT_NULL
) {
- $this->error_messages[] = sprintf( 'Unexpected data found at path "%s"', $full_path );
+ $this->error_details[] = sprintf( 'Unexpected data found at path "%s"', $full_path );
return false;
}
@@ -214,7 +221,7 @@ protected function expectedDataFound( array $response, array $expected_data, str
case $is_field_rule:
// Fail if matcher fails
if ( ! $this->{$matcher}( $actual_data, $expected_value, $match_wanted, $path ) ) {
- $this->error_messages[] = sprintf(
+ $this->error_details[] = sprintf(
'Data found at path "%1$s" %2$s the provided value',
$path,
$match_wanted ? 'doesn\'t match' : 'shouldn\'t match'
@@ -236,6 +243,12 @@ protected function expectedDataFound( array $response, array $expected_data, str
$nested_rule_passing = $this->expectedDataFound( $response, $nested_rule, $next_path );
if ( ! $nested_rule_passing ) {
+ $this->error_details[] = sprintf(
+ "Data found at path \"%1\$s\" %2\$s fails the following rules: \n\t\t %3\$s",
+ $next_path,
+ $match_wanted ? 'doesn\'t match' : 'shouldn\'t match',
+ \json_encode( $nested_rule, JSON_PRETTY_PRINT )
+ );
return false;
}
}
@@ -245,13 +258,13 @@ protected function expectedDataFound( array $response, array $expected_data, str
// Fail if matcher fails.
if ( ! $this->{$matcher}( $actual_data, $expected_value, $match_wanted, $path ) ) {
if ( $check_order ) {
- $this->error_messages[] = sprintf(
+ $this->error_details[] = sprintf(
'Data found at path "%1$s" %2$s the provided value',
$full_path,
$match_wanted ? 'doesn\'t match' : 'shouldn\'t match'
);
} else {
- $this->error_messages[] = sprintf(
+ $this->error_details[] = sprintf(
'%1$s found in %2$s list at path "%3$s"',
$match_wanted ? 'Unexpected data ' : 'Expected data not ',
strtolower( $type ),
@@ -266,7 +279,7 @@ protected function expectedDataFound( array $response, array $expected_data, str
return true;
default:
$this->logger->logData( ['INVALID_DATA_OBJECT', $expected_data ] );
- $this->error_messages[] = "Invalid data object provided for evaluation. \n\t" . json_encode( $expected_data, JSON_PRETTY_PRINT );
+ $this->error_details[] = "Invalid data object provided for evaluation. \n\t" . json_encode( $expected_data, JSON_PRETTY_PRINT );
return false;
}
}
@@ -329,7 +342,7 @@ function( $v ) {
}
// Fail if no match found.
- $this->error_messages[] = sprintf( 'No errors found that occured at path "%1$s"', $path );
+ $this->error_details[] = sprintf( 'No errors found that occured at path "%1$s"', $path );
return false;
case 'ERROR_MESSAGE':
$this->logger->logData(
@@ -361,7 +374,7 @@ function( $v ) {
}
// Fail if no match found.
- $this->error_messages[] = sprintf(
+ $this->error_details[] = sprintf(
'No errors found with a message that %1$s "%2$s"',
$search_type_messages[ $search_type ],
$needle
@@ -370,7 +383,7 @@ function( $v ) {
return false;
default:
$this->logger->logData( ['INVALID_DATA_OBJECT', $expected_data ] );
- $this->error_messages[] = "Invalid data object provided for evaluation. \n\t" . json_encode( $expected_data, JSON_PRETTY_PRINT );
+ $this->error_details[] = "Invalid data object provided for evaluation. \n\t" . json_encode( $expected_data, JSON_PRETTY_PRINT );
return false;
}
}
@@ -567,6 +580,7 @@ protected function findSubstring( $haystack, $needle, $search_type ) {
public function matches($response): bool {
// Ensure response is valid.
if ( ! $this->responseIsValid( $response ) ) {
+ $this->error_message = 'GraphQL response is invalid';
return false;
}
@@ -574,7 +588,19 @@ public function matches($response): bool {
}
public function failureDescription($other): string {
- return "GraphQL response failed validation: \n\n\t• " . implode( "\n\n\t• ", $this->error_messages );
+ $output = '';
+
+ if ( ! empty( $this->error_message ) ) {
+ $output .= $this->error_message;
+ } else {
+ $output .= 'GraphQL response failed validation';
+ }
+
+ if ( ! empty( $this->error_details ) ) {
+ $output .= ": \n\n\t• " . implode( "\n\n\t• ", $this->error_details );
+ }
+
+ return $output;
}
/**
diff --git a/src/Constraint/QueryErrorConstraint.php b/src/Constraint/QueryErrorConstraint.php
index b303b56..f69f3e2 100644
--- a/src/Constraint/QueryErrorConstraint.php
+++ b/src/Constraint/QueryErrorConstraint.php
@@ -22,7 +22,7 @@ public function matches($response): bool {
// Throw if response has errors.
if ( ! array_key_exists( 'errors', $response ) ) {
- $this->error_messages[] = 'No errors was thrown during the previous GraphQL requested. May need to use "--debug" flag to see contents of previous request.';
+ $this->error_message = "No errors was thrown during the previous GraphQL requested. \n Use \"--debug\" flag to see contents of previous request.";
return false;
}
@@ -37,7 +37,9 @@ public function matches($response): bool {
foreach( $this->validationRules as $expected_data ) {
if ( empty( $expected_data['type'] ) ) {
$this->logger->logData( array( 'INVALID_DATA_OBJECT' => $expected_data ) );
- $this->error_messages[] = 'Invalid data object provided for evaluation.';
+ $this->error_details[] = 'Invalid data object provided for evaluation.';
+ $data_passed = false;
+ $error_passed = false;
continue;
}
@@ -54,6 +56,7 @@ public function matches($response): bool {
}
if ( ! $data_passed || ! $error_passed) {
+ $this->error_message = 'The GraphQL response failed the following steps in validation';
return false;
}
diff --git a/src/Constraint/QuerySuccessfulConstraint.php b/src/Constraint/QuerySuccessfulConstraint.php
index 98713d1..1510572 100644
--- a/src/Constraint/QuerySuccessfulConstraint.php
+++ b/src/Constraint/QuerySuccessfulConstraint.php
@@ -24,7 +24,7 @@ public function matches($response): bool {
// Throw if response has errors.
if ( array_key_exists( 'errors', $response ) ) {
- $this->error_messages[] = 'An error was thrown during the previous GraphQL requested. May need to use "--debug" flag to see contents of previous request.';
+ $this->error_message = "An error was thrown during the previous GraphQL requested. \n Use \"--debug\" flag to see contents of previous request.";
return false;
}
@@ -32,6 +32,7 @@ public function matches($response): bool {
if ( empty( $this->validationRules ) ) {
return true;
}
+
// Check validation rules.
$passed = true;
@@ -42,6 +43,7 @@ public function matches($response): bool {
}
if ( ! $passed ) {
+ $this->error_message = 'The GraphQL response failed the following steps in validation';
return false;
}
diff --git a/tests/codeception/acceptance.suite.dist.yml b/tests/codeception/acceptance.suite.dist.yml
index ceeaebd..8f4f72f 100644
--- a/tests/codeception/acceptance.suite.dist.yml
+++ b/tests/codeception/acceptance.suite.dist.yml
@@ -14,22 +14,19 @@ modules:
config:
WPDb:
dsn: '%TEST_SITE_DB_DSN%'
- user: '%TEST_SITE_DB_USER%'
- password: '%TEST_SITE_DB_PASSWORD%'
- dump: 'tests/_data/dump.sql'
- #import the dump before the tests; this means the test site database will be repopulated before the tests.
- populate: true
- # re-import the dump between tests; this means the test site database will be repopulated between the tests.
- cleanup: true
+ user: '%TEST_DB_USER%'
+ password: '%TEST_DB_PASSWORD%'
+ dump: 'tests/codeception/_data/dump.sql'
+ populate: false
+ cleanup: false
waitlock: 10
url: '%TEST_SITE_WP_URL%'
- urlReplacement: true #replace the hardcoded dump URL with the one above
- tablePrefix: '%TEST_SITE_TABLE_PREFIX%'
+ urlReplacement: true
+ tablePrefix: '%TEST_TABLE_PREFIX%'
+
WPBrowser:
url: '%TEST_SITE_WP_URL%'
+ wpRootFolder: '%WP_ROOT_FOLDER%'
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
\ No newline at end of file
+ adminPath: '/wp-admin'
\ No newline at end of file
diff --git a/tests/codeception/functional.suite.dist.yml b/tests/codeception/functional.suite.dist.yml
index 140252e..077a1a2 100644
--- a/tests/codeception/functional.suite.dist.yml
+++ b/tests/codeception/functional.suite.dist.yml
@@ -8,33 +8,51 @@ modules:
enabled:
- WPDb
- WPBrowser
- # - WPFilesystem
+ - WPFilesystem
- Asserts
+ - \Tests\WPGraphQL\Codeception\Module\QueryAsserts
+ - \Tests\WPGraphQL\Codeception\Module\WPGraphQL
- \Helper\Functional
config:
+ \Tests\WPGraphQL\Codeception\Module\WPGraphQL:
+ endpoint: '%TEST_SITE_WP_URL%/graphql'
+ auth_header: 'Basic %TEST_SITE_ADMIN_APP_PASSWORD%'
WPDb:
dsn: '%TEST_SITE_DB_DSN%'
- user: '%TEST_SITE_DB_USER%'
- password: '%TEST_SITE_DB_PASSWORD%'
- dump: 'tests/_data/dump.sql'
+ user: '%TEST_DB_USER%'
+ password: '%TEST_DB_PASSWORD%'
+ dump: 'tests/codeception/_data/dump.sql'
populate: true
cleanup: true
waitlock: 10
url: '%TEST_SITE_WP_URL%'
urlReplacement: true
- tablePrefix: '%TEST_SITE_TABLE_PREFIX%'
+ tablePrefix: '%TEST_TABLE_PREFIX%'
+
WPBrowser:
url: '%TEST_SITE_WP_URL%'
+ wpRootFolder: '%WP_ROOT_FOLDER%'
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
+ adminPath: '/wp-admin'
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
+ uploads: '/wp-content/uploads'
+
+ WPLoader:
+ loadOnly: true
+ 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: "WPGraphQLTestcase"
+ plugins: ['wp-graphql/wp-graphql.php']
+ activatePlugins: ['wp-graphql/wp-graphql.php']
diff --git a/tests/codeception/functional/QueryAssertsModuleTestCest.php b/tests/codeception/functional/QueryAssertsModuleTestCest.php
new file mode 100644
index 0000000..cad82f5
--- /dev/null
+++ b/tests/codeception/functional/QueryAssertsModuleTestCest.php
@@ -0,0 +1,76 @@
+ [
+ 'post' => [
+ 'id' => 'cG9zdDox'
+ ]
+ ],
+ ];
+
+ $I->assertResponseIsValid( $data );
+
+ $data = [
+ 'errors' => [
+ [ 'message' => 'Invalid ID' ]
+ ],
+ 'data' => null,
+ ];
+
+ $I->assertResponseIsValid( $data, false );
+ }
+
+ public function testAssertQuerySuccessful( FunctionalTester $I ) {
+ $data = [
+ 'data' => [
+ 'post' => [
+ 'id' => 'cG9zdDox'
+ ]
+ ],
+ ];
+
+ $I->assertQuerySuccessful( $data );
+
+ $expected = [
+ $I->expectNode(
+ 'post',
+ [
+ $I->expectField( 'id', $I->asRelayId( 'post', 1 ) )
+ ]
+ )
+ ];
+
+ $I->assertQuerySuccessful( $data, $expected );
+ }
+
+ public function testAssertQueryError( FunctionalTester $I ) {
+ $data = [
+ 'errors' => [
+ 'message' => "Internal server error",
+ 'extensions' => [
+ 'category' => 'internal',
+ ],
+ 'locations' => [
+ [
+ 'line' => 2,
+ 'column' => 3,
+ ],
+ ],
+ 'path' => [
+ 'post',
+ ],
+ ],
+ 'data' => [ 'post' => null ],
+ ];
+
+ $I->assertQueryError( $data );
+ }
+}
diff --git a/tests/codeception/functional/WPGraphQLModuleTestCest.php b/tests/codeception/functional/WPGraphQLModuleTestCest.php
new file mode 100644
index 0000000..8cca2a4
--- /dev/null
+++ b/tests/codeception/functional/WPGraphQLModuleTestCest.php
@@ -0,0 +1,293 @@
+wantTo( 'send a GET request to the GraphQL endpoint and return a response' );
+
+ $I->haveManyPostsInDatabase(5);
+ $post_id = $I->havePostInDatabase( [ 'post_title' => 'Test Post' ] );
+
+ $query = '{
+ posts {
+ nodes {
+ id
+ title
+ }
+ }
+ }';
+
+ $response = $I->getRequest( $query );
+ $expected = [
+ $I->expectNode(
+ 'posts.nodes',
+ [
+ $I->expectField( 'id', $I->asRelayId( 'post', $post_id ) ),
+ $I->expectField( 'title', 'Test Post' )
+ ]
+ )
+ ];
+
+ $I->assertQuerySuccessful( $response, $expected );
+ }
+
+ public function testPostRequest( FunctionalTester $I, $scenario ) {
+ $I->wantTo( 'send a POST request to the GraphQL endpoint and return a response' );
+
+ $query = 'mutation ( $input: CreatePostInput! ) {
+ createPost( input: $input ) {
+ post {
+ id
+ title
+ }
+ }
+ }';
+
+ $variables = [
+ 'input' => [
+ 'title' => 'Test Post',
+ 'content' => 'Test Post content',
+ 'slug' => 'test-post',
+ 'status' => 'PUBLISH'
+ ]
+ ];
+
+ $response = $I->postRequest( $query, $variables );
+ $expected = [
+ $I->expectObject(
+ 'createPost.post',
+ [
+ $I->expectField( 'id', Signal::NOT_NULL ),
+ $I->expectField( 'title', 'Test Post' )
+ ]
+ )
+ ];
+
+ $I->assertQuerySuccessful( $response, $expected );
+ }
+
+ public function testBatchRequest( FunctionalTester $I, $scenario ) {
+ $I->wantTo( 'send a batch request to the GraphQL endpoint and return a response' );
+
+ $I->haveManyPostsInDatabase(20);
+
+ $operations = [
+ [
+ 'query' => 'mutation ( $input: CreatePostInput! ) {
+ createPost( input: $input ) {
+ post {
+ id
+ title
+ slug
+ status
+ }
+ }
+ }',
+ 'variables' => [
+ 'input' => [
+ 'title' => 'Wowwy Zowwy 1',
+ 'content' => 'Wowwy Zowwy 1 content',
+ 'slug' => 'wowwy-zowwy-1',
+ 'status' => 'PUBLISH',
+ ]
+ ]
+ ],
+ [
+ 'query' => 'mutation ( $input: CreatePostInput! ) {
+ createPost( input: $input ) {
+ post {
+ id
+ title
+ }
+ }
+ }',
+ 'variables' => [
+ 'input' => [
+ 'title' => 'Wowwy Zowwy 2',
+ 'content' => 'Wowwy Zowwy 2 content',
+ 'slug' => 'wowwy-zowwy-2',
+ 'status' => 'PUBLISH',
+ ]
+ ]
+ ],
+ [
+ 'query' => '{
+ posts(first: 2 where: { search: "Wowwy Zowwy" } ) {
+ nodes {
+ id
+ title
+ }
+ }
+ }'
+ ]
+ ];
+
+ $responses = $I->batchRequest( $operations );
+
+ $I->assertQuerySuccessful(
+ $responses[0],
+ [
+ $I->expectObject(
+ 'createPost.post',
+ [
+ $I->expectField( 'id', Signal::NOT_NULL ),
+ $I->expectField( 'title', 'Wowwy Zowwy 1' )
+ ]
+ )
+ ]
+ );
+
+ $I->assertQuerySuccessful(
+ $responses[1],
+ [
+ $I->expectObject(
+ 'createPost.post',
+ [
+ $I->expectField( 'id', Signal::NOT_NULL ),
+ $I->expectField( 'title', 'Wowwy Zowwy 2' )
+ ]
+ )
+ ]
+ );
+
+ $I->assertQuerySuccessful(
+ $responses[2],
+ [
+ $I->expectNode(
+ 'posts.nodes',
+ [
+ $I->expectField( 'id', Signal::NOT_NULL ),
+ $I->expectField( 'title', 'Wowwy Zowwy 1' )
+ ],
+ ),
+ $I->expectNode(
+ 'posts.nodes',
+ [
+ $I->expectField( 'id', Signal::NOT_NULL ),
+ $I->expectField( 'title', 'Wowwy Zowwy 2' )
+ ]
+ )
+ ]
+ );
+ }
+
+ public function testConcurrentRequests( FunctionalTester $I, $scenario ) {
+ $I->wantTo( 'send concurrent requests to the GraphQL endpoint and return a response' );
+
+ $I->haveManyPostsInDatabase(20);
+
+ $operations = [
+ [
+ 'query' => 'mutation ( $input: CreatePostInput! ) {
+ createPost( input: $input ) {
+ post {
+ id
+ title
+ slug
+ status
+ }
+ }
+ }',
+ 'variables' => [
+ 'input' => [
+ 'title' => 'Scream 1',
+ 'content' => 'Scream 1 content',
+ 'slug' => 'scream-1',
+ 'status' => 'PUBLISH',
+ ]
+ ]
+ ],
+ [
+ 'query' => '{
+ posts(where: { search: "Scream" }) {
+ nodes {
+ id
+ title
+ }
+ }
+ }'
+ ],
+ [
+ 'query' => 'mutation ( $input: CreatePostInput! ) {
+ createPost( input: $input ) {
+ post {
+ id
+ title
+ }
+ }
+ }',
+ 'variables' => [
+ 'input' => [
+ 'title' => 'Scream 2',
+ 'content' => 'Scream 2 content',
+ 'slug' => 'scream-2',
+ 'status' => 'PUBLISH',
+ ]
+ ]
+ ],
+ [
+ 'query' => '{
+ posts(where: { search: "Scream" }) {
+ nodes {
+ id
+ title
+ }
+ }
+ }'
+ ],
+ ];
+
+ $responses = $I->concurrentRequests( $operations, [], 0 );
+
+ $I->assertQuerySuccessful(
+ $responses[0],
+ [
+ $I->expectObject(
+ 'createPost.post',
+ [
+ $I->expectField( 'id', Signal::NOT_NULL ),
+ $I->expectField( 'title', 'Scream 1' )
+ ]
+ )
+ ]
+ );
+
+ $I->assertQuerySuccessful(
+ $responses[1],
+ [
+ $I->expectField(
+ 'posts.nodes',
+ Signal::IS_FALSY
+ )
+ ]
+ );
+
+ $I->assertQuerySuccessful(
+ $responses[2],
+ [
+ $I->expectObject(
+ 'createPost.post',
+ [
+ $I->expectField( 'id', Signal::NOT_NULL ),
+ $I->expectField( 'title', 'Scream 2' )
+ ]
+ )
+ ]
+ );
+
+ $I->assertQuerySuccessful(
+ $responses[3],
+ [
+ $I->expectField(
+ 'posts.nodes',
+ Signal::IS_FALSY
+ )
+ ]
+ );
+ }
+}
diff --git a/tests/codeception/wpunit/QueryConstraintTest.php b/tests/codeception/wpunit/QueryConstraintTest.php
index d5b6104..aff3024 100644
--- a/tests/codeception/wpunit/QueryConstraintTest.php
+++ b/tests/codeception/wpunit/QueryConstraintTest.php
@@ -84,7 +84,7 @@ public function testFailureDescription() {
$constraint = new QueryConstraint($this->logger);
$response = [4, 5, 6];
$this->assertFalse($constraint->matches($response));
- $this->assertEquals("GraphQL response failed validation: \n\n\t• The GraphQL query response must be provided as an associative array.", $constraint->failureDescription($response));
+ $this->assertEquals("GraphQL response is invalid: \n\n\t• The GraphQL query response must be provided as an associative array.", $constraint->failureDescription($response));
}
public function testToString() {
diff --git a/tests/phpunit/bootstrap.php b/tests/phpunit/bootstrap.php
index 9c236f3..13e6715 100644
--- a/tests/phpunit/bootstrap.php
+++ b/tests/phpunit/bootstrap.php
@@ -39,5 +39,22 @@ function wpgraphql_testcase_filter_active_plugins_for_phpunit( $active_plugins )
tests_add_filter( 'site_option_active_sitewide_plugins', 'wpgraphql_testcase_filter_active_plugins_for_phpunit' );
tests_add_filter( 'option_active_plugins', 'wpgraphql_testcase_filter_active_plugins_for_phpunit' );
+
+function _manually_load_wpgraphql_deps() {
+ $autoload_file = WP_PLUGIN_DIR . '/wp-graphql/vendor/autoload.php';
+ if ( file_exists( $autoload_file ) ) {
+ require_once $autoload_file;
+ }
+
+ if ( ! defined( 'WPGRAPHQL_AUTOLOAD' ) ) {
+ define( 'WPGRAPHQL_AUTOLOAD', false );
+ }
+}
+tests_add_filter( 'muplugins_loaded', '_manually_load_wpgraphql_deps' );
+
+//if ( function_exists( 'graphql_can_load_plugin' ) ) {
+
+//}
+
// @see https://core.trac.wordpress.org/browser/trunk/tests/phpunit/includes/bootstrap.php
require $_tests_dir . '/includes/bootstrap.php';
\ No newline at end of file
diff --git a/tests/phpunit/unit/test-queryconstraint.php b/tests/phpunit/unit/test-queryconstraint.php
index 151ed3f..27888bf 100644
--- a/tests/phpunit/unit/test-queryconstraint.php
+++ b/tests/phpunit/unit/test-queryconstraint.php
@@ -84,7 +84,7 @@ public function test_FailureDescription() {
$constraint = new QueryConstraint($this->logger);
$response = [4, 5, 6];
$this->assertFalse($constraint->matches($response));
- $this->assertEquals("GraphQL response failed validation: \n\n\t• The GraphQL query response must be provided as an associative array.", $constraint->failureDescription($response));
+ $this->assertEquals("GraphQL response is invalid: \n\n\t• The GraphQL query response must be provided as an associative array.", $constraint->failureDescription($response));
}
public function test_ToString() {