Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
66c6107
add wpcli
masteradhoc Jun 14, 2026
99477dd
add new function to clear login rate limit for cli usage
masteradhoc Jun 14, 2026
13b1bf6
add CLI features
masteradhoc Jun 14, 2026
70de7f9
uppercase folder
masteradhoc Jun 14, 2026
ab2a154
add CLI folder to phpstan
masteradhoc Jun 14, 2026
61e10ba
add wp cli stubs, fix lint errors
masteradhoc Jun 14, 2026
6faf97b
update with sybre's feedback
masteradhoc Jun 18, 2026
a2f4521
Add WP-CLI command tests and address review feedback
nimesh-xecurify Jul 10, 2026
9bd65e3
Assert confirmation prompt is shown in disable gate tests
nimesh-xecurify Jul 10, 2026
faefcb1
Merge branch 'WordPress:master' into 233-add-wpcli-foundation
masteradhoc Jul 12, 2026
a3fc583
Merge pull request #1 from nimesh-xecurify/wp-cli-tests-work
masteradhoc Jul 12, 2026
6f78be7
add since tags to docblock
masteradhoc Jul 12, 2026
e6dab20
add test cases and count guard
masteradhoc Jul 14, 2026
daf78a2
add count and regenerating backup logic
masteradhoc Jul 23, 2026
66cb25d
Merge branch 'master' into 233-add-wpcli-foundation
masteradhoc Jul 23, 2026
fe02f79
Merge branch 'master' into 233-add-wpcli-foundation
masteradhoc Jul 31, 2026
005bc32
multi-item array on a new line
masteradhoc Jul 31, 2026
9463ce6
fix indent issues
masteradhoc Jul 31, 2026
7f13b1b
fix indent #2
masteradhoc Jul 31, 2026
d66e26e
Merge branch 'master' into 233-add-wpcli-foundation
masteradhoc Aug 19, 2026
a745df2
fix tests
masteradhoc Aug 19, 2026
f890923
Merge remote-tracking branch 'upstream/master' into 233-add-wpcli-fou…
masteradhoc Sep 9, 2026
cd493a2
move wp-cli-stubs to dev-master so compatibility is there
masteradhoc Sep 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
712 changes: 712 additions & 0 deletions CLI/class-two-factor-cli-command.php

Large diffs are not rendered by default.

