Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
651 changes: 651 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)
- REST API permission callbacks (`rest_api_can_edit_user_and_update_two_factor_options`)
Expand Down Expand Up @@ -154,7 +155,25 @@ 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()`).
18 changes: 17 additions & 1 deletion class-two-factor-core.php
Original file line number Diff line number Diff line change
Expand Up @@ -1186,7 +1186,7 @@ public static function login_html( $user, $login_nonce, $redirect_to, $error_msg

foreach ( $backup_providers as $backup_provider_key => $backup_provider ) {
$backup_link_args['provider'] = $backup_provider_key;
$links[] = array(
$links[] = array(
'url' => self::login_url( $backup_link_args ),
'label' => $backup_provider->get_alternative_provider_label(),
);
Expand Down Expand Up @@ -1474,6 +1474,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": "^2.12",
"phpcompatibility/phpcompatibility-wp": "3.0.0-alpha2",
"phpunit/phpunit": "^8.5|^9.6",
"spatie/phpunit-watcher": "^1.23",
Expand Down
60 changes: 52 additions & 8 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 @@ -3,8 +3,11 @@ includes:
parameters:
level: 0
paths:
- CLI
- includes
- providers
- class-two-factor-compat.php
- class-two-factor-core.php
- two-factor.php
scanFiles:
- vendor/php-stubs/wp-cli-stubs/wp-cli-stubs.php
24 changes: 24 additions & 0 deletions tests/class-two-factor-core.php
Original file line number Diff line number Diff line change
Expand Up @@ -772,6 +772,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