Skip to content

Latest commit

Β 

History

History
526 lines (378 loc) Β· 24.8 KB

File metadata and controls

526 lines (378 loc) Β· 24.8 KB

Contributing to WP Sudo

Prerequisites

  • PHP 8.1+
  • Composer
  • MySQL 8.0 server plus client tools (mysql, mysqladmin) (for integration tests only)
  • SVN + unzip (for integration test setup only β€” apt install subversion unzip on Ubuntu, brew install subversion on macOS)

Setup

composer install

Git hooks

Install the pre-commit hook to enforce reviewer agent approval before every AI-generated commit:

cp .githooks/pre-commit .git/hooks/pre-commit
chmod +x .git/hooks/pre-commit

For your own (non-AI) commits, bypass the hook with USER_COMMIT=1:

USER_COMMIT=1 git commit -m "message"

Running Tests

Unit tests (fast, no database)

composer test:unit

Runs in ~0.3s. No external dependencies β€” all WordPress functions are mocked with Brain\Monkey.

Integration tests (real WordPress + MySQL)

# Recommended: pin WP test paths to the repo-local .tmp directory.
# This avoids accidentally using stale global /tmp test configs with mismatched DB credentials.
export WP_TESTS_DIR="$PWD/.tmp/wordpress-tests-lib"
export WP_CORE_DIR="$PWD/.tmp/wordpress"

# One-time setup: installs the WordPress 7.0 (GA) test library and creates test DB.
# 7.0 resolves to the branches/7.0 test library (the same lane CI's WP 7.0 matrix uses)
# and the latest 7.0.x core. Pass "latest" to track the newest stable line instead.
bash bin/install-wp-tests.sh wordpress_test root root 127.0.0.1 7.0

# Run tests
composer test:integration

Set WP_MULTISITE=1 to run the multisite test suite:

WP_MULTISITE=1 composer test:integration

If the generated host-side WordPress test config points at a stale or unreachable Docker-published MySQL port after a wp-env rebuild, composer test:integration now falls back automatically to the running wp-env tests-cli container and executes the same suite there against the tests-mysql service.

