diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3469612e5..5638fdb46 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -96,6 +96,7 @@ jobs: uses: actions/setup-node@v4 with: node-version-file: '.nvmrc' + cache: 'npm' - name: Cache Composer uses: actions/cache@v4 @@ -105,35 +106,40 @@ jobs: restore-keys: | ${{ runner.os }}-composer-unit- - # This job only needs vendor/bin/phpunit (from Composer) and the wp-env - # CLI. A full `npm ci` pulls ~2,200 packages and has taken anywhere from - # 36s to 7 minutes on hosted runners; @wordpress/env alone is ~400 - # packages and installs in ~30s. It is installed into a scratch prefix - # outside the repo so npm does not reconcile against package-lock.json - # and pull the whole tree anyway. The version is read from the lockfile - # so it cannot drift from what developers run locally. - - name: Install Composer dependencies - run: composer install --no-interaction --no-progress - - - name: Install wp-env - run: | - version=$(node -p "require('./package-lock.json').packages['node_modules/@wordpress/env'].version") - echo "Installing @wordpress/env@$version" - npm install --prefix "$RUNNER_TEMP/wp-env" --no-audit --no-fund "@wordpress/env@$version" - echo "$RUNNER_TEMP/wp-env/node_modules/.bin" >> "$GITHUB_PATH" + # This job only needs vendor/bin/phpunit and the wp-env CLI, so it used to + # install @wordpress/env on its own into a scratch prefix rather than run + # a full `npm ci`. That install resolved dependency ranges fresh against + # npm instead of obeying package-lock.json, which made the job depend on + # whatever upstream had published that day: a broken @wp-playground/cli + # release, pulled in transitively by @wordpress/env, failed every run + # while the lockfile-based jobs were unaffected. + # + # `npm ci` installs the locked tree, so the job can no longer break + # because of a third-party release. The postinstall hook runs + # `patch-package && composer install`, which is what provides + # vendor/bin/phpunit, so this single step covers both toolchains. + # + # Deliberately not `--ignore-scripts`, for the same reason as the e2e + # job below: besides skipping the Composer install, that flag made this + # step take 1m30s instead of ~36s from a cold cache. + - name: Install dependencies + run: npm ci # The wp-env sources directory is deliberately not cached. A restored # ~/.wp-env carries the previous run's install state, which skipped the # plugin's activation hook and left the relationships table missing. + # .wp-env.ci.json omits the HTTPS URLs and the proxy lifecycle script + # that .wp-env.json uses for local development. Runners have no local CA + # and no proxy container, so they serve the site over plain HTTP. - name: Start wp-env - run: wp-env start + run: npx wp-env start --config .wp-env.ci.json - name: Run unit tests - run: wp-env run tests-cli --env-cwd="wp-content/plugins/$(basename "$PWD")" vendor/bin/phpunit + run: npx wp-env run tests-cli --config .wp-env.ci.json --env-cwd="wp-content/plugins/$(basename "$PWD")" vendor/bin/phpunit - name: Stop wp-env if: always() - run: wp-env stop + run: npx wp-env stop --config .wp-env.ci.json e2e: name: E2E (Playwright) @@ -192,7 +198,7 @@ jobs: env: PLAYWRIGHT_CACHE_HIT: ${{ steps.playwright-cache.outputs.cache-hit }} run: | - npm run env:start > wp-env-start.log 2>&1 & + npx wp-env start --config .wp-env.ci.json > wp-env-start.log 2>&1 & wp_env_pid=$! if [ "$PLAYWRIGHT_CACHE_HIT" = "true" ]; then @@ -215,14 +221,18 @@ jobs: exit 1 fi + # Runners have no local CA and no HTTPS proxy, so the suite talks to + # wp-env's published port directly over plain HTTP. WP_BASE_URL is the + # single switch for this; see tests/e2e/playwright.config.js. - name: Run E2E tests env: CLOUDINARY_E2E_URL: ${{ secrets.CLOUDINARY_E2E_URL }} + WP_BASE_URL: http://localhost:8889 run: npm run test:e2e - name: Stop wp-env if: always() - run: npm run env:stop + run: npx wp-env stop --config .wp-env.ci.json - name: Upload Playwright artifacts if: failure() diff --git a/.gitignore b/.gitignore index 686f45bd3..263b138a3 100644 --- a/.gitignore +++ b/.gitignore @@ -46,6 +46,9 @@ coverage/html/ # wp-env personal overrides .wp-env.override.json +# Locally generated TLS certificates and CA for the wp-env HTTPS proxy +/.wp-env/certs/ + # IDE .vscode diff --git a/.wp-env.ci.json b/.wp-env.ci.json new file mode 100644 index 000000000..975eba8e4 --- /dev/null +++ b/.wp-env.ci.json @@ -0,0 +1,26 @@ +{ + "core": "WordPress/WordPress#7.1", + "phpVersion": "8.5", + "plugins": ["."], + "config": { + "WP_DEBUG": true, + "WP_DEBUG_LOG": true, + "SCRIPT_DEBUG": true + }, + "mappings": { + "wp-content/mu-plugins": "./.wp-env/mu-plugins" + }, + "lifecycleScripts": { + "afterStart": "./.wp-env/scripts/fix-loopback.sh" + }, + "env": { + "tests": { + "phpVersion": "8.2", + "config": { + "WP_DEBUG": true, + "WP_DEBUG_LOG": true, + "SCRIPT_DEBUG": true + } + } + } +} diff --git a/.wp-env.json b/.wp-env.json index 975eba8e4..7827d5fec 100644 --- a/.wp-env.json +++ b/.wp-env.json @@ -5,13 +5,18 @@ "config": { "WP_DEBUG": true, "WP_DEBUG_LOG": true, - "SCRIPT_DEBUG": true + "SCRIPT_DEBUG": true, + "WP_HOME": "https://cloudinary.local.wpenv.net", + "WP_SITEURL": "https://cloudinary.local.wpenv.net", + "FORCE_SSL_ADMIN": true, + "WP_CONTENT_URL": "https://cloudinary.local.wpenv.net/wp-content", + "WP_PLUGIN_URL": "https://cloudinary.local.wpenv.net/wp-content/plugins" }, "mappings": { "wp-content/mu-plugins": "./.wp-env/mu-plugins" }, "lifecycleScripts": { - "afterStart": "./.wp-env/scripts/fix-loopback.sh" + "afterStart": "./.wp-env/scripts/after-start.sh" }, "env": { "tests": { @@ -19,7 +24,12 @@ "config": { "WP_DEBUG": true, "WP_DEBUG_LOG": true, - "SCRIPT_DEBUG": true + "SCRIPT_DEBUG": true, + "WP_HOME": "https://tests.cloudinary.local.wpenv.net", + "WP_SITEURL": "https://tests.cloudinary.local.wpenv.net", + "FORCE_SSL_ADMIN": true, + "WP_CONTENT_URL": "https://tests.cloudinary.local.wpenv.net/wp-content", + "WP_PLUGIN_URL": "https://tests.cloudinary.local.wpenv.net/wp-content/plugins" } } } diff --git a/.wp-env/docker/mkcert/Dockerfile b/.wp-env/docker/mkcert/Dockerfile new file mode 100644 index 000000000..0d9bde6a4 --- /dev/null +++ b/.wp-env/docker/mkcert/Dockerfile @@ -0,0 +1,16 @@ +FROM golang:1.24-alpine + +# Set the version tag to build. +ENV MKCERT_VERSION="v1.4.4" + +RUN apk add --no-cache git + +RUN git clone https://github.com/FiloSottile/mkcert /go/mkcert \ + && cd /go/mkcert \ + && git checkout "tags/$MKCERT_VERSION" -b "build/$MKCERT_VERSION" \ + && go build -ldflags "-X main.Version=$MKCERT_VERSION" -o /bin/mkcert + +# mkcert reads and writes its CA here. The compose file mounts the project's +# certificate directory over it, so both the CA and the issued certificates +# stay inside the repository instead of the developer's global mkcert store. +WORKDIR /root/.local/share/mkcert diff --git a/.wp-env/mu-plugins/https-proxy.php b/.wp-env/mu-plugins/https-proxy.php new file mode 100644 index 000000000..83e3af333 --- /dev/null +++ b/.wp-env/mu-plugins/https-proxy.php @@ -0,0 +1,105 @@ +/dev/null) + + if [ -n "$value" ]; then + echo "$value" + return + fi + done +} + +WP_ENV_PORT="${WP_ENV_PORT:-$(read_configured_port 'config.port')}" +WP_ENV_PORT="${WP_ENV_PORT:-8888}" + +WP_ENV_TESTS_PORT="${WP_ENV_TESTS_PORT:-$(read_configured_port 'config.env && config.env.tests && config.env.tests.port')}" +WP_ENV_TESTS_PORT="${WP_ENV_TESTS_PORT:-8889}" + +# Prints the names of this project's wp-env containers, one per line. +# +# Identifies them by the bind mount of this repository, which wp-env adds to +# every WordPress and CLI container as the plugin directory. Matching on the +# container name is not safe: wp-env derives its Compose project name from a +# hash of the config path, so a pattern loose enough to match it also matches +# other projects' containers, and these scripts would then edit /etc/hosts and +# the certificate store of an unrelated environment. +# +# Only the WordPress and CLI services carry this mount, so the database +# containers, which need neither the host alias nor the certificate authority, +# are excluded automatically. +wp_env_containers() { + local container + + for container in $(docker ps --format '{{.Names}}'); do + if docker inspect "$container" --format '{{json .Mounts}}' 2>/dev/null | + grep -q "\"Source\":\"$PROJECT_DIR\""; then + echo "$container" + fi + done +} diff --git a/.wp-env/scripts/fix-loopback.sh b/.wp-env/scripts/fix-loopback.sh index 423fc6265..abb5e35f8 100755 --- a/.wp-env/scripts/fix-loopback.sh +++ b/.wp-env/scripts/fix-loopback.sh @@ -1,28 +1,33 @@ #!/bin/bash -# Fix loopback requests in wp-env Docker environment. +# Fix plain-HTTP loopback requests in the wp-env Docker environment. # -# WordPress in wp-env thinks its URL is localhost:8888, but inside the -# container Apache only listens on port 80. This causes self-pinging -# REST API requests (used by Cloudinary's sync daemon) to fail with -# cURL error 7. Adding port 8888 to Apache resolves this. +# wp-env publishes WordPress on a host port (8888 by default), but inside the +# container Apache only listens on port 80. Self-pinging REST API requests +# (used by Cloudinary's sync daemon) that target the published port therefore +# fail with cURL error 7. Adding that port to Apache resolves this. # -# This script finds the wp-env WordPress container and configures Apache -# to also listen on port 8888, enabling loopback requests to succeed. +# The environment now runs over HTTPS through the proxy in .wp-env/proxy/, +# where loopback goes through the proxy instead. This fix stays because the +# published port remains reachable as a fallback, and because the CI +# environment (.wp-env.ci.json) runs on plain HTTP and relies on it. -# Find the wp-env wordpress container (exclude tests container). -CONTAINER=$(docker ps --format '{{.Names}}' | grep -E 'wordpress-1$' | grep -v tests | head -1) +source "$(dirname "${BASH_SOURCE[0]}")/config.sh" + +# The development WordPress container. wp_env_containers scopes the search to +# this project; see config.sh for why the container name cannot be matched +# directly. +CONTAINER=$(wp_env_containers | grep -v -- '-tests-' | grep -- '-wordpress-' | head -1) if [ -z "$CONTAINER" ]; then echo "Warning: Could not find wp-env WordPress container. Loopback fix skipped." exit 0 fi -# Add Listen 8888 if not already present, then graceful restart Apache. -docker exec "$CONTAINER" bash -c \ - "grep -q 'Listen 8888' /etc/apache2/ports.conf || (echo 'Listen 8888' >> /etc/apache2/ports.conf && apache2ctl graceful)" 2>/dev/null - -if [ $? -eq 0 ]; then - echo "Loopback fix applied: Apache now also listens on port 8888 inside the container." +# Add the listener if not already present, then gracefully restart Apache. +# The port comes from config.sh, which resolves any wp-env port override. +if docker exec "$CONTAINER" bash -c \ + "grep -q 'Listen $WP_ENV_PORT' /etc/apache2/ports.conf || (echo 'Listen $WP_ENV_PORT' >> /etc/apache2/ports.conf && apache2ctl graceful)" 2>/dev/null; then + echo "Loopback fix applied: Apache now also listens on port $WP_ENV_PORT inside the container." else echo "Warning: Failed to apply loopback fix." fi diff --git a/.wp-env/scripts/proxy-down.sh b/.wp-env/scripts/proxy-down.sh new file mode 100755 index 000000000..f0cc57dd4 --- /dev/null +++ b/.wp-env/scripts/proxy-down.sh @@ -0,0 +1,18 @@ +#!/bin/bash +# Stop the TLS proxy. +# +# The proxy is its own compose project, so `wp-env stop` does not know about it. +# The env:stop and env:destroy npm scripts call this first. + +set -euo pipefail + +source "$(dirname "${BASH_SOURCE[0]}")/config.sh" + +export DEV_HOST TESTS_HOST PROXY_HTTP_PORT PROXY_HTTPS_PORT WP_ENV_PORT WP_ENV_TESTS_PORT + +docker compose \ + --project-name "$PROXY_PROJECT" \ + --file "$PROXY_DIR/docker-compose.yml" \ + down --remove-orphans + +echo "HTTPS proxy stopped." diff --git a/.wp-env/scripts/proxy-up.sh b/.wp-env/scripts/proxy-up.sh new file mode 100755 index 000000000..36c0dc506 --- /dev/null +++ b/.wp-env/scripts/proxy-up.sh @@ -0,0 +1,68 @@ +#!/bin/bash +# Start the TLS proxy in front of wp-env. +# +# Issues the certificate on first run, then brings up nginx on ports 80 and 443. +# Run automatically by after-start.sh; safe to run on its own to restart the +# proxy without restarting wp-env. + +set -euo pipefail + +source "$(dirname "${BASH_SOURCE[0]}")/config.sh" + +# Report which container or process holds a port, so a conflict names the +# culprit instead of surfacing as a raw Docker bind error. +describe_port_holder() { + local port="$1" + local container + + container=$(docker ps --format '{{.Names}}\t{{.Ports}}' | grep -E ":$port->" | cut -f1 | head -1) + + if [ -n "$container" ]; then + echo "Docker container '$container'" + return + fi + + local process + process=$(lsof -nP -iTCP:"$port" -sTCP:LISTEN -Fc 2>/dev/null | grep '^c' | head -1 | cut -c2-) + + if [ -n "$process" ]; then + echo "process '$process'" + return + fi + + echo "another process" +} + +# Ignore ports already held by our own proxy; those are from a previous start +# and compose will reuse them. +check_port() { + local port="$1" + + if [ -z "$(lsof -nP -iTCP:"$port" -sTCP:LISTEN -t 2>/dev/null)" ]; then + return 0 + fi + + if docker compose --project-name "$PROXY_PROJECT" ps --quiet 2>/dev/null | grep -q .; then + return 0 + fi + + echo "Error: port $port is already in use by $(describe_port_holder "$port")." >&2 + echo "Stop it and run 'npm run env:proxy:up' to finish starting the HTTPS proxy." >&2 + return 1 +} + +check_port "$PROXY_HTTP_PORT" +check_port "$PROXY_HTTPS_PORT" + +mkdir -p "$CERTS_DIR" + +export DEV_HOST TESTS_HOST PROXY_HTTP_PORT PROXY_HTTPS_PORT WP_ENV_PORT WP_ENV_TESTS_PORT + +docker compose \ + --project-name "$PROXY_PROJECT" \ + --file "$PROXY_DIR/docker-compose.yml" \ + up --detach --build --remove-orphans + +echo "HTTPS proxy running:" +echo " Development: https://$DEV_HOST" +echo " Tests: https://$TESTS_HOST" diff --git a/.wp-env/scripts/run-e2e.sh b/.wp-env/scripts/run-e2e.sh new file mode 100755 index 000000000..dc883032e --- /dev/null +++ b/.wp-env/scripts/run-e2e.sh @@ -0,0 +1,25 @@ +#!/bin/bash +# Run Playwright with the local certificate authority trusted, when there is one. +# +# Chromium trusts the local CA through the OS keychain (`npm run +# env:install-cert`), but Playwright's Node-side APIRequestContext, which +# globalSetup uses to authenticate, ships its own CA bundle and ignores the +# keychain. NODE_EXTRA_CA_CERTS points Node at the CA. +# +# The variable is only exported when the file is actually present. Node prints +# "Ignoring extra certs ... No such file or directory" when it is not, which is +# noise in CI, where the suite runs over plain HTTP and no certificate exists. +# +# Arguments are passed through to `playwright test`. + +set -euo pipefail + +source "$(dirname "${BASH_SOURCE[0]}")/config.sh" + +CA_FILE="$CERTS_DIR/rootCA.pem" + +if [ -z "${NODE_EXTRA_CA_CERTS:-}" ] && [ -f "$CA_FILE" ]; then + export NODE_EXTRA_CA_CERTS="$CA_FILE" +fi + +exec npx playwright test --config tests/e2e/playwright.config.js "$@" diff --git a/.wp-env/scripts/trust-ca.sh b/.wp-env/scripts/trust-ca.sh new file mode 100755 index 000000000..a16ac89e9 --- /dev/null +++ b/.wp-env/scripts/trust-ca.sh @@ -0,0 +1,81 @@ +#!/bin/bash +# Make HTTPS loopback requests work inside the wp-env containers. +# +# The plugin's sync daemon calls its own REST API. Once WP_HOME is an https:// +# URL those calls leave the container, so the container has to be able to both +# resolve the host name and verify the certificate: +# +# 1. Point the proxy host names at the Docker host gateway, because +# cloudinary.local.wpenv.net resolves to 127.0.0.1, which inside a +# container means the container itself. +# 2. Install the mkcert root CA so the self-signed certificate verifies. +# +# Without step 2 every loopback request would need sslverify disabled, and the +# HTTPS path would never be exercised the way it is in production. +# +# Container IPs and the gateway address change between restarts, so this runs on +# every `wp-env start` and rewrites what it finds. + +set -euo pipefail + +source "$(dirname "${BASH_SOURCE[0]}")/config.sh" + +CA_FILE="$CERTS_DIR/rootCA.pem" + +if [ ! -f "$CA_FILE" ]; then + echo "Warning: no root CA at $CA_FILE. Run 'npm run env:proxy:up' first; skipping loopback trust setup." + exit 0 +fi + +# This project's WordPress and CLI containers. The CLI containers are included +# so WP-CLI commands and the PHPUnit suite reach the site over HTTPS too. +CONTAINERS=$(wp_env_containers) + +if [ -z "$CONTAINERS" ]; then + echo "Warning: no wp-env containers found. Loopback trust setup skipped." + exit 0 +fi + +for container in $CONTAINERS; do + # host.docker.internal is mapped to host-gateway in wp-env's compose file, + # so resolving it inside the container gives the address the proxy is + # reachable on. + gateway=$(docker exec "$container" getent hosts host.docker.internal 2>/dev/null | awk '{print $1}' | head -1) + + if [ -z "$gateway" ]; then + echo "Warning: could not resolve the host gateway in $container; skipping." + continue + fi + + # Replace any previous entry so a changed gateway address cannot leave a + # stale line behind, then append the current one. + # + # Docker bind-mounts /etc/hosts, so `sed -i` fails with "Device or resource + # busy": it works by renaming a temporary file over the target. Filter into + # a temporary file and copy the contents back instead, which writes through + # the existing inode. Errors are surfaced rather than discarded, because a + # silent failure here leaves a stale address behind and breaks loopback in a + # way that is hard to trace back to this script. + if ! docker exec --user root "$container" bash -c " + set -e + grep -v -e '$DEV_HOST' -e '$TESTS_HOST' /etc/hosts > /tmp/hosts.new + echo '$gateway $DEV_HOST $TESTS_HOST' >> /tmp/hosts.new + cat /tmp/hosts.new > /etc/hosts + rm -f /tmp/hosts.new + "; then + echo "Warning: could not update /etc/hosts in $container." + fi + + # update-ca-certificates rebuilds the bundle that both PHP and curl read. + docker cp "$CA_FILE" "$container:/usr/local/share/ca-certificates/mkcert-root-ca.crt" >/dev/null 2>&1 || { + echo "Warning: could not copy the root CA into $container." + continue + } + + docker exec --user root "$container" update-ca-certificates >/dev/null 2>&1 || { + echo "Warning: could not install the root CA in $container." + continue + } +done + +echo "Loopback trust configured: containers resolve $DEV_HOST and trust the local CA." diff --git a/README.md b/README.md index 15ef3665a..5c3a10b65 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,7 @@ Stay tuned for updates, tips and tutorials: [Blog](https://cloudinary.com/blog), - [npm](https://www.npmjs.com/) v10+ - [Composer](https://getcomposer.org/) - [Docker](https://www.docker.com/) (required for the WordPress local environment via `wp-env`) +- [mkcert](https://github.com/FiloSottile/mkcert) (required once, to trust the local HTTPS certificate) ### Local Development Setup @@ -74,9 +75,17 @@ Stay tuned for updates, tips and tutorials: [Blog](https://cloudinary.com/blog), npm run env:start ``` - This spins up a WordPress instance at [http://localhost:8888](http://localhost:8888) with the plugin activated and `WP_DEBUG` enabled. A loopback fix is applied automatically so REST API self-requests work inside the container. + This spins up a WordPress instance at [https://cloudinary.local.wpenv.net](https://cloudinary.local.wpenv.net) with the plugin activated and `WP_DEBUG` enabled, plus a tests instance at [https://tests.cloudinary.local.wpenv.net](https://tests.cloudinary.local.wpenv.net). An nginx proxy serves both over HTTPS, so local behaviour matches production for `is_ssl()`, `Secure` cookies and loopback requests. No `/etc/hosts` entry is needed. -5. **Build front-end assets:** +5. **Trust the local certificate (first run only):** + + ```bash + npm run env:install-cert + ``` + + Adds the generated certificate authority to your OS trust store, so the browser accepts the site without a warning. It asks for your password. Certificates live in `.wp-env/certs/`, which is gitignored. + +6. **Build front-end assets:** ```bash npm run build # One-time production build @@ -90,6 +99,9 @@ Stay tuned for updates, tips and tutorials: [Blog](https://cloudinary.com/blog), | `npm run env:start` | Start the local WordPress environment | | `npm run env:stop` | Stop the local WordPress environment | | `npm run env:destroy` | Remove the local environment completely | +| `npm run env:install-cert` | Trust the local HTTPS certificate (once) | +| `npm run env:proxy:up` | Start the HTTPS proxy on its own | +| `npm run env:proxy:down` | Stop the HTTPS proxy | | `npm run env:logs` | View container logs | | `npm run env:cli` | Run WP-CLI commands inside the container | | `npm run env:clean` | Reset the environment (removes all data) | @@ -103,6 +115,16 @@ Stay tuned for updates, tips and tutorials: [Blog](https://cloudinary.com/blog), | `npm run lint:style` | Run stylelint on SCSS files | | `npm run i18n` | Generate translation files | +### Troubleshooting the local environment + +| Symptom | Fix | +| ------- | --- | +| Browser warns the certificate is untrusted | `npm run env:install-cert`. If it persists, delete `.wp-env/certs/`, run `npm run env:start`, then trust it again. | +| Port 80 or 443 is in use | Another project holds it; the error names the container. Stop it, then `npm run env:proxy:up`. | +| `Call to undefined function Cloudinary\get_plugin_instance()` | The plugin is inactive after switching between `.wp-env.json` and `.wp-env.ci.json`. Destroy and start again under the same config. | + +CI runs over plain HTTP via `--config .wp-env.ci.json`, because runners have no certificate authority. Keep the shared values in both config files in sync. + ### Create a Plugin Release Package Run `npm run package` to create the plugin release in the `/build` directory and package it as `cloudinary-image-management-and-manipulation-in-the-cloud-cdn.zip` in the root directory. @@ -134,6 +156,7 @@ E2E tests run against a wp-env site using Playwright. npm install npx playwright install --with-deps chromium npm run env:start +npm run env:install-cert ``` ### Running the tests @@ -142,6 +165,12 @@ npm run env:start npm run test:e2e ``` +The suite runs against the HTTPS tests site. Start it through the npm scripts, which point Node at the local certificate authority. Override the target with `WP_BASE_URL`: + +```bash +WP_BASE_URL=http://localhost:8889 npm run test:e2e +``` + ### Wizard test credentials `tests/e2e/wizard-setup.spec.js` exercises the live Cloudinary connection flow, so it needs a real connection string. Provide one of two ways: diff --git a/package.json b/package.json index f10faf94a..345c33435 100644 --- a/package.json +++ b/package.json @@ -20,8 +20,11 @@ "deploy-assets": "grunt deploy-assets", "dev": "wp-scripts start", "env:start": "wp-env start", - "env:stop": "wp-env stop", - "env:destroy": "wp-env destroy", + "env:stop": "./.wp-env/scripts/proxy-down.sh && wp-env stop", + "env:destroy": "./.wp-env/scripts/proxy-down.sh && wp-env destroy", + "env:install-cert": "CAROOT=./.wp-env/certs mkcert -install", + "env:proxy:up": "./.wp-env/scripts/proxy-up.sh", + "env:proxy:down": "./.wp-env/scripts/proxy-down.sh", "env:logs": "wp-env logs", "env:cli": "wp-env run cli", "env:clean": "wp-env clean all", @@ -38,9 +41,9 @@ "readme": "composer readme", "prepare": "husky", "test:e2e": "npm-run-all --silent test:e2e:parallel test:e2e:serial", - "test:e2e:parallel": "playwright test --config tests/e2e/playwright.config.js --grep-invert @serial", - "test:e2e:serial": "playwright test --config tests/e2e/playwright.config.js --grep @serial --workers=1", - "test:e2e:debug": "playwright test --config tests/e2e/playwright.config.js --ui", + "test:e2e:parallel": "./.wp-env/scripts/run-e2e.sh --grep-invert @serial", + "test:e2e:serial": "./.wp-env/scripts/run-e2e.sh --grep @serial --workers=1", + "test:e2e:debug": "./.wp-env/scripts/run-e2e.sh --ui", "test:unit": "wp-env run tests-cli --env-cwd=\"wp-content/plugins/$(basename \"$PWD\")\" vendor/bin/phpunit" }, "lint-staged": { diff --git a/tests/e2e/global-setup.js b/tests/e2e/global-setup.js index 9a241ce97..5eb0f0796 100644 --- a/tests/e2e/global-setup.js +++ b/tests/e2e/global-setup.js @@ -26,7 +26,7 @@ module.exports = async function globalSetup( config ) { fs.mkdirSync( path.dirname( storageStatePath ), { recursive: true } ); const requestContext = await request.newContext( { - baseURL: baseURL || 'http://localhost:8889', + baseURL: baseURL || 'https://tests.cloudinary.local.wpenv.net', } ); const requestUtils = new RequestUtils( requestContext, { diff --git a/tests/e2e/playwright.config.js b/tests/e2e/playwright.config.js index 00605eb83..8fd788edf 100644 --- a/tests/e2e/playwright.config.js +++ b/tests/e2e/playwright.config.js @@ -2,6 +2,7 @@ * External dependencies */ const { defineConfig, devices } = require( '@playwright/test' ); +const fs = require( 'fs' ); const path = require( 'path' ); // Load env vars from a project-root .env file so devs don't have to @@ -18,6 +19,40 @@ const STORAGE_STATE_PATH = process.env.STORAGE_STATE_PATH || path.join( process.cwd(), 'artifacts/storage-states/admin.json' ); +// @wordpress/e2e-test-utils-playwright reads WP_BASE_URL from the environment +// rather than from Playwright's `baseURL`, and falls back to +// http://localhost:8889 (see its build/config.js). Setting the variable here +// keeps the URL defined in one place: RequestUtils, the storage state and the +// browser contexts all agree, and a stale localhost default cannot send +// requests around the proxy. +const BASE_URL = + process.env.WP_BASE_URL || 'https://tests.cloudinary.local.wpenv.net'; + +process.env.WP_BASE_URL = BASE_URL; + +// The local environment is served over HTTPS by the proxy in .wp-env/proxy/, +// using a certificate from the locally generated CA. Chromium trusts it via the +// OS keychain (`npm run env:install-cert`), but Playwright's Node-side +// APIRequestContext -- which globalSetup uses to authenticate -- ships its own +// CA bundle and ignores the keychain. +// +// NODE_EXTRA_CA_CERTS is read once when Node starts, so it cannot be set from +// here; .wp-env/scripts/run-e2e.sh exports it before launching Playwright. +// Fail loudly rather than let the run die later inside globalSetup with an +// opaque TLS error. +const LOCAL_CA_PATH = path.join( process.cwd(), '.wp-env/certs/rootCA.pem' ); + +if ( + BASE_URL.startsWith( 'https://' ) && + ! process.env.NODE_EXTRA_CA_CERTS && + fs.existsSync( LOCAL_CA_PATH ) +) { + throw new Error( + 'NODE_EXTRA_CA_CERTS is not set, so Node cannot verify the local HTTPS certificate.\n' + + 'Run the suite with `npm run test:e2e`, which sets it for you.' + ); +} + module.exports = defineConfig( { testDir: '.', reporter: process.env.CI ? [ [ 'github' ], [ 'list' ] ] : 'list', @@ -38,7 +73,7 @@ module.exports = defineConfig( { outputDir: path.join( process.cwd(), 'artifacts/test-results' ), globalSetup: require.resolve( './global-setup.js' ), use: { - baseURL: process.env.WP_BASE_URL || 'http://localhost:8889', + baseURL: BASE_URL, trace: 'retain-on-failure', screenshot: 'only-on-failure', video: 'retain-on-failure',