Skip to content

Commit aee05d9

Browse files
committed
Tests: Cover recovery mode links.
Add tests for `WP_Recovery_Mode_Link_Service` covering URL generation and filtering, invalid and expired credentials, recovery cookie setup, redirects, and consumption of used keys. Developed in: #13992 Props huzaifaalmesbah. See #65819. git-svn-id: https://develop.svn.wordpress.org/trunk@64114 602fd350-edb4-49c9-b593-d223f7449a82
1 parent 097d794 commit aee05d9

1 file changed

Lines changed: 360 additions & 0 deletions

File tree

Lines changed: 360 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,360 @@
1+
<?php
2+
3+
/**
4+
* Test the WP_Recovery_Mode_Link_Service class.
5+
*
6+
* @group error-protection
7+
*
8+
* @covers WP_Recovery_Mode_Link_Service
9+
*/
10+
class Tests_Error_Protection_wpRecoveryModeLinkService extends WP_UnitTestCase {
11+
12+
/**
13+
* Original $pagenow value.
14+
*
15+
* @var string|null
16+
*/
17+
private $orig_pagenow;
18+
19+
/**
20+
* Original $_GET superglobal.
21+
*
22+
* @var array
23+
*/
24+
private $orig_get;
25+
26+
/**
27+
* Cookie service instance.
28+
*
29+
* @var WP_Recovery_Mode_Cookie_Service
30+
*/
31+
private $cookie_service;
32+
33+
/**
34+
* Key service instance.
35+
*
36+
* @var WP_Recovery_Mode_Key_Service
37+
*/
38+
private $key_service;
39+
40+
/**
41+
* Link service instance under test.
42+
*
43+
* @var WP_Recovery_Mode_Link_Service
44+
*/
45+
private $service;
46+
47+
/**
48+
* Sets up each test method.
49+
*/
50+
public function set_up() {
51+
parent::set_up();
52+
53+
$this->orig_pagenow = isset( $GLOBALS['pagenow'] ) ? $GLOBALS['pagenow'] : null;
54+
$this->orig_get = $_GET;
55+
$_GET = array();
56+
57+
$this->cookie_service = new WP_Recovery_Mode_Cookie_Service();
58+
$this->key_service = new WP_Recovery_Mode_Key_Service();
59+
$this->service = new WP_Recovery_Mode_Link_Service( $this->cookie_service, $this->key_service );
60+
}
61+
62+
/**
63+
* Tears down each test method.
64+
*/
65+
public function tear_down() {
66+
if ( null !== $this->orig_pagenow ) {
67+
$GLOBALS['pagenow'] = $this->orig_pagenow;
68+
} else {
69+
unset( $GLOBALS['pagenow'] );
70+
}
71+
72+
$_GET = $this->orig_get;
73+
delete_option( 'recovery_keys' );
74+
75+
parent::tear_down();
76+
}
77+
78+
/**
79+
* Tests that generate_url creates a valid recovery mode URL with required query parameters.
80+
*
81+
* @ticket 46130
82+
*
83+
* @covers WP_Recovery_Mode_Link_Service::generate_url
84+
*/
85+
public function test_generate_url() {
86+
$url = $this->service->generate_url();
87+
88+
$this->assertStringStartsWith( wp_login_url(), $url );
89+
90+
$query = wp_parse_url( $url, PHP_URL_QUERY );
91+
$this->assertNotEmpty( $query );
92+
93+
wp_parse_str( $query, $query_args );
94+
95+
$this->assertArrayHasKey( 'action', $query_args );
96+
$this->assertSame( WP_Recovery_Mode_Link_Service::LOGIN_ACTION_ENTER, $query_args['action'] );
97+
98+
$this->assertArrayHasKey( 'rm_token', $query_args );
99+
$this->assertNotEmpty( $query_args['rm_token'] );
100+
101+
$this->assertArrayHasKey( 'rm_key', $query_args );
102+
$this->assertNotEmpty( $query_args['rm_key'] );
103+
104+
// Verify the generated key is stored and can be validated.
105+
$validated = $this->key_service->validate_recovery_mode_key( $query_args['rm_token'], $query_args['rm_key'], HOUR_IN_SECONDS );
106+
$this->assertTrue( $validated );
107+
}
108+
109+
/**
110+
* Tests that the recovery_mode_begin_url filter can modify the generated recovery mode URL.
111+
*
112+
* @ticket 46130
113+
*
114+
* @covers WP_Recovery_Mode_Link_Service::generate_url
115+
*/
116+
public function test_generate_url_filter() {
117+
$filter_captured = array();
118+
119+
$filter_callback = function ( $url, $token, $key ) use ( &$filter_captured ) {
120+
$filter_captured = array(
121+
'url' => $url,
122+
'token' => $token,
123+
'key' => $key,
124+
);
125+
126+
return add_query_arg( 'custom_param', 'wp_test', $url );
127+
};
128+
129+
add_filter( 'recovery_mode_begin_url', $filter_callback, 10, 3 );
130+
$url = $this->service->generate_url();
131+
remove_filter( 'recovery_mode_begin_url', $filter_callback, 10 );
132+
133+
$this->assertStringContainsString( 'custom_param=wp_test', $url );
134+
$this->assertNotEmpty( $filter_captured['token'] );
135+
$this->assertNotEmpty( $filter_captured['key'] );
136+
$this->assertStringContainsString( 'action=' . WP_Recovery_Mode_Link_Service::LOGIN_ACTION_ENTER, $filter_captured['url'] );
137+
}
138+
139+
/**
140+
* Tests that handle_begin_link bails early if $GLOBALS['pagenow'] is not wp-login.php.
141+
*
142+
* @ticket 46130
143+
*
144+
* @covers WP_Recovery_Mode_Link_Service::handle_begin_link
145+
*/
146+
public function test_handle_begin_link_bails_if_not_login_page() {
147+
$GLOBALS['pagenow'] = 'index.php';
148+
$_GET = array(
149+
'action' => WP_Recovery_Mode_Link_Service::LOGIN_ACTION_ENTER,
150+
'rm_token' => 'token123',
151+
'rm_key' => 'key123',
152+
);
153+
154+
$result = $this->service->handle_begin_link( HOUR_IN_SECONDS );
155+
$this->assertNull( $result );
156+
}
157+
158+
/**
159+
* Tests that handle_begin_link bails early if $GLOBALS['pagenow'] is not set.
160+
*
161+
* @ticket 46130
162+
*
163+
* @covers WP_Recovery_Mode_Link_Service::handle_begin_link
164+
*/
165+
public function test_handle_begin_link_bails_if_pagenow_not_set() {
166+
unset( $GLOBALS['pagenow'] );
167+
$_GET = array(
168+
'action' => WP_Recovery_Mode_Link_Service::LOGIN_ACTION_ENTER,
169+
'rm_token' => 'token123',
170+
'rm_key' => 'key123',
171+
);
172+
173+
$result = $this->service->handle_begin_link( HOUR_IN_SECONDS );
174+
$this->assertNull( $result );
175+
}
176+
177+
/**
178+
* Tests that handle_begin_link bails early when action parameter is missing.
179+
*
180+
* @ticket 46130
181+
*
182+
* @covers WP_Recovery_Mode_Link_Service::handle_begin_link
183+
*/
184+
public function test_handle_begin_link_bails_if_action_missing() {
185+
$GLOBALS['pagenow'] = 'wp-login.php';
186+
$_GET = array(
187+
'rm_token' => 'token123',
188+
'rm_key' => 'key123',
189+
);
190+
191+
$result = $this->service->handle_begin_link( HOUR_IN_SECONDS );
192+
$this->assertNull( $result );
193+
}
194+
195+
/**
196+
* Tests that handle_begin_link bails early when action parameter is not LOGIN_ACTION_ENTER.
197+
*
198+
* @ticket 46130
199+
*
200+
* @covers WP_Recovery_Mode_Link_Service::handle_begin_link
201+
*/
202+
public function test_handle_begin_link_bails_if_action_invalid() {
203+
$GLOBALS['pagenow'] = 'wp-login.php';
204+
$_GET = array(
205+
'action' => 'login',
206+
'rm_token' => 'token123',
207+
'rm_key' => 'key123',
208+
);
209+
210+
$result = $this->service->handle_begin_link( HOUR_IN_SECONDS );
211+
$this->assertNull( $result );
212+
}
213+
214+
/**
215+
* Tests that handle_begin_link bails early when rm_token parameter is missing.
216+
*
217+
* @ticket 46130
218+
*
219+
* @covers WP_Recovery_Mode_Link_Service::handle_begin_link
220+
*/
221+
public function test_handle_begin_link_bails_if_rm_token_missing() {
222+
$GLOBALS['pagenow'] = 'wp-login.php';
223+
$_GET = array(
224+
'action' => WP_Recovery_Mode_Link_Service::LOGIN_ACTION_ENTER,
225+
'rm_key' => 'key123',
226+
);
227+
228+
$result = $this->service->handle_begin_link( HOUR_IN_SECONDS );
229+
$this->assertNull( $result );
230+
}
231+
232+
/**
233+
* Tests that handle_begin_link bails early when rm_key parameter is missing.
234+
*
235+
* @ticket 46130
236+
*
237+
* @covers WP_Recovery_Mode_Link_Service::handle_begin_link
238+
*/
239+
public function test_handle_begin_link_bails_if_rm_key_missing() {
240+
$GLOBALS['pagenow'] = 'wp-login.php';
241+
$_GET = array(
242+
'action' => WP_Recovery_Mode_Link_Service::LOGIN_ACTION_ENTER,
243+
'rm_token' => 'token123',
244+
);
245+
246+
$result = $this->service->handle_begin_link( HOUR_IN_SECONDS );
247+
$this->assertNull( $result );
248+
}
249+
250+
/**
251+
* Tests that handle_begin_link dies when validation returns WP_Error due to missing token.
252+
*
253+
* @ticket 46130
254+
*
255+
* @covers WP_Recovery_Mode_Link_Service::handle_begin_link
256+
*/
257+
public function test_handle_begin_link_dies_on_unrecognized_token() {
258+
$GLOBALS['pagenow'] = 'wp-login.php';
259+
$_GET = array(
260+
'action' => WP_Recovery_Mode_Link_Service::LOGIN_ACTION_ENTER,
261+
'rm_token' => 'nonexistent_token',
262+
'rm_key' => 'some_key',
263+
);
264+
265+
$this->expectException( 'WPDieException' );
266+
$this->service->handle_begin_link( HOUR_IN_SECONDS );
267+
}
268+
269+
/**
270+
* Tests that handle_begin_link dies when validation returns WP_Error due to expired key.
271+
*
272+
* @ticket 46130
273+
*
274+
* @covers WP_Recovery_Mode_Link_Service::handle_begin_link
275+
*/
276+
public function test_handle_begin_link_dies_on_expired_key() {
277+
$token = $this->key_service->generate_recovery_mode_token();
278+
$key = $this->key_service->generate_and_store_recovery_mode_key( $token );
279+
280+
// Manually update stored key timestamp to simulate expiration.
281+
$records = get_option( 'recovery_keys', array() );
282+
$records[ $token ]['created_at'] = time() - HOUR_IN_SECONDS - 10;
283+
update_option( 'recovery_keys', $records );
284+
285+
$GLOBALS['pagenow'] = 'wp-login.php';
286+
$_GET = array(
287+
'action' => WP_Recovery_Mode_Link_Service::LOGIN_ACTION_ENTER,
288+
'rm_token' => $token,
289+
'rm_key' => $key,
290+
);
291+
292+
$this->expectException( 'WPDieException' );
293+
$this->service->handle_begin_link( HOUR_IN_SECONDS );
294+
}
295+
296+
/**
297+
* Tests that handle_begin_link successfully sets the cookie and redirects when key is valid.
298+
*
299+
* @ticket 46130
300+
*
301+
* @covers WP_Recovery_Mode_Link_Service::handle_begin_link
302+
*/
303+
public function test_handle_begin_link_success() {
304+
$token = $this->key_service->generate_recovery_mode_token();
305+
$key = $this->key_service->generate_and_store_recovery_mode_key( $token );
306+
307+
$GLOBALS['pagenow'] = 'wp-login.php';
308+
$_GET = array(
309+
'action' => WP_Recovery_Mode_Link_Service::LOGIN_ACTION_ENTER,
310+
'rm_token' => $token,
311+
'rm_key' => $key,
312+
);
313+
314+
$cookie_length_filtered = false;
315+
$cookie_length_callback = function ( $length ) use ( &$cookie_length_filtered ) {
316+
$cookie_length_filtered = true;
317+
return $length;
318+
};
319+
add_filter( 'recovery_mode_cookie_length', $cookie_length_callback );
320+
321+
$redirect_location = '';
322+
$redirect_callback = function ( $location ) use ( &$redirect_location ) {
323+
$redirect_location = $location;
324+
// Throw an exception to halt execution before die; is reached.
325+
throw new Exception( 'redirect_intercepted' );
326+
};
327+
add_filter( 'wp_redirect', $redirect_callback );
328+
329+
set_error_handler(
330+
static function ( $errno, $errstr ) {
331+
if ( false !== strpos( $errstr, 'Cannot modify header information' ) ) {
332+
return true;
333+
}
334+
return false;
335+
}
336+
);
337+
338+
try {
339+
$this->service->handle_begin_link( HOUR_IN_SECONDS );
340+
$this->fail( 'Expected redirect exception was not thrown.' );
341+
} catch ( Exception $e ) {
342+
$this->assertSame( 'redirect_intercepted', $e->getMessage() );
343+
} finally {
344+
restore_error_handler();
345+
remove_filter( 'recovery_mode_cookie_length', $cookie_length_callback );
346+
remove_filter( 'wp_redirect', $redirect_callback );
347+
}
348+
349+
// Verify cookie service attempted to set cookie.
350+
$this->assertTrue( $cookie_length_filtered, 'Expected recovery_mode_cookie_length filter to have fired during set_cookie.' );
351+
352+
// Verify redirect destination.
353+
$expected_url = add_query_arg( 'action', WP_Recovery_Mode_Link_Service::LOGIN_ACTION_ENTERED, wp_login_url() );
354+
$this->assertSame( $expected_url, $redirect_location );
355+
356+
// Verify key was consumed and removed.
357+
$records = get_option( 'recovery_keys', array() );
358+
$this->assertArrayNotHasKey( $token, $records );
359+
}
360+
}

0 commit comments

Comments
 (0)