The test suite uses PHPUnit and runs inside the Docker-based @wordpress/env environment against a live WordPress install. The npm run composer script is a wrapper that executes composer inside the tests-cli container at the plugin path.
# Full test suite: every test:* script, single site then multisite
npm test
# One suite at a time
npm run test:single
npm run test:multisite
# Watch mode (re-runs on file changes, no coverage)
npm run test-watch:single
npm run test-watch:multisite
# Full test suite with coverage (requires xdebug-enabled env)
npm run env start -- --xdebug=coverage
npm testEach suite writes its own coverage reports, so neither overwrites the other: single site to tests/logs/clover.xml and tests/logs/html/, multisite to tests/logs/clover-multisite.xml and tests/logs/html-multisite/. Open the index.html inside either HTML directory to view that suite's report.
The coverage whitelist is duplicated between phpunit.xml.dist and phpunit-multisite.xml because PHPUnit has no mechanism for sharing that block; keep the two in sync when adding source directories. CI uploads both Clover reports to Codecov, which merges them — the multisite suite is the only one that reaches the is_multisite() branches.
npm test is npm-run-all test:*, so it runs test:single and then test:multisite (phpunit-multisite.xml) in sequence. To run just one:
npm run test:single
npm run test:multisiteThe watch scripts are deliberately named test-watch:* rather than test:watch-* so the test:* glob does not match them — otherwise npm test would launch a watcher and never exit. A single watcher process cannot drive both configs, so there is one script per suite:
npm run test-watch:single
npm run test-watch:multisiteRun the suites sequentially, not concurrently. Two simultaneous wp-env run invocations race on wp-env's own initialization and can leave the environment needing npm run env start again. To overlap them, start both PHPUnit processes inside a single container invocation instead.
Both suites share the one tests-wordpress database, and multisite needs extra columns on wp_users (spam, deleted) that single site does not have. The multisite config therefore isolates its tables behind a separate prefix via WORDPRESS_TABLE_PREFIX (see phpunit-multisite.xml), so the two schemas can coexist and the suites can run in either order. Without that, whichever suite ran second would fail against the other's schema.
Tests that depend on editing another user must account for edit_users being reserved for network super admins on multisite; the fixtures call grant_super_admin() under is_multisite() for this reason.
Pass PHPUnit arguments through the composer wrapper:
# Run a single test class
npm run composer -- test -- --filter Test_ClassTwoFactorCore
# Run a single test method
npm run composer -- test -- --filter test_create_login_nonce
# Run by @group annotation
npm run composer -- test -- --group totp
npm run composer -- test -- --group email
npm run composer -- test -- --group backup-codes
npm run composer -- test -- --group providers
npm run composer -- test -- --group core
npm run composer -- test -- --group cli
# Run a single file
npm run composer -- test -- tests/providers/class-two-factor-totp.phpClass: Tests_Two_Factor
Smoke tests that the plugin loaded correctly: the TWO_FACTOR_DIR constant is defined and the core classes exist.
Class: Test_ClassTwoFactorCore · Group: core
The largest test file. Covers the full authentication lifecycle managed by Two_Factor_Core:
- Hook registration (
add_hooks) - Provider registration and retrieval (
get_providers,get_enabled_providers_for_user,get_available_providers_for_user,get_primary_provider_for_user) - Login interception (
filter_authenticate,show_two_factor_login,process_provider) - Login nonce creation, verification, and deletion
- Rate limiting (
get_user_time_delay,is_user_rate_limited,clear_login_rate_limit) - Session management: two-factor factored vs. non-factored sessions, session destruction on 2FA enable/disable, revalidation
- Password reset flow (compromise detection, email notifications, reset notices and their nonce validation)
- REST API permission callbacks (
rest_api_can_edit_user_and_update_two_factor_options) - User settings actions (
trigger_user_settings_action,current_user_can_update_two_factor_options) - Profile settings UI output (
user_two_factor_options), including recovery-codes wording with and without the Backup Codes provider - Uninstall cleanup (user meta and site-wide options)
- Filter hooks (
two_factor_providers,two_factor_primary_provider_for_user,two_factor_user_api_login_enable)
Class: Tests_Two_Factor_Provider · Group: providers
Tests the abstract Two_Factor_Provider base class:
- Singleton pattern (
get_instance) - Code generation (
get_code) and request sanitization (sanitize_code_from_request) get_keyreturning the class nameis_supported_for_user(globally registered vs. not)- Default implementations of
get_alternative_provider_label,pre_process_authentication,uninstall_user_meta_keys,uninstall_options
Class: Tests_Two_Factor_Totp · Groups: providers, totp
Tests Two_Factor_Totp:
- Base32 encode/decode (including invalid input exception)
- QR code URL generation
- TOTP key storage and retrieval per user
- Auth code validation (current tick, spaces stripped, invalid chars rejected)
validate_code_for_userreplay protection- Algorithm variants: SHA1, SHA256, SHA512 (code generation and authentication)
- Secret padding (
pad_secret)
Class: Tests_Two_Factor_Totp_REST_API · Groups: providers, totp
Extends WP_Test_REST_TestCase. Tests the TOTP REST endpoints:
- Setting a TOTP key with a valid/invalid/missing auth code
- Updating an existing TOTP key
- Deleting own secret
- Admin deleting another user's secret
- Non-admin cannot delete another user's secret
Class: Tests_Two_Factor_Email · Groups: providers, email
Tests Two_Factor_Email:
- Token generation and validation (same user, different user, deleted token)
- Email delivery (
generate_and_email_token) - Authentication page rendering (no user, no token, existing token)
validate_authentication(valid, missing input, spaces stripped)- Token TTL and expiry
- Token generation time tracking
- Custom token length filter
pre_process_authentication(resend vs. no resend)- User options UI output
- Uninstall meta key cleanup
Class: Tests_Two_Factor_Backup_Codes · Groups: providers, backup-codes
Tests Two_Factor_Backup_Codes:
- Code generation and validation
- Replay prevention (code invalidated after use)
- Cross-user isolation (code invalid for different user)
is_available_for_user(no codes vs. codes generated)- User options UI output
- Code deletion
two_factor_backup_code_lengthfilter for customizing code length
Class: Tests_Two_Factor_Backup_Codes_REST_API · Groups: providers, backup-codes
Extends WP_Test_REST_TestCase. Tests the backup codes REST endpoints:
- Generate codes and validate the downloadable file contents
- User cannot generate codes for a different user
- Admin can generate codes for other users
Class: Tests_Two_Factor_Dummy · Groups: providers, dummy
Tests the Two_Factor_Dummy provider (always passes authentication — used as a test fixture):
get_instance,get_label,authentication_page,validate_authentication,is_available_for_user
Class: Tests_Two_Factor_Dummy_Secure · Groups: providers, dummy
Tests Two_Factor_Dummy_Secure (a fixture that always fails authentication, used to test the provider class name filter):
get_keyoverride returnsTwo_Factor_Dummy- Authentication page rendering
validate_authenticationalways returns falsetwo_factor_provider_classnamefilter
Class: Tests_Two_Factor_CLI_Command · Group: cli
Tests the Two_Factor_CLI_Command WP-CLI command class. The WP-CLI runtime is not loaded during PHPUnit, so the suite loads lightweight test doubles for WP_CLI, WP_CLI_Command, and the WP_CLI\Utils helpers (see Test Helpers) that capture output for assertions and throw on error()/confirm():
- User resolution by ID, login, and email; "user not found" errors
status— output for users with and without 2FA, backup-code count,--formatpassthroughlist-providers— registered providers listed,--formatpassthroughenable— enabling secret-free providers; session destruction on change; refusing TOTP (no stale "Phase 3" pointer) and backup codes; unknown provider and missing-argument errorsdisable(single provider) — removal leaves others intact, session destruction on change, idempotent no-op, confirmation required without--yesdisable(all) — full reset clears providers/throttle/nonce state and destroys sessions, preserves the compromised-password-reset flag, idempotent no-op, stale-meta cleanup guarding the fail-closed email fallback, confirmation required without--yesbackup-codes generate— default and--countcode counts, regeneration replaces the set, enables the provider so codes are usable at login, session destruction when first enabled, unknown-action and missing-argument errorsunlock— clears the login throttle for a rate-limited user; no-op message otherwise
tests/bootstrap.php— Locates the WordPress test library (viaWP_TESTS_DIRenv var, relative path, or/tmp/wordpress-tests-lib), loads the plugin viamuplugins_loaded, then boots the WP test environment.tests/class-two-factor-dummy-secure.php— DefinesTwo_Factor_Dummy_Secure, a test-only provider class that spoofs the key ofTwo_Factor_Dummybut always failsvalidate_authentication. Used byTests_Two_Factor_Dummy_Secureand some core tests.tests/cli/— WP-CLI test doubles loaded byTests_Two_Factor_CLI_Command:class-wp-cli-command.php(empty base-class stub),class-wp-cli.php(captures output intoWP_CLI::$logger, throws onerror()/confirm()),class-wp-cli-mock-exit-exception.php(stands in for a process exit), andwp-cli-utils.php(WP_CLI\Utils\get_flag_value()andformat_items()).Two_Factor_Redirect_Exception— Defined intests/class-two-factor-core.php. The core tests' render helper interceptswp_redirectand throws this exception, so code paths that redirect andexitcan be tested without terminating the test process.