If 127.0.0.1 is not serving MySQL but Local by Flywheel is running, bin/install-wp-tests.sh now auto-detects a single Local MySQL socket under ~/Library/Application Support/Local/run/*/mysql/mysqld.sock and rewrites the generated DB_HOST to localhost:/path/to/mysqld.sock.

If you have multiple Local sites running and want to choose a specific socket, pass it explicitly:

WP_TESTS_DIR="$PWD/.tmp/wordpress-tests-lib" \
WP_CORE_DIR="$PWD/.tmp/wordpress" \
bash bin/install-wp-tests.sh \
  wordpress_test \
  root \
  root \
  "localhost:$HOME/Library/Application Support/Local/run/<site-id>/mysql/mysqld.sock" \
  7.0

To discover the available Local sockets:

find "$HOME/Library/Application Support/Local/run" -name mysqld.sock

If you do not want to export variables in your shell, prefix each command:

WP_TESTS_DIR="$PWD/.tmp/wordpress-tests-lib" \
WP_CORE_DIR="$PWD/.tmp/wordpress" \
composer test:integration

WP_TESTS_DIR="$PWD/.tmp/wordpress-tests-lib" \
WP_CORE_DIR="$PWD/.tmp/wordpress" \
WP_MULTISITE=1 composer test:integration

E2E tests against Studio/Local (Flywheel)

You can run Playwright against an existing local WordPress site (Studio or Local by Flywheel) instead of wp-env:

WP_BASE_URL="http://your-local-site.test" \
WP_USERNAME="admin" \
WP_PASSWORD="your-password" \
npm run test:e2e:local

Use the local site's real admin credentials. WP_BASE_URL can be http:// or https:// depending on your local environment.

For WordPress Studio, use the Studio site's localhost port directly. Studio is the recommended local path for SQLite-focused verification because it avoids pretending that the default wp-env Docker stack exercises SQLite:

WP_BASE_URL="http://localhost:8881" \
WP_USERNAME="admin" \
WP_PASSWORD="password" \
npm run test:e2e:local

If you point Playwright at a Studio site, keep WP_BASE_URL and WP_REQUEST_BASE_URL on the same localhost:<port> origin unless you intentionally configured a custom domain for that Studio instance.

To run the multisite network-admin regression against the multisite-subdomains Local site:

WP_BASE_URL="https://multisite-subdomains.local" \
WP_USERNAME="test" \
WP_PASSWORD="test" \
npm run test:e2e:local:multisite

This spec is local-only. It skips automatically under the default single-site wp-env configuration used in CI.

Local site plugin drift and symlink management

Some Local sites use a copied wp-content/plugins/wp-sudo directory instead of the live repo checkout. In that case the site can drift behind main even when this repo is up to date.

The canonical helper for compare/sync/symlink operations is:

npm run local:plugin -- status
npm run local:plugin -- sync
npm run local:plugin -- link

By default it targets the active Local multisite environment used for multisite-subdomains.local:

/Users/danknauss/Development/Local Sites/multisite-subdomains/app/public/wp-content/plugins/wp-sudo

Override that path with SITE_PLUGIN=/path/to/site/wp-content/plugins/wp-sudo.

Before debugging unexpected Local-site behaviour, run:

npm run local:plugin -- status

If the site copy differs, sync it from the repo before continuing:

npm run local:plugin -- sync

If you want zero drift risk, replace the copied plugin directory with a symlink to this repo checkout:

npm run local:plugin -- link

The link command preserves the previous copied plugin directory as a timestamped backup before replacing it with a symlink.

E2E CI split: functional vs visual

The Playwright suite is split into two projects:

  • chromium β€” required functional E2E coverage. This is what npm run test:e2e runs locally and what the required E2E Tests GitHub workflow runs in CI.
  • chromium-visual β€” visual regression baselines only. This is what npm run test:e2e:visual runs locally and what the separate non-blocking E2E Visual Baselines workflow runs.

Run the functional suite locally:

npm run test:e2e

If wp-env gets stuck after a partial Docker startup, clear the generated stack and retry:

npm run env:stop
npm run env:clean
npm run env:start
npm run env:assert-wp-version

The last command verifies that both wp-env environments are actually running WordPress 7.0-RC1, not just that the containers started.

Run both Playwright projects locally:

npm run test:e2e:all

Run visual baselines only:

npm run test:e2e:visual

Refresh visual snapshots intentionally:

npm run test:e2e:visual -- --update-snapshots

Do not update visual snapshots just to make required CI pass. Review the uploaded Playwright artifacts from the E2E Visual Baselines workflow first, then refresh snapshots only when the UI change is intentional.

Release CI gates vs breadth checks

WP Sudo now has two different kinds of browser/compatibility workflows:

  • Primary release gates: PHPUnit, Psalm, CodeQL, E2E Tests, and E2E Nginx Smoke. These should all be green before tagging a release.
  • Breadth workflows: WordPress Compat Sweep and E2E SQLite Smoke. These expand compatibility signal across older WordPress minors and SQLite, but they are not the fast-feedback path for every push or PR.

The nginx smoke workflow is treated as a first-class release gate because it is fast, stable, and catches stack-sensitive issues around routing, cookies, redirects, replay, and AJAX behavior that the default Apache stack can miss.

The scheduled compatibility sweep and SQLite smoke workflow should still be checked before release when the touched code could plausibly affect:

  • WordPress version compatibility (6.3 through 6.6)
  • MariaDB-specific behavior outside the main integration matrix
  • SQLite request handling, bootstrap timing, or alternate persistence behavior
  • stack-sensitive flows such as redirects, cookies, request replay, and admin/AJAX request handling

When SQLite-specific release assurance matters, run the Studio checklist in docs/studio-sqlite-release-runbook.md instead of treating the CI smoke lane as a full substitute for local SQLite verification.

When expanding the alternate-stack smoke pack, keep it focused on stack-sensitive behaviors only. Do not clone the full Playwright suite onto every environment. Prefer adding or extending smoke cases when a flow depends on:

  • cookies or session persistence
  • redirects or replay after auth
  • admin-bar or AJAX request handling
  • settings POST replay
  • challenge rendering or browser-visible auth transitions

If mysql or mysqladmin is not found, install client tooling first:

# macOS (Homebrew)
brew install mysql-client

# Ubuntu/Debian
sudo apt install mysql-client

Homebrew may install client binaries without linking them on PATH. If needed:

# Apple Silicon
export PATH="/opt/homebrew/opt/mysql-client/bin:$PATH"

# Intel macOS
export PATH="/usr/local/opt/mysql-client/bin:$PATH"

Static analysis + code style

composer analyse:phpstan  # PHPStan
composer analyse:psalm    # Psalm + WordPress plugin/stubs
composer analyse          # Runs both
composer lint      # PHPCS (WordPress-Extra + WordPress-Docs + VIP rulesets)
composer lint:fix  # Auto-fix PHPCS violations

Metrics refresh (when counts change)

Current live counts are centralized in docs/current-metrics.md.

composer verify:metrics

If it reports drift, update docs/current-metrics.md first, then re-run composer verify:metrics until it passes.

Documentation drift checklist

  • Update docs/current-metrics.md first whenever counts change.
  • Update docs/release-status.md first whenever release state changes (stable tag, unreleased main work, latest supported WordPress release, forward-lane pin, or delayed release date assumptions).
  • Prefer linking to docs/current-metrics.md and docs/release-status.md instead of copying volatile counts or dates into prose.
  • When WordPress release timing changes, grep for stale fixed-date references (for example April 9, 2026, 7.0-RC1, or GA) across docs/, readmes, and maintainer instruction files.
  • Treat .planning/ as historical working material unless a file explicitly says it is current.

Test Strategy

Two environments are used deliberately β€” choose based on what you are testing:

Unit tests (tests/Unit/) use Brain\Monkey to mock all WordPress functions. Use for:

  • Request matching logic (Gate surfaces, action registry)
  • Session state machine and policy enforcement
  • Hook registration
  • Settings sanitization and defaults
  • Upgrader migration logic

Integration tests (tests/Integration/) load real WordPress against a MySQL database. Use for:

  • Full reauth flows (Gate β†’ Challenge β†’ Session β†’ Stash)
  • Real bcrypt password verification (wp_check_password)
  • Transient TTL and cookie behavior
  • REST and AJAX gating
  • Two Factor plugin interaction
  • Multisite session isolation
  • Upgrader migrations against real DB

When in doubt: if the test needs a real database, real crypto, or calls that cross class boundaries in production, write an integration test.

Coverage Goals

Do not optimize for a single global coverage percentage. WP Sudo is a security plugin, so coverage targets are risk-weighted by test category:

  • Unit tests: target 90%+ line coverage on business logic in includes/, and near-100% branch coverage on security-critical state machines and policy code such as Sudo_Session, Gate, Challenge, Action_Registry, and request replay logic.
  • Integration tests: target 100% scenario coverage for security-critical WordPress flows at least once in a real environment. That includes session activation/expiry, grace-window behavior, password failure/throttle/lockout/expiry recovery, 2FA pending/validation/resend/expiry, stash save/get/delete/replay, AJAX/REST/admin exit behavior, and multisite isolation.
  • E2E/browser tests: target 100% coverage of every distinct user-visible challenge/replay branch. Each visible password, 2FA, stale-session, resend, throttle, lockout, expiry-recovery, and replay path should have at least one Playwright case.
  • Bridge files: target 100% unit coverage for first-party integration bridges. These adapters are thin and should be fully covered directly.
  • Lifecycle code: target 100% integration coverage for activation, deactivation, uninstall, and upgrade routines.

Static analysis is part of the assurance target, not optional polish. composer analyse:phpstan, composer analyse:psalm, composer lint, and the required GitHub workflows should remain green at all times.

Current Coverage Snapshot

Current live counts and matrix details are centralized in docs/current-metrics.md. Current stable-vs-forward release posture is centralized in docs/release-status.md. Keep this section qualitative to avoid stale duplication.

In broad terms, the strongest covered area is the challenge flow itself: password and 2FA auth, stale tabs, resend behavior, throttle/lockout UX, expiry recovery, and request replay all have automated coverage. The main remaining gaps are matrix depth on alternate stacks rather than core flow depth: the full browser suite still runs only on the default Apache + MariaDB lane, while nginx, multisite nginx, and SQLite currently run focused stack-smoke subsets.

Separate Multisite Alternate-Stack Lane

Keep tests/e2e/specs/stack-smoke.spec.ts single-site only. Its job is fast stack-sensitivity coverage around cookies, redirects, replay, AJAX, and REST, not full environment matrix depth.

Multisite alternate-stack coverage now lives in its own lane:

That lane intentionally starts small:

  • network-admin challenge cancel/return behavior
  • one gated network-admin POST replay

It is a breadth workflow, not a merge gate. Keep single-site and multisite alternate-stack signals separate so failures stay easy to interpret. Keep SQLite multisite out of CI until the nginx + MariaDB multisite lane proves stable over time.

WordPress Playground

Every PR automatically gets a "Try in WordPress Playground" comment with a link that installs the plugin from that PR's commit and lands you in the admin logged in as admin / password.

Current Playground previews are pinned to WordPress 7.0-RC1. See docs/release-status.md for the current forward-lane posture and latest stable WordPress release.

For WordPress 7.0 release signoff, do not treat the green RC-era CI matrix as a substitute for the remaining RC/GA manual passes. RC1 is recorded in the 15.0 Release Signoff Log table in tests/MANUAL-TESTING.md; repeat that signoff for the scheduled RC3/RC4 checkpoints and again for the final 7.0 release before claiming final readiness. See docs/release-status.md for the current dates.

Blueprint files

The repo ships several Playground blueprints. All are source-only artifacts and are excluded from the distributed plugin ZIP (see .pressshipignore).

File Tier Purpose Install source Documented in
blueprint.json Public demo "Try latest release" β€” stable-tag demo with the standard seed archive/refs/tags/vX.Y.Z.zip (bumped at tag time) readme.md / readme.txt badges
blueprint-main.json Public demo "Try main" β€” tracks the main branch archive/refs/heads/main.zip readme.md / readme.txt badges
blueprint-editor-reauth.json Public/reviewer scenario In-editor reauthentication demo β€” opens the block editor without an active sudo session so Block Directory plugin install/activate can trigger the modal release branch or main while the feature is unreleased; tag-pinned copy after release if kept as a public demo readme.md while featured; docs/ui-ux-testing-prompts.md Β§6c
blueprint-editor-2fa.json Public/reviewer scenario In-editor two-factor demo (Milestone B) β€” installs Two Factor, enrolls the admin in TOTP (public sandbox secret), and shows the current code in an editor notice so the in-modal 2FA step can be completed after the password main while unreleased; tag-pinned copy after release if kept as a public demo readme.md while featured
blueprint-recovery-mode.json Reviewer scenario Break-glass WP_SUDO_RECOVERY_MODE demo main docs/ui-ux-testing-prompts.md Β§6a
blueprint-user-switching.json Reviewer scenario Session-theft demo via the User Switching plugin main docs/ui-ux-testing-prompts.md Β§6b
blueprint-multisite.json Public/reviewer scenario Multisite/network-admin demo β€” converts the standard-seed single site to a network (enableMultisite), network-activates WP Sudo, and lands the super admin on the network Sudo settings page (/wp-admin/network/settings.php?page=wp-sudo-settings) main while featured; tag-pinned copy after release if kept as a public demo readme.md while featured; docs/ui-ux-testing-prompts.md Β§6d
.github/playground/sqlite-stack-smoke.blueprint.json CI smoke SQLite drop-in stack smoke used by the E2E-SQLite workflow (CI-driven) .github/workflows/e2e-sqlite.yml

The two standard public blueprints are surfaced via the readme "Try in Playground" badges. Feature-specific public demos, such as the in-editor reauthentication blueprint, may be temporarily surfaced while the feature is new; before tagging a release, either tag-pin them like blueprint.json or clearly move them back to reviewer-only status. The reviewer blueprints stage hard-to-reach states for manual review (not linked from the readme by default); the CI smoke blueprint is owned by its workflow. When adding a new blueprint, add it to .pressshipignore and to this table.

WordPress 7.0 Final Prep Checklist

Use this checklist for each later RC and again at GA:

  1. Repin the forward WordPress lane references if the build string changed:
  2. Run section 15 in tests/MANUAL-TESTING.md and record the result in the 15.0 Release Signoff Log.
  3. Verify the standard local checks:
    • composer test:integration
    • WP_MULTISITE=1 composer test:integration
    • composer analyse:phpstan
    • composer analyse:psalm
    • composer lint
  4. At GA only, update Tested up to and release metadata surfaces:
  5. After WordPress 7.0 GA lands, remove the temporary handle_err_admin_role() workaround β€” done; rewrite_role_error() and render_role_error_notice() removed in v3.4.0 after Trac #64690 confirmed in WP 7.0 GA.

What you can test in Playground

Feature Notes
Plugin activation & settings page βœ…
Gate fires on dangerous actions (plugin activate/delete, user delete, etc.) βœ…
Challenge / reauthentication page βœ…
Password verification & session cookie βœ…
Admin bar countdown timer βœ…
Request stash & replay after auth βœ…
Rate limiting / 5-attempt lockout βœ… within session
Session expiry by time βœ… wait out the configured duration (1–15 min)
Two Factor plugin (TOTP) βœ… installed automatically via blueprint
unfiltered_html removed from Editor role βœ…
Block-editor reauthentication UI βœ… use blueprint-editor-reauth.json; in the editor, open the inserter, search a Block Directory block, and install/activate it to trigger the password modal

Editor-triggered sudo challenge scope

The block/site editor middleware handles any cookie-authenticated REST response with code: "sudo_required", but the default WordPress UI only exposes a small set of dangerous actions from inside editor screens β€” chiefly Block Directory install/activate (/wp/v2/plugins), which is the primary Playground demo path. Ordinary content/design saves (posts, pages, templates, global styles, navigation, media, widgets, fonts, reusable blocks) are deliberately not gated.

The canonical, per-route inventory of which editor actions can trip a sudo challenge lives in docs/ui-ux-testing-prompts.md Β§6c (the Editor-triggerable gated-action inventory). Keep it as the single source; if a future release adds a first-party visible editor control for another gated REST route, update that inventory, this release checklist, and the Playground instructions together.

What won't work in Playground

Feature Why
WP-CLI / Cron entry point policies No CLI in browser
REST / XML-RPC entry point policies Network disabled in Playground
Two Factor email / magic-link providers PHP outbound network is off
WebAuthn bridge (security key gating) Browser WebAuthn API unavailable in Playground sandbox
Multisite behaviour in the standard public demos The default public blueprints are single-site; add a dedicated multisite blueprint/link when a release needs a network-admin demo
State after refreshing Playground Full reset on page reload

Transients and user meta persist across normal WP navigation within a session, but are wiped if you reload the Playground page itself. Use the integration test suite (composer test:integration) to verify transient TTL, real bcrypt, and multisite isolation β€” those require a real MySQL database and can't be covered by Playground.

To test the current main branch interactively without opening a PR, use blueprint.json directly: https://playground.wordpress.net/# + the URL-encoded contents of that file.

TDD Workflow

  1. Write a failing test first β€” commit or show it before writing production code
  2. Write the minimum production code to pass
  3. composer test:unit must pass before every commit
  4. composer analyse must pass before every commit (PHPStan + Psalm)

MU-Plugin (Local Dev)

The one-click installer on the Settings page copies the shim via file_put_contents. If it fails due to file permissions in your local environment, install manually:

  1. Copy mu-plugin/wp-sudo-gate.php from the plugin directory
  2. Paste it into wp-content/mu-plugins/wp-sudo-gate.php (create the directory if needed)
  3. The mu-plugin will be active on the next page load

Commit Conventions

Use conventional commit format. Run tests and static analysis before committing.

Manual Testing

The tests/MANUAL-TESTING.md checklist covers 20 test areas across all surfaces (admin UI, AJAX, REST, CLI, Cron, XML-RPC, WPGraphQL, multisite). Run through it before tagging a release. The file includes expected results, curl commands, and cleanup steps. For structured UI/UX-focused testing scenarios, see docs/ui-ux-testing-prompts.md.

Extending Gated Actions

Use the wp_sudo_gated_actions filter. See docs/developer-reference.md for the rule structure and examples.

Bridge Plugins

The bridges/ directory contains drop-in mu-plugins that gate third-party plugin actions:

Bridge Target Plugin What It Gates
wp-sudo-wp2fa-bridge.php WP 2FA (Melapress) Connects WP 2FA's TOTP/email/backup code methods to the sudo challenge page
wp-sudo-webauthn-bridge.php Two Factor Provider for WebAuthn Gates security key registration and deletion via AJAX

To create a new bridge:

  1. Copy an existing bridge as a template
  2. Add a class-existence guard so rules are only injected when the target plugin is active
  3. Add AJAX and/or REST rules via wp_sudo_gated_actions
  4. Add unit tests in tests/Unit/ and manual test steps in tests/MANUAL-TESTING.md
  5. Document in docs/developer-reference.md and docs/two-factor-integration.md (if 2FA-related)

See docs/developer-reference.md for the full worked example.

Audit Hooks

Nine action hooks are available for external logging. See docs/developer-reference.md.