21 changes: 20 additions & 1 deletion TESTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ 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.php
Expand All @@ -56,7 +57,7 @@ The largest test file. Covers the full authentication lifecycle managed by `Two_
- 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`)
- 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`)
Expand Down Expand Up @@ -155,8 +156,26 @@ Tests `Two_Factor_Dummy_Secure` (a fixture that always _fails_ authentication, u
- `validate_authentication` always returns false
- `two_factor_provider_classname` filter

### WP-CLI Commands — `tests/cli/class-two-factor-cli-command.php`

**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, `--format` passthrough
- `list-providers` — registered providers listed, `--format` passthrough
- `enable` — enabling secret-free providers; session destruction on change; refusing TOTP (no stale "Phase 3" pointer) and backup codes; unknown provider and missing-argument errors
- `disable` (single provider) — removal leaves others intact, session destruction on change, idempotent no-op, confirmation required without `--yes`
- `disable` (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 `--yes`
- `backup-codes generate` — default and `--count` code counts, regeneration replaces the set, enables the provider so codes are usable at login, session destruction when first enabled, unknown-action and missing-argument errors
- `unlock` — clears the login throttle for a rate-limited user; no-op message otherwise

## Test Helpers

- **`tests/bootstrap.php`** — Locates the WordPress test library (via `WP_TESTS_DIR` env var, relative path, or `/tmp/wordpress-tests-lib`), loads the plugin via `muplugins_loaded`, then boots the WP test environment.
- **`tests/class-two-factor-dummy-secure.php`** — Defines `Two_Factor_Dummy_Secure`, a test-only provider class that spoofs the key of `Two_Factor_Dummy` but always fails `validate_authentication`. Used by `Tests_Two_Factor_Dummy_Secure` and some core tests.
- **`tests/cli/`** — WP-CLI test doubles loaded by `Tests_Two_Factor_CLI_Command`: `class-wp-cli-command.php` (empty base-class stub), `class-wp-cli.php` (captures output into `WP_CLI::$logger`, throws on `error()`/`confirm()`), `class-wp-cli-mock-exit-exception.php` (stands in for a process exit), and `wp-cli-utils.php` (`WP_CLI\Utils\get_flag_value()` and `format_items()`).
- **`Two_Factor_Redirect_Exception`** — Defined in `tests/class-two-factor-core.php`. The core tests' render helper intercepts `wp_redirect` and throws this exception, so code paths that redirect and `exit` can be tested without terminating the test process.
16 changes: 16 additions & 0 deletions class-two-factor-core.php
Original file line number Diff line number Diff line change
Expand Up @@ -1473,6 +1473,22 @@ public static function is_user_rate_limited( $user ) {
return apply_filters( 'two_factor_is_user_rate_limited', $rate_limited, $user );
}

/**
* Clear the login rate-limit and failed-attempt counter for a user.
*
* Used by the WP-CLI `unlock` and `disable` (all) commands so there is one
* tested code path for clearing throttle state rather than deleting the meta
* keys directly from each call site.
*
* @since 0.17.0
*
* @param WP_User $user The user whose throttle state should be cleared.
*/
public static function clear_login_rate_limit( $user ) {
delete_user_meta( $user->ID, self::USER_RATE_LIMIT_KEY );
delete_user_meta( $user->ID, self::USER_FAILED_LOGIN_ATTEMPTS_KEY );
}

/**
* Determine if the current user session is logged in with 2FA.
*
Expand Down
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"require-dev": {
"automattic/vipwpcs": "^3.0",
"dealerdirect/phpcodesniffer-composer-installer": "^1.0",
"php-stubs/wp-cli-stubs": "dev-master",
"phpcompatibility/phpcompatibility-wp": "3.0.0-alpha2",
"phpunit/phpunit": "^8.5|^9.6",
"spatie/phpunit-watcher": "^1.23",
Expand Down
51 changes: 49 additions & 2 deletions composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions phpstan.dist.neon
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@ parameters:
# Runtime values in WordPress are not guaranteed to match the documented types.
treatPhpDocTypesAsCertain: false
paths:
- CLI
- providers
- settings
- class-two-factor-compat.php
- class-two-factor-core.php
- two-factor.php
scanFiles:
- vendor/php-stubs/wp-cli-stubs/wp-cli-stubs.php
2 changes: 1 addition & 1 deletion providers/class-two-factor-totp.php
Original file line number Diff line number Diff line change
Expand Up @@ -469,7 +469,7 @@ public function get_user_totp_key( $user_id ) {
* @param int $user_id User ID.
* @param string $key TOTP secret key.
*
* @return boolean If the key was stored successfully.
* @return int|bool Meta ID if the key did not exist, true on update, false on failure.
*/
public function set_user_totp_key( $user_id, $key ) {
return update_user_meta( $user_id, self::SECRET_META_KEY, $key );
Expand Down
24 changes: 24 additions & 0 deletions tests/class-two-factor-core.php
Original file line number Diff line number Diff line change
Expand Up @@ -781,6 +781,30 @@ public function test_is_user_rate_limited() {
$this->assertFalse( Two_Factor_Core::is_user_rate_limited( $user ) );
}

/**
* Test that clearing the login rate limit removes the throttle state.
*
* @covers Two_Factor_Core::clear_login_rate_limit
*/
public function test_clear_login_rate_limit() {
$user = $this->get_dummy_user();

// Put the user into a rate-limited state.
update_user_meta( $user->ID, Two_Factor_Core::USER_FAILED_LOGIN_ATTEMPTS_KEY, 5 );
update_user_meta( $user->ID, Two_Factor_Core::USER_RATE_LIMIT_KEY, time() );
$this->assertTrue( Two_Factor_Core::is_user_rate_limited( $user ) );

Two_Factor_Core::clear_login_rate_limit( $user );

$this->assertFalse( Two_Factor_Core::is_user_rate_limited( $user ) );
$this->assertEmpty( get_user_meta( $user->ID, Two_Factor_Core::USER_RATE_LIMIT_KEY, true ) );
$this->assertEmpty( get_user_meta( $user->ID, Two_Factor_Core::USER_FAILED_LOGIN_ATTEMPTS_KEY, true ) );

// Clearing an already-clean user is a harmless no-op.
Two_Factor_Core::clear_login_rate_limit( $user );
$this->assertFalse( Two_Factor_Core::is_user_rate_limited( $user ) );
}

/**
* Test that the "invalid login attempts have occurred" login notice works as expected.
*
Expand Down
Loading
Loading