Make WordPress Core

Changeset 64369


Ignore:
Timestamp:
10/15/2026 10:32:10 PM (29 hours ago)
Author:
lancewillett
Message:

Tests: Expand recovery mode cookie service coverage.

Cover request cookies, explicit cookie precedence, filtered cookie lifetimes, and session ID extraction in WP_Recovery_Mode_Cookie_Service. Verify fallback authentication key and salt storage and reuse when the default secret keys are configured.

Developed in: ​https://github.com/WordPress/wordpress-develop/pull/14164

Props huzaifaalmesbah.
See #65819.

File:
1 edited

Legend:

Unmodified
Added
Removed
  • trunk/tests/phpunit/tests/error-protection/wpRecoveryModeCookieService.php

    r60729 r64369  
    77
    88        /**
     9         * Whether the recovery mode cookie was set before the test.
     10         *
     11         * @var bool
     12         */
     13        private $had_cookie;
     14
     15        /**
     16         * Original value of the recovery mode cookie.
     17         *
     18         * @var mixed
     19         */
     20        private $orig_cookie;
     21
     22        public function set_up() {
     23                parent::set_up();
     24
     25                $this->had_cookie  = array_key_exists( RECOVERY_MODE_COOKIE, $_COOKIE );
     26                $this->orig_cookie = $this->had_cookie ? $_COOKIE[ RECOVERY_MODE_COOKIE ] : null;
     27
     28                unset( $_COOKIE[ RECOVERY_MODE_COOKIE ] );
     29        }
     30
     31        public function tear_down() {
     32                if ( $this->had_cookie ) {
     33                        $_COOKIE[ RECOVERY_MODE_COOKIE ] = $this->orig_cookie;
     34                } else {
     35                        unset( $_COOKIE[ RECOVERY_MODE_COOKIE ] );
     36                }
     37
     38                parent::tear_down();
     39        }
     40
     41        /**
     42         * Builds a signed recovery mode cookie value.
     43         *
     44         * @param int    $created_at Timestamp the cookie was created at.
     45         * @param string $random     Random part of the cookie, used to derive the session ID.
     46         * @return string Cookie value.
     47         */
     48        private function build_cookie( $created_at, $random ) {
     49                $service    = new WP_Recovery_Mode_Cookie_Service();
     50                $reflection = new ReflectionMethod( $service, 'recovery_mode_hash' );
     51                if ( PHP_VERSION_ID < 80100 ) {
     52                        $reflection->setAccessible( true );
     53                }
     54
     55                $to_sign = sprintf( 'recovery_mode|%s|%s', $created_at, $random );
     56
     57                return base64_encode( sprintf( '%s|%s', $to_sign, $reflection->invoke( $service, $to_sign ) ) );
     58        }
     59
     60        /**
    961         * @ticket 46130
    1062         *
    … …  
    105157                $this->assertTrue( $service->validate_cookie( $reflection->invoke( $service ) ) );
    106158        }
     159
     160        /**
     161         * @ticket 65819
     162         *
     163         * @covers WP_Recovery_Mode_Cookie_Service::is_cookie_set
     164         *
     165         * @dataProvider data_is_cookie_set
     166         *
     167         * @param mixed $cookie   Cookie value, or null to leave the cookie unset.
     168         * @param bool  $expected Whether the cookie counts as set.
     169         */
     170        public function test_is_cookie_set( $cookie, $expected ) {
     171                if ( null !== $cookie ) {
     172                        $_COOKIE[ RECOVERY_MODE_COOKIE ] = $cookie;
     173                }
     174
     175                $service = new WP_Recovery_Mode_Cookie_Service();
     176
     177                $this->assertSame( $expected, $service->is_cookie_set() );
     178        }
     179
     180        /**
     181         * Data provider.
     182         *
     183         * @return array[]
     184         */
     185        public static function data_is_cookie_set() {
     186                return array(
     187                        'cookie not set' => array( null, false ),
     188                        'empty string'   => array( '', false ),
     189                        'zero string'    => array( '0', false ),
     190                        'cookie value'   => array( 'cmVjb3ZlcnlfbW9kZQ==', true ),
     191                );
     192        }
     193
     194        /**
     195         * @ticket 65819
     196         *
     197         * @covers WP_Recovery_Mode_Cookie_Service::validate_cookie
     198         *
     199         * @dataProvider data_missing_cookies
     200         *
     201         * @param mixed $cookie Cookie value, or null to leave the cookie unset.
     202         */
     203        public function test_validate_cookie_returns_wp_error_if_no_cookie_is_present( $cookie ) {
     204                if ( null !== $cookie ) {
     205                        $_COOKIE[ RECOVERY_MODE_COOKIE ] = $cookie;
     206                }
     207
     208                $service = new WP_Recovery_Mode_Cookie_Service();
     209
     210                $error = $service->validate_cookie();
     211                $this->assertWPError( $error );
     212                $this->assertSame( 'no_cookie', $error->get_error_code() );
     213        }
     214
     215        /**
     216         * Data provider.
     217         *
     218         * @return array[]
     219         */
     220        public static function data_missing_cookies() {
     221                return array(
     222                        'cookie not set' => array( null ),
     223                        'empty string'   => array( '' ),
     224                        'zero string'    => array( '0' ),
     225                );
     226        }
     227
     228        /**
     229         * @ticket 65819
     230         *
     231         * @covers WP_Recovery_Mode_Cookie_Service::validate_cookie
     232         */
     233        public function test_validate_cookie_reads_cookie_from_request() {
     234                $_COOKIE[ RECOVERY_MODE_COOKIE ] = $this->build_cookie( time(), 'abcdefghij0123456789' );
     235
     236                $service = new WP_Recovery_Mode_Cookie_Service();
     237
     238                $this->assertTrue( $service->validate_cookie() );
     239        }
     240
     241        /**
     242         * @ticket 65819
     243         *
     244         * @covers WP_Recovery_Mode_Cookie_Service::validate_cookie
     245         */
     246        public function test_validate_cookie_prefers_cookie_argument_over_request() {
     247                $_COOKIE[ RECOVERY_MODE_COOKIE ] = $this->build_cookie( time(), 'abcdefghij0123456789' );
     248
     249                $service = new WP_Recovery_Mode_Cookie_Service();
     250
     251                $error = $service->validate_cookie( 'gibberish' );
     252                $this->assertWPError( $error );
     253                $this->assertSame( 'invalid_format', $error->get_error_code() );
     254        }
     255
     256        /**
     257         * @ticket 65819
     258         *
     259         * @covers WP_Recovery_Mode_Cookie_Service::validate_cookie
     260         *
     261         * @dataProvider data_filtered_cookie_lengths
     262         *
     263         * @param int         $age           Age of the cookie in seconds.
     264         * @param int         $cookie_length Filtered cookie length in seconds.
     265         * @param string|true $expected      Expected error code, or true if the cookie is valid.
     266         */
     267        public function test_validate_cookie_uses_filtered_cookie_length( $age, $cookie_length, $expected ) {
     268                $filter = new MockAction();
     269                add_filter( 'recovery_mode_cookie_length', array( $filter, 'filter' ) );
     270                add_filter(
     271                        'recovery_mode_cookie_length',
     272                        static function () use ( $cookie_length ) {
     273                                return $cookie_length;
     274                        },
     275                        20
     276                );
     277
     278                $service = new WP_Recovery_Mode_Cookie_Service();
     279                $result  = $service->validate_cookie( $this->build_cookie( time() - $age, 'abcdefghij0123456789' ) );
     280
     281                if ( true === $expected ) {
     282                        $this->assertTrue( $result );
     283                } else {
     284                        $this->assertWPError( $result );
     285                        $this->assertSame( $expected, $result->get_error_code() );
     286                }
     287
     288                $this->assertSame( array( array( 604800 ) ), $filter->get_args(), 'The filter should receive the default length of one week.' );
     289        }
     290
     291        /**
     292         * Data provider.
     293         *
     294         * @return array[]
     295         */
     296        public static function data_filtered_cookie_lengths() {
     297                return array(
     298                        'shorter length expires a recent cookie' => array( 2 * HOUR_IN_SECONDS, HOUR_IN_SECONDS, 'expired' ),
     299                        'zero length expires any older cookie'   => array( MINUTE_IN_SECONDS, 0, 'expired' ),
     300                        'longer length keeps an old cookie'      => array( 2 * WEEK_IN_SECONDS, 3 * WEEK_IN_SECONDS, true ),
     301                );
     302        }
     303
     304        /**
     305         * @ticket 65819
     306         *
     307         * @covers WP_Recovery_Mode_Cookie_Service::get_session_id_from_cookie
     308         */
     309        public function test_get_session_id_from_cookie_returns_hash_of_random_part() {
     310                $service = new WP_Recovery_Mode_Cookie_Service();
     311                $cookie  = base64_encode( 'recovery_mode|1234567890|abcdefghij0123456789|signature' );
     312
     313                $this->assertSame( '6256c24ff916241741c75edc27548d4ba4124a29', $service->get_session_id_from_cookie( $cookie ) );
     314        }
     315
     316        /**
     317         * @ticket 65819
     318         *
     319         * @covers WP_Recovery_Mode_Cookie_Service::get_session_id_from_cookie
     320         */
     321        public function test_get_session_id_from_cookie_reads_cookie_from_request() {
     322                $_COOKIE[ RECOVERY_MODE_COOKIE ] = base64_encode( 'recovery_mode|1234567890|abcdefghij0123456789|signature' );
     323
     324                $service = new WP_Recovery_Mode_Cookie_Service();
     325
     326                $this->assertSame( '6256c24ff916241741c75edc27548d4ba4124a29', $service->get_session_id_from_cookie() );
     327        }
     328
     329        /**
     330         * @ticket 65819
     331         *
     332         * @covers WP_Recovery_Mode_Cookie_Service::get_session_id_from_cookie
     333         *
     334         * @dataProvider data_missing_cookies
     335         *
     336         * @param mixed $cookie Cookie value, or null to leave the cookie unset.
     337         */
     338        public function test_get_session_id_from_cookie_returns_wp_error_if_no_cookie_is_present( $cookie ) {
     339                if ( null !== $cookie ) {
     340                        $_COOKIE[ RECOVERY_MODE_COOKIE ] = $cookie;
     341                }
     342
     343                $service = new WP_Recovery_Mode_Cookie_Service();
     344
     345                $error = $service->get_session_id_from_cookie();
     346                $this->assertWPError( $error );
     347                $this->assertSame( 'no_cookie', $error->get_error_code() );
     348        }
     349
     350        /**
     351         * @ticket 65819
     352         *
     353         * @covers WP_Recovery_Mode_Cookie_Service::get_session_id_from_cookie
     354         *
     355         * @dataProvider data_cookies_with_invalid_format
     356         *
     357         * @param string $cookie Cookie value.
     358         */
     359        public function test_get_session_id_from_cookie_returns_wp_error_if_invalid_format( $cookie ) {
     360                $service = new WP_Recovery_Mode_Cookie_Service();
     361
     362                $error = $service->get_session_id_from_cookie( $cookie );
     363                $this->assertWPError( $error );
     364                $this->assertSame( 'invalid_format', $error->get_error_code() );
     365        }
     366
     367        /**
     368         * Data provider.
     369         *
     370         * @return array[]
     371         */
     372        public static function data_cookies_with_invalid_format() {
     373                return array(
     374                        'not base64 encoded' => array( 'gibberish' ),
     375                        'too few parts'      => array( base64_encode( 'recovery_mode|1234567890|abcdefghij0123456789' ) ),
     376                        'too many parts'     => array( base64_encode( 'recovery_mode|1234567890|abcdefghij0123456789|signature|extra' ) ),
     377                );
     378        }
     379
     380        /**
     381         * @ticket 65819
     382         *
     383         * @covers WP_Recovery_Mode_Cookie_Service::recovery_mode_hash
     384         */
     385        public function test_recovery_mode_hash_uses_stored_key_and_salt_with_default_secret_keys() {
     386                if ( 'put your unique phrase here' !== AUTH_KEY || 'put your unique phrase here' !== AUTH_SALT ) {
     387                        $this->markTestSkipped( 'This test requires the default AUTH_KEY and AUTH_SALT values.' );
     388                }
     389
     390                update_site_option( 'recovery_mode_auth_key', 'test-auth-key' );
     391                update_site_option( 'recovery_mode_auth_salt', 'test-auth-salt' );
     392
     393                $service    = new WP_Recovery_Mode_Cookie_Service();
     394                $reflection = new ReflectionMethod( $service, 'recovery_mode_hash' );
     395                if ( PHP_VERSION_ID < 80100 ) {
     396                        $reflection->setAccessible( true );
     397                }
     398
     399                $this->assertSame(
     400                        '3b83c8f3e016b4ed284de9e6300874a516e896a3',
     401                        $reflection->invoke( $service, 'recovery_mode|1234567890|abcdefghij0123456789' )
     402                );
     403        }
     404
     405        /**
     406         * @ticket 65819
     407         *
     408         * @covers WP_Recovery_Mode_Cookie_Service::recovery_mode_hash
     409         */
     410        public function test_recovery_mode_hash_generates_and_reuses_key_and_salt_with_default_secret_keys() {
     411                if ( 'put your unique phrase here' !== AUTH_KEY || 'put your unique phrase here' !== AUTH_SALT ) {
     412                        $this->markTestSkipped( 'This test requires the default AUTH_KEY and AUTH_SALT values.' );
     413                }
     414
     415                delete_site_option( 'recovery_mode_auth_key' );
     416                delete_site_option( 'recovery_mode_auth_salt' );
     417
     418                $service    = new WP_Recovery_Mode_Cookie_Service();
     419                $reflection = new ReflectionMethod( $service, 'recovery_mode_hash' );
     420                if ( PHP_VERSION_ID < 80100 ) {
     421                        $reflection->setAccessible( true );
     422                }
     423
     424                $hash = $reflection->invoke( $service, 'recovery_mode|1234567890|abcdefghij0123456789' );
     425                $key  = get_site_option( 'recovery_mode_auth_key' );
     426                $salt = get_site_option( 'recovery_mode_auth_salt' );
     427
     428                $this->assertIsString( $key, 'A key should be generated and stored.' );
     429                $this->assertSame( 64, strlen( $key ), 'The stored key should be 64 characters long.' );
     430                $this->assertIsString( $salt, 'A salt should be generated and stored.' );
     431                $this->assertSame( 64, strlen( $salt ), 'The stored salt should be 64 characters long.' );
     432                $this->assertNotSame( $key, $salt, 'The key and salt should differ.' );
     433
     434                $this->assertSame( $hash, $reflection->invoke( $service, 'recovery_mode|1234567890|abcdefghij0123456789' ), 'The same data should produce the same hash.' );
     435                $this->assertSame( $key, get_site_option( 'recovery_mode_auth_key' ), 'The stored key should be reused.' );
     436                $this->assertSame( $salt, get_site_option( 'recovery_mode_auth_salt' ), 'The stored salt should be reused.' );
     437        }
    107438}
Note: See TracChangeset for help on using the changeset viewer.