From e4b9d464bff56f8a1530a784327577edaeafe720 Mon Sep 17 00:00:00 2001 From: Geoff Taylor Date: Mon, 13 May 2024 21:14:54 -0400 Subject: [PATCH 01/14] feat: WPGraphQL Codeception Module for E2E test implemented and tested --- .env.testing | 5 +- .github/workflows/continous-integration.yml | 7 +- .gitignore | 6 +- codeception.dist.yml | 1 + composer.json | 23 +- docker-compose.yml | 4 +- docker/Dockerfile | 31 +- docker/entrypoint.sh | 54 +++ local/config/.htaccess | 15 + local/config/enable-app-passwords.php | 7 + local/config/wp-config.php | 8 +- phpunit.xml | 1 + src/Codeception/Module/QueryAsserts.php | 190 +++++++++ src/Codeception/Module/WPGraphQL.php | 367 ++++++++++++++++++ tests/codeception/acceptance.suite.dist.yml | 23 +- tests/codeception/functional.suite.dist.yml | 24 +- .../functional/WPGraphQLModuleTestCest.php | 297 ++++++++++++++ 17 files changed, 1021 insertions(+), 42 deletions(-) create mode 100644 docker/entrypoint.sh create mode 100644 local/config/.htaccess create mode 100644 local/config/enable-app-passwords.php create mode 100644 src/Codeception/Module/QueryAsserts.php create mode 100644 src/Codeception/Module/WPGraphQL.php create mode 100644 tests/codeception/functional/WPGraphQLModuleTestCest.php diff --git a/.env.testing b/.env.testing index 3d2af61..8e4040f 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_PORT=3306 TEST_SITE_DB_NAME=wordpress TEST_SITE_DB_USER=wordpress TEST_SITE_DB_PASSWORD=password @@ -13,6 +14,6 @@ TEST_DB_HOST=mysql 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..a39842a 100644 --- a/.github/workflows/continous-integration.yml +++ b/.github/workflows/continous-integration.yml @@ -50,7 +50,12 @@ 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. 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..11c6c9c 100644 --- a/codeception.dist.yml +++ b/codeception.dist.yml @@ -26,3 +26,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..9a7b7cf 100644 --- a/composer.json +++ b/composer.json @@ -17,23 +17,30 @@ "src/" ] }, + "repositories": [ + { + "type": "composer", + "url": "https://wpackagist.org" + } + ], "require": { - "php-extended/polyfill-php80-str-utils": "^1.3" + "php-extended/polyfill-php80-str-utils": "^1.3", + "ivome/graphql-relay-php": "^0.7.0" }, "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 --", + "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 --", "phpunit": "phpunit --", - "run-codeception": "env TEST_DB=mysql:3306 composer cli vendor/bin/codecept run wpunit", + "run-codeception": "env TEST_DB=mysql:3306 composer cli vendor/bin/codecept run", "run-phpunit": "env TEST_DB=mysql_phpunit:3306 composer cli vendor/bin/phpunit" }, "extra": { @@ -45,11 +52,11 @@ "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": { "allow-plugins": { diff --git a/docker-compose.yml b/docker-compose.yml index 2cb0395..d2f3413 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -26,8 +26,10 @@ services: - .:/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: - 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 diff --git a/docker/Dockerfile b/docker/Dockerfile index e95ee7e..b27ff5e 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -23,5 +23,32 @@ 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 +WORKDIR /var/www/html + +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" + +# 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..b8b680b --- /dev/null +++ b/docker/entrypoint.sh @@ -0,0 +1,54 @@ +#!/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}:${DB_PORT:-3306}" -- echo "Application database is operationally..." + +# 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 + +echo "Setting pretty permalinks..." +wp rewrite structure '/%year%/%monthnum%/%postname%/' --allow-root + +app_user="admin" +app_password=$(wp user application-password create 1 testing --porcelain --allow-root) + +echo TEST_SITE_ADMIN_APP_PASSWORD="$(echo -n "${app_user}:${app_password}" | base64)" >> $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 + +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/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..ea38835 --- /dev/null +++ b/src/Codeception/Module/QueryAsserts.php @@ -0,0 +1,190 @@ +logger = new Signal(); + } + + /** + * Wrapper for the "GraphQLRelay\Relay::toGlobalId()" function. + * + * @return string + */ + public function asRelayId() { + return \GraphQLRelay\Relay::toGlobalId( ...func_get_args() ); + } + + /** + * 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/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..b8aaa37 100644 --- a/tests/codeception/functional.suite.dist.yml +++ b/tests/codeception/functional.suite.dist.yml @@ -8,33 +8,37 @@ 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' diff --git a/tests/codeception/functional/WPGraphQLModuleTestCest.php b/tests/codeception/functional/WPGraphQLModuleTestCest.php new file mode 100644 index 0000000..8a1c95f --- /dev/null +++ b/tests/codeception/functional/WPGraphQLModuleTestCest.php @@ -0,0 +1,297 @@ +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 + ) + ] + ); + } + + public function testErrorThrownOnInvalidEndpoint( FunctionalTester $I, $scenario ) { + + } +} From fca45576037469f31a5d70fac587fa608a4efa06 Mon Sep 17 00:00:00 2001 From: Geoff Taylor Date: Tue, 14 May 2024 13:24:47 -0400 Subject: [PATCH 02/14] devops: Docker configurations modulated & PHPUnit bootstrap updated --- composer.json | 7 ++-- docker-compose.yml | 41 ++++++++++++++----- docker/{Dockerfile => codeception.Dockerfile} | 4 +- docker/entrypoint.sh | 2 + docker/wp-phpunit.Dockerfile | 27 ++++++++++++ local/config/wp-tests-config.php | 2 +- phpunit.xml | 1 + tests/phpunit/bootstrap.php | 17 ++++++++ 8 files changed, 85 insertions(+), 16 deletions(-) rename docker/{Dockerfile => codeception.Dockerfile} (98%) create mode 100644 docker/wp-phpunit.Dockerfile diff --git a/composer.json b/composer.json index 9a7b7cf..e44948c 100644 --- a/composer.json +++ b/composer.json @@ -37,11 +37,12 @@ "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 --", + "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", - "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", diff --git a/docker-compose.yml b/docker-compose.yml index d2f3413..27809c1 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,7 +2,7 @@ version: '3' services: - mysql: + codecept_db: image: mariadb:10.2 environment: MYSQL_ROOT_PASSWORD: password @@ -10,34 +10,55 @@ services: MYSQL_USER: wordpress MYSQL_PASSWORD: password - 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: + 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 - 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: "" + + 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 + diff --git a/docker/Dockerfile b/docker/codeception.Dockerfile similarity index 98% rename from docker/Dockerfile rename to docker/codeception.Dockerfile index b27ff5e..a765746 100644 --- a/docker/Dockerfile +++ b/docker/codeception.Dockerfile @@ -23,8 +23,6 @@ ENV XDEBUG_MODE=coverage RUN docker-php-ext-install \ pdo_mysql -WORKDIR /var/www/html - ENV WP_ROOT_FOLDER="/var/www/html" ENV WORDPRESS_DB_HOST=${TEST_SITE_DB_HOST} ENV WORDPRESS_DB_PORT=${TEST_SITE_DB_PORT} @@ -34,6 +32,8 @@ 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 diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh index b8b680b..a378731 100644 --- a/docker/entrypoint.sh +++ b/docker/entrypoint.sh @@ -34,6 +34,8 @@ fi echo "Setting pretty permalinks..." wp rewrite structure '/%year%/%monthnum%/%postname%/' --allow-root +wp user application-password delete 1 --all --allow-root + app_user="admin" app_password=$(wp user application-password create 1 testing --porcelain --allow-root) diff --git a/docker/wp-phpunit.Dockerfile b/docker/wp-phpunit.Dockerfile new file mode 100644 index 0000000..66a1613 --- /dev/null +++ b/docker/wp-phpunit.Dockerfile @@ -0,0 +1,27 @@ +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 + +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/wp-tests-config.php b/local/config/wp-tests-config.php index 4c21a3e..7d43f8e 100644 --- a/local/config/wp-tests-config.php +++ b/local/config/wp-tests-config.php @@ -11,7 +11,7 @@ define( 'DB_NAME', 'wordpress' ); define( 'DB_USER', 'root' ); define( 'DB_PASSWORD', '' ); -define( 'DB_HOST', 'mysql_phpunit' ); +define( 'DB_HOST', 'phpunit_db' ); define( 'DB_CHARSET', 'utf8' ); define( 'DB_COLLATE', '' ); diff --git a/phpunit.xml b/phpunit.xml index 10f056e..6cfee88 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -2,6 +2,7 @@ bootstrap="tests/phpunit/bootstrap.php" backupGlobals="false" colors="true" + verbose="true" convertErrorsToExceptions="true" convertNoticesToExceptions="true" convertWarningsToExceptions="true" 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 From 8a81003cfe92ff2e852f4a3f62a0cad3338f3abe Mon Sep 17 00:00:00 2001 From: Geoff Taylor Date: Tue, 14 May 2024 13:31:00 -0400 Subject: [PATCH 03/14] devops: .env.testing fixed --- .env.testing | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.env.testing b/.env.testing index 8e4040f..61c7414 100644 --- a/.env.testing +++ b/.env.testing @@ -1,5 +1,5 @@ -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 From d9e6d96fd154523d9c65280a33c4b94c7ab8cffc Mon Sep 17 00:00:00 2001 From: Geoff Taylor Date: Tue, 14 May 2024 13:32:10 -0400 Subject: [PATCH 04/14] devops: .env.testing fixed --- .env.testing | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.env.testing b/.env.testing index 61c7414..dc32f71 100644 --- a/.env.testing +++ b/.env.testing @@ -10,7 +10,7 @@ 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_ From d8d4be315522b48b57d8d9dc3943bec707416f23 Mon Sep 17 00:00:00 2001 From: Geoff Taylor Date: Tue, 14 May 2024 13:37:27 -0400 Subject: [PATCH 05/14] devops: entrypoint.sh fixed --- docker/entrypoint.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh index a378731..31c079a 100644 --- a/docker/entrypoint.sh +++ b/docker/entrypoint.sh @@ -12,7 +12,7 @@ cd "${WP_ROOT_FOLDER}" || exit set +u # Ensure mysql is loaded -wait-for-it -s -t 300 "${TEST_SITE_DB_HOST}:${DB_PORT:-3306}" -- echo "Application database is operationally..." +wait-for-it -s -t 300 "${WORDPRESS_DB_HOST}:${WORDPRESS_DB_PORT:-3306}" -- echo "Application database is operationally..." # Install WP if not yet installed echo "Installing WordPress..." @@ -43,8 +43,8 @@ echo TEST_SITE_ADMIN_APP_PASSWORD="$(echo -n "${app_user}:${app_password}" | bas echo "Dumping app database..." wp db export "${PROJECT_DIR}/tests/codeception/_data/dump.sql" \ - --dbuser="${TEST_SITE_DB_USER}" \ - --dbpass="${TEST_SITE_DB_PASSWORD}" \ + --dbuser="${WORDPRESS_DB_USER}" \ + --dbpass="${WORDPRESS_DB_PASSWORD}" \ --skip-plugins \ --skip-themes \ --allow-root From b768d1fa90b09b3e8098a10667dd55de07603de8 Mon Sep 17 00:00:00 2001 From: Geoff Taylor Date: Tue, 14 May 2024 16:30:52 -0400 Subject: [PATCH 06/14] feat: QueryAssertsModuleTestCest added --- composer.json | 6 +- docker-compose.yml | 21 ++++- docker/entrypoint.sh | 39 ++++++++-- local/config/wp-config.php | 30 -------- src/Codeception/Module/QueryAsserts.php | 9 ++- tests/codeception/functional.suite.dist.yml | 14 ++++ .../functional/QueryAssertsModuleTestCest.php | 76 +++++++++++++++++++ .../functional/WPGraphQLModuleTestCest.php | 4 - 8 files changed, 152 insertions(+), 47 deletions(-) delete mode 100644 local/config/wp-config.php create mode 100644 tests/codeception/functional/QueryAssertsModuleTestCest.php diff --git a/composer.json b/composer.json index e44948c..1d62007 100644 --- a/composer.json +++ b/composer.json @@ -24,8 +24,7 @@ } ], "require": { - "php-extended/polyfill-php80-str-utils": "^1.3", - "ivome/graphql-relay-php": "^0.7.0" + "php-extended/polyfill-php80-str-utils": "^1.3" }, "require-dev": { "composer/installers": "^1.9", @@ -60,6 +59,9 @@ "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 27809c1..8ad413e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -9,6 +9,10 @@ services: MYSQL_DATABASE: wordpress MYSQL_USER: wordpress MYSQL_PASSWORD: password + networks: + codecept: + aliases: + - codecept_db codeception_testing: image: wp-graphql/codeception-testing:${WP_VERSION:-latest} @@ -24,13 +28,17 @@ services: 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/.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. + networks: + codecept: + aliases: + - codeception_testing phpunit_db: image: mariadb:10.2 @@ -39,6 +47,10 @@ services: MYSQL_ALLOW_EMPTY_PASSWORD: "yes" MYSQL_DATABASE: "wordpress" MYSQL_ROOT_PASSWORD: "" + networks: + phpunit: + aliases: + - phpunit_db wp_phpunit_testing: image: wp-graphql/wp-phpunit-testing:${WP_VERSION:-latest} @@ -61,4 +73,11 @@ services: 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/entrypoint.sh b/docker/entrypoint.sh index 31c079a..38440a7 100644 --- a/docker/entrypoint.sh +++ b/docker/entrypoint.sh @@ -12,7 +12,24 @@ cd "${WP_ROOT_FOLDER}" || exit set +u # Ensure mysql is loaded -wait-for-it -s -t 300 "${WORDPRESS_DB_HOST}:${WORDPRESS_DB_PORT:-3306}" -- echo "Application database is operationally..." +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..." @@ -31,24 +48,32 @@ if [ -f "${PROJECT_DIR}/tests/codeception/_data/dump.sql" ]; then rm -rf "${PROJECT_DIR}/tests/codeception/_data/dump.sql" fi -echo "Setting pretty permalinks..." -wp rewrite structure '/%year%/%monthnum%/%postname%/' --allow-root - 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 TEST_SITE_ADMIN_APP_PASSWORD="$(echo -n "${app_user}:${app_password}" | base64)" >> $PROJECT_DIR/.env.docker +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="${WORDPRESS_DB_USER}" \ - --dbpass="${WORDPRESS_DB_PASSWORD}" \ + --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 diff --git a/local/config/wp-config.php b/local/config/wp-config.php deleted file mode 100644 index d1c6f16..0000000 --- a/local/config/wp-config.php +++ /dev/null @@ -1,30 +0,0 @@ - [ + '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 index 8a1c95f..8cca2a4 100644 --- a/tests/codeception/functional/WPGraphQLModuleTestCest.php +++ b/tests/codeception/functional/WPGraphQLModuleTestCest.php @@ -290,8 +290,4 @@ public function testConcurrentRequests( FunctionalTester $I, $scenario ) { ] ); } - - public function testErrorThrownOnInvalidEndpoint( FunctionalTester $I, $scenario ) { - - } } From 5286c179ccf6b898fe30937038d726915f67b716 Mon Sep 17 00:00:00 2001 From: Geoff Taylor Date: Tue, 14 May 2024 16:34:10 -0400 Subject: [PATCH 07/14] feat: QueryAssertsModuleTestCest added --- tests/codeception/acceptance/.gitkeep | 0 tests/codeception/unit/.gitkeep | 0 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 tests/codeception/acceptance/.gitkeep create mode 100644 tests/codeception/unit/.gitkeep diff --git a/tests/codeception/acceptance/.gitkeep b/tests/codeception/acceptance/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/tests/codeception/unit/.gitkeep b/tests/codeception/unit/.gitkeep new file mode 100644 index 0000000..e69de29 From f1b375fd7a146cec184f2e96c8e3fb0b756f0595 Mon Sep 17 00:00:00 2001 From: Geoff Taylor Date: Tue, 14 May 2024 16:43:30 -0400 Subject: [PATCH 08/14] devops: CI parameters updated --- .github/workflows/continous-integration.yml | 4 +++- codeception.dist.yml | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/continous-integration.yml b/.github/workflows/continous-integration.yml index a39842a..a45fb8a 100644 --- a/.github/workflows/continous-integration.yml +++ b/.github/workflows/continous-integration.yml @@ -61,7 +61,9 @@ jobs: - 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 diff --git a/codeception.dist.yml b/codeception.dist.yml index 11c6c9c..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 From 8495806d0cedf1965023eb20fbe69c01ff90255a Mon Sep 17 00:00:00 2001 From: Geoff Taylor Date: Tue, 14 May 2024 16:47:18 -0400 Subject: [PATCH 09/14] devops: CI script fixed --- .github/workflows/continous-integration.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/continous-integration.yml b/.github/workflows/continous-integration.yml index a45fb8a..6c46e71 100644 --- a/.github/workflows/continous-integration.yml +++ b/.github/workflows/continous-integration.yml @@ -61,7 +61,7 @@ jobs: - name: Run Codeception Tests w/ Docker. env: PHP_VERSION: ${{ matrix.php }} - run: + run: | composer run-codeception -- -- functional composer run-codeception -- -- wpunit --coverage --coverage-xml From a05f37db7fefaea036ff649392595e9257c61607 Mon Sep 17 00:00:00 2001 From: Geoff Taylor Date: Tue, 14 May 2024 16:58:13 -0400 Subject: [PATCH 10/14] devops: CI script codecoverage deployment fixed --- .github/workflows/continous-integration.yml | 3 +-- tests/codeception/acceptance/.gitkeep | 0 tests/codeception/unit/.gitkeep | 0 3 files changed, 1 insertion(+), 2 deletions(-) delete mode 100644 tests/codeception/acceptance/.gitkeep delete mode 100644 tests/codeception/unit/.gitkeep diff --git a/.github/workflows/continous-integration.yml b/.github/workflows/continous-integration.yml index 6c46e71..54b312f 100644 --- a/.github/workflows/continous-integration.yml +++ b/.github/workflows/continous-integration.yml @@ -72,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/tests/codeception/acceptance/.gitkeep b/tests/codeception/acceptance/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/tests/codeception/unit/.gitkeep b/tests/codeception/unit/.gitkeep deleted file mode 100644 index e69de29..0000000 From c375fef22da6db8e7ccae5e9babdbcff23d4224a Mon Sep 17 00:00:00 2001 From: Geoff Taylor Date: Tue, 14 May 2024 17:16:11 -0400 Subject: [PATCH 11/14] devops: Error message added for nested rule failure --- src/Constraint/QueryConstraint.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Constraint/QueryConstraint.php b/src/Constraint/QueryConstraint.php index cc7a9d0..3cca2bf 100644 --- a/src/Constraint/QueryConstraint.php +++ b/src/Constraint/QueryConstraint.php @@ -236,6 +236,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_messages[] = 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; } } From 5fd5b73cc5ddb2bf6098db74870f97243513e216 Mon Sep 17 00:00:00 2001 From: Geoff Taylor Date: Tue, 14 May 2024 17:31:58 -0400 Subject: [PATCH 12/14] devops: Error messaging improved --- src/Constraint/QueryConstraint.php | 61 +++++++++++++------- src/Constraint/QueryErrorConstraint.php | 7 ++- src/Constraint/QuerySuccessfulConstraint.php | 3 +- 3 files changed, 47 insertions(+), 24 deletions(-) diff --git a/src/Constraint/QueryConstraint.php b/src/Constraint/QueryConstraint.php index 3cca2bf..e3e0814 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,7 +243,7 @@ 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_messages[] = sprintf( + $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', @@ -251,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 ), @@ -272,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; } } @@ -335,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( @@ -367,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 @@ -376,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; } } @@ -580,7 +587,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..ffa293d 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. May need to 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 one or more of the expected validation rules.'; return false; } diff --git a/src/Constraint/QuerySuccessfulConstraint.php b/src/Constraint/QuerySuccessfulConstraint.php index 98713d1..b3af2f7 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. May need to use "--debug" flag to see contents of previous request.'; return false; } @@ -42,6 +42,7 @@ public function matches($response): bool { } if ( ! $passed ) { + $this->error_message = 'The GraphQL response failed one or more of the expected validation rules.'; return false; } From e6e9b236cd01e8e366a9a53db6d4a91590e98473 Mon Sep 17 00:00:00 2001 From: Geoff Taylor Date: Tue, 14 May 2024 17:37:15 -0400 Subject: [PATCH 13/14] devops: Error messaging improved --- src/Constraint/QueryConstraint.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Constraint/QueryConstraint.php b/src/Constraint/QueryConstraint.php index e3e0814..a3cf7aa 100644 --- a/src/Constraint/QueryConstraint.php +++ b/src/Constraint/QueryConstraint.php @@ -580,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; } From 1afdcf11bbf6abe06efb16ec5ec00629316730b3 Mon Sep 17 00:00:00 2001 From: Geoff Taylor Date: Tue, 14 May 2024 17:52:42 -0400 Subject: [PATCH 14/14] devops: Unit tests updated --- src/Constraint/QueryConstraint.php | 4 ++-- src/Constraint/QueryErrorConstraint.php | 4 ++-- src/Constraint/QuerySuccessfulConstraint.php | 5 +++-- tests/codeception/wpunit/QueryConstraintTest.php | 2 +- tests/phpunit/unit/test-queryconstraint.php | 2 +- 5 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/Constraint/QueryConstraint.php b/src/Constraint/QueryConstraint.php index a3cf7aa..ae85556 100644 --- a/src/Constraint/QueryConstraint.php +++ b/src/Constraint/QueryConstraint.php @@ -580,7 +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.'; + $this->error_message = 'GraphQL response is invalid'; return false; } @@ -597,7 +597,7 @@ public function failureDescription($other): string { } if ( ! empty( $this->error_details ) ) { - $output .= ": \n\n\t•" . implode( "\n\n\t• ", $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 ffa293d..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_message = '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; } @@ -56,7 +56,7 @@ public function matches($response): bool { } if ( ! $data_passed || ! $error_passed) { - $this->error_message = 'The GraphQL response failed one or more of the expected validation rules.'; + $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 b3af2f7..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_message = '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,7 +43,7 @@ public function matches($response): bool { } if ( ! $passed ) { - $this->error_message = 'The GraphQL response failed one or more of the expected validation rules.'; + $this->error_message = 'The GraphQL response failed the following steps in validation'; return false; } 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/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() {