Completed
Push — renovate/wordpress-monorepo ( 8cdadc...6203b6 )
by
unknown
552:54 queued 542:45
created

mock_jetpack_userless_options()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
nc 3
nop 2
dl 0
loc 10
rs 9.9332
c 0
b 0
f 0
1
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
2
3
namespace Automattic\Jetpack\Connection;
4
5
use Automattic\Jetpack\Connection\Plugin as Connection_Plugin;
6
use Automattic\Jetpack\Connection\Plugin_Storage as Connection_Plugin_Storage;
7
use Automattic\Jetpack\Constants;
8
use PHPUnit\Framework\TestCase;
9
use Requests_Utility_CaseInsensitiveDictionary;
10
use WorDBless\Options as WorDBless_Options;
11
use WP_REST_Request;
12
use WP_REST_Server;
13
use WP_User;
14
15
/**
16
 * Unit tests for the REST API endpoints.
17
 *
18
 * @package automattic/jetpack-connection
19
 * @see \Automattic\Jetpack\Connection\REST_Connector
20
 */
21
class Test_REST_Endpoints extends TestCase {
22
23
	const BLOG_TOKEN = 'new.blogtoken';
24
	const BLOG_ID    = 42;
25
	const USER_ID    = 111;
26
27
	/**
28
	 * REST Server object.
29
	 *
30
	 * @var WP_REST_Server
31
	 */
32
	private $server;
33
34
	/**
35
	 * The original hostname to restore after tests are finished.
36
	 *
37
	 * @var string
38
	 */
39
	private $api_host_original;
40
41
	/**
42
	 * Setting up the test.
43
	 *
44
	 * @before
45
	 */
46
	public function set_up() {
47
		global $wp_rest_server;
48
49
		$wp_rest_server = new WP_REST_Server();
50
		$this->server   = $wp_rest_server;
51
52
		do_action( 'rest_api_init' );
53
		new REST_Connector( new Manager() );
54
55
		add_action( 'jetpack_disabled_raw_options', array( $this, 'bypass_raw_options' ) );
56
57
		$user = wp_get_current_user();
58
		$user->add_cap( 'jetpack_reconnect' );
59
60
		$this->api_host_original                                  = Constants::get_constant( 'JETPACK__WPCOM_JSON_API_BASE' );
61
		Constants::$set_constants['JETPACK__WPCOM_JSON_API_BASE'] = 'https://public-api.wordpress.com';
62
63
		Constants::$set_constants['JETPACK__API_BASE'] = 'https://jetpack.wordpress.com/jetpack.';
64
65
		set_transient( 'jetpack_assumed_site_creation_date', '2020-02-28 01:13:27' );
66
	}
67
68
	/**
69
	 * Returning the environment into its initial state.
70
	 *
71
	 * @after
72
	 */
73
	public function tear_down() {
74
		remove_action( 'jetpack_disabled_raw_options', array( $this, 'bypass_raw_options' ) );
75
76
		$user = wp_get_current_user();
77
		$user->remove_cap( 'jetpack_reconnect' );
78
79
		Constants::$set_constants['JETPACK__WPCOM_JSON_API_BASE'] = $this->api_host_original;
80
81
		delete_transient( 'jetpack_assumed_site_creation_date' );
82
83
		WorDBless_Options::init()->clear_options();
84
	}
85
86
	/**
87
	 * Testing the `/jetpack/v4/remote_authorize` endpoint.
88
	 */
89
	public function test_remote_authorize() {
90
		add_filter( 'jetpack_options', array( $this, 'mock_jetpack_userless_options' ), 10, 2 );
91
		add_filter( 'pre_http_request', array( $this, 'intercept_auth_token_request' ), 10, 3 );
92
93
		wp_cache_set(
94
			self::USER_ID,
95
			(object) array(
96
				'ID'         => self::USER_ID,
97
				'user_email' => '[email protected]',
98
			),
99
			'users'
100
		);
101
102
		$secret_1 = 'Az0g39toGWlYiTJ4NnDuAz0g39toGWlY';
103
104
		$secrets = array(
105
			'jetpack_authorize_' . self::USER_ID => array(
106
				'secret_1' => $secret_1,
107
				'secret_2' => 'zfIFcym2Jlzd8AVgzfIFcym2Jlzd8AVg',
108
				'exp'      => time() + 60,
109
			),
110
		);
111
112
		// phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
113
		$options_filter = function ( $value ) use ( $secrets ) {
114
			return $secrets;
115
		};
116
		add_filter( 'pre_option_' . Secrets::LEGACY_SECRETS_OPTION_NAME, $options_filter );
117
118
		$user_caps_filter = function ( $allcaps, $caps, $args, $user ) {
119
			if ( $user instanceof WP_User && self::USER_ID === $user->ID ) {
0 ignored issues
show
Bug introduced by
The class WP_User does not exist. Is this class maybe located in a folder that is not analyzed, or in a newer version of your dependencies than listed in your composer.lock/composer.json?
Loading history...
120
				$allcaps['manage_options'] = true;
121
				$allcaps['administrator']  = true;
122
			}
123
124
			return $allcaps;
125
		};
126
		add_filter( 'user_has_cap', $user_caps_filter, 10, 4 );
127
128
		$this->request = new WP_REST_Request( 'POST', '/jetpack/v4/remote_authorize' );
129
		$this->request->set_header( 'Content-Type', 'application/json' );
130
		$this->request->set_body( '{ "state": "' . self::USER_ID . '", "secret": "' . $secret_1 . '", "redirect_uri": "https://example.org", "code": "54321" }' );
131
132
		$response = $this->server->dispatch( $this->request );
133
		$data     = $response->get_data();
134
135
		remove_filter( 'user_has_cap', $user_caps_filter );
136
		remove_filter( 'pre_option_' . Secrets::LEGACY_SECRETS_OPTION_NAME, $options_filter );
137
		remove_filter( 'pre_http_request', array( $this, 'intercept_auth_token_request' ) );
138
		remove_filter( 'jetpack_options', array( $this, 'mock_jetpack_userless_options' ) );
139
140
		wp_cache_delete( self::USER_ID, 'users' );
141
142
		wp_set_current_user( 0 );
143
144
		$this->assertEquals( 200, $response->get_status() );
145
		$this->assertEquals( 'authorized', $data['result'] );
146
	}
147
148
	/**
149
	 * Testing the `/jetpack/v4/connection` endpoint.
150
	 */
151
	public function test_connection() {
152
		add_filter( 'jetpack_offline_mode', '__return_true' );
153
		try {
154
			$this->request = new WP_REST_Request( 'GET', '/jetpack/v4/connection' );
155
156
			$response = $this->server->dispatch( $this->request );
157
			$data     = $response->get_data();
158
159
			$this->assertFalse( $data['isActive'] );
160
			$this->assertFalse( $data['isRegistered'] );
161
			$this->assertTrue( $data['offlineMode']['isActive'] );
162
		} finally {
163
			remove_filter( 'jetpack_offline_mode', '__return_true' );
164
		}
165
	}
166
167
	/**
168
	 * Testing the `/jetpack/v4/connection/plugins` endpoint.
169
	 */
170
	public function test_connection_plugins() {
171
		$user = wp_get_current_user();
172
		$user->add_cap( 'activate_plugins' );
173
174
		$plugins = array(
175
			array(
176
				'name' => 'Plugin Name 1',
177
				'slug' => 'plugin-slug-1',
178
			),
179
			array(
180
				'name' => 'Plugin Name 2',
181
				'slug' => 'plugin-slug-2',
182
			),
183
		);
184
185
		array_walk(
186
			$plugins,
187
			function ( $plugin ) {
188
				( new Connection_Plugin( $plugin['slug'] ) )->add( $plugin['name'] );
189
			}
190
		);
191
192
		Connection_Plugin_Storage::configure();
193
194
		$this->request = new WP_REST_Request( 'GET', '/jetpack/v4/connection/plugins' );
195
196
		$response = $this->server->dispatch( $this->request );
197
198
		$user->remove_cap( 'activate_plugins' );
199
200
		$this->assertEquals( $plugins, $response->get_data() );
201
	}
202
203
	/**
204
	 * Testing the `connection/reconnect` endpoint, full reconnect.
205
	 */
206
	public function test_connection_reconnect_full() {
207
		$this->setup_reconnect_test( null );
208
		add_filter( 'jetpack_connection_disconnect_site_wpcom', '__return_false' );
209
		add_filter( 'pre_http_request', array( $this, 'intercept_register_request' ), 10, 3 );
210
211
		$response = $this->server->dispatch( $this->build_reconnect_request() );
212
		$data     = $response->get_data();
213
214
		remove_filter( 'pre_http_request', array( $this, 'intercept_register_request' ), 10 );
215
		remove_filter( 'jetpack_connection_disconnect_site_wpcom', '__return_false' );
216
		$this->shutdown_reconnect_test( null );
217
218
		$this->assertEquals( 200, $response->get_status() );
219
		$this->assertEquals( 'in_progress', $data['status'] );
220
		$this->assertSame( 0, strpos( $data['authorizeUrl'], 'https://jetpack.wordpress.com/jetpack.authorize/' ) );
221
	}
222
223
	/**
224
	 * Testing the `connection/reconnect` endpoint, successful partial reconnect (blog token).
225
	 */
226 View Code Duplication
	public function test_connection_reconnect_partial_blog_token_success() {
227
		$this->setup_reconnect_test( 'blog_token' );
228
		add_filter( 'pre_http_request', array( $this, 'intercept_refresh_blog_token_request' ), 10, 3 );
229
230
		$response = $this->server->dispatch( $this->build_reconnect_request() );
231
		$data     = $response->get_data();
232
233
		remove_filter( 'pre_http_request', array( $this, 'intercept_refresh_blog_token_request' ), 10 );
234
		$this->shutdown_reconnect_test( 'blog_token' );
235
236
		$this->assertEquals( 200, $response->get_status() );
237
		$this->assertEquals( 'completed', $data['status'] );
238
	}
239
240
	/**
241
	 * Testing the `connection/reconnect` endpoint, failed partial reconnect (blog token).
242
	 */
243 View Code Duplication
	public function test_connection_reconnect_partial_blog_token_fail() {
244
		$this->setup_reconnect_test( 'blog_token' );
245
		add_filter( 'pre_http_request', array( $this, 'intercept_refresh_blog_token_request_fail' ), 10, 3 );
246
247
		$response = $this->server->dispatch( $this->build_reconnect_request() );
248
		$data     = $response->get_data();
249
250
		remove_filter( 'pre_http_request', array( $this, 'intercept_refresh_blog_token_request_fail' ), 10 );
251
		$this->shutdown_reconnect_test( 'blog_token' );
252
253
		$this->assertEquals( 500, $response->get_status() );
254
		$this->assertEquals( 'jetpack_secret', $data['code'] );
255
	}
256
257
	/**
258
	 * Testing the `connection/reconnect` endpoint, successful partial reconnect (user token).
259
	 */
260
	public function test_connection_reconnect_partial_user_token_success() {
261
		$this->setup_reconnect_test( 'user_token' );
262
263
		$response = $this->server->dispatch( $this->build_reconnect_request() );
264
		$data     = $response->get_data();
265
266
		$this->shutdown_reconnect_test( 'user_token' );
267
268
		$this->assertEquals( 200, $response->get_status() );
269
		$this->assertEquals( 'in_progress', $data['status'] );
270
		$this->assertSame( 0, strpos( $data['authorizeUrl'], 'https://jetpack.wordpress.com/jetpack.authorize/' ) );
271
	}
272
273
	/**
274
	 * Testing the `connection/reconnect` endpoint, userless (full reconnect).
275
	 */
276
	public function test_connection_reconnect_userless() {
277
		add_filter( 'jetpack_options', array( $this, 'mock_jetpack_userless_options' ), 10, 2 );
278
		add_filter( 'jetpack_connection_disconnect_site_wpcom', '__return_false' );
279
		add_filter( 'pre_http_request', array( $this, 'intercept_register_request' ), 10, 3 );
280
281
		$response = $this->server->dispatch( $this->build_reconnect_request() );
282
		$data     = $response->get_data();
283
284
		remove_filter( 'pre_http_request', array( $this, 'intercept_register_request' ), 10 );
285
		remove_filter( 'jetpack_connection_disconnect_site_wpcom', '__return_false' );
286
		remove_filter( 'jetpack_options', array( $this, 'mock_jetpack_userless_options' ) );
287
288
		$this->assertEquals( 200, $response->get_status() );
289
		$this->assertEquals( 'completed', $data['status'] );
290
	}
291
292
	/**
293
	 * This filter callback allow us to skip the database query by `Jetpack_Options` to retrieve the option.
294
	 *
295
	 * @param array $options List of options already skipping the database request.
296
	 *
297
	 * @return array
298
	 */
299
	public function bypass_raw_options( array $options ) {
300
		$options[ Secrets::LEGACY_SECRETS_OPTION_NAME ] = true;
301
302
		return $options;
303
	}
304
305
	/**
306
	 * Intercept the `jetpack.register` API request sent to WP.com, and mock the response.
307
	 *
308
	 * @param bool|array $response The existing response.
309
	 * @param array      $args The request arguments.
310
	 * @param string     $url The request URL.
311
	 *
312
	 * @return array
313
	 */
314
	public function intercept_register_request( $response, $args, $url ) {
315
		if ( false === strpos( $url, 'jetpack.register' ) ) {
316
			return $response;
317
		}
318
319
		return array(
320
			'headers'  => new Requests_Utility_CaseInsensitiveDictionary( array( 'content-type' => 'application/json' ) ),
321
			'body'     => wp_json_encode(
322
				array(
323
					'jetpack_id'     => '12345',
324
					'jetpack_secret' => 'sample_secret',
325
				)
326
			),
327
			'response' => array(
328
				'code'    => 200,
329
				'message' => 'OK',
330
			),
331
		);
332
	}
333
334
	/**
335
	 * Intercept the `jetpack-token-health` API request sent to WP.com, and mock the "invalid blog token" response.
336
	 *
337
	 * @param bool|array $response The existing response.
338
	 * @param array      $args The request arguments.
339
	 * @param string     $url The request URL.
340
	 *
341
	 * @return array
342
	 */
343
	public function intercept_validate_tokens_request_invalid_blog_token( $response, $args, $url ) {
344
		if ( false === strpos( $url, 'jetpack-token-health' ) ) {
345
			return $response;
346
		}
347
348
		return $this->build_validate_tokens_response( 'blog_token' );
349
	}
350
351
	/**
352
	 * Intercept the `jetpack-token-health` API request sent to WP.com, and mock the "invalid user token" response.
353
	 *
354
	 * @param bool|array $response The existing response.
355
	 * @param array      $args The request arguments.
356
	 * @param string     $url The request URL.
357
	 *
358
	 * @return array
359
	 */
360
	public function intercept_validate_tokens_request_invalid_user_token( $response, $args, $url ) {
361
		if ( false === strpos( $url, 'jetpack-token-health' ) ) {
362
			return $response;
363
		}
364
365
		return $this->build_validate_tokens_response( 'user_token' );
366
	}
367
368
	/**
369
	 * Intercept the `jetpack-token-health` API request sent to WP.com, and mock the "valid tokens" response.
370
	 *
371
	 * @param bool|array $response The existing response.
372
	 * @param array      $args The request arguments.
373
	 * @param string     $url The request URL.
374
	 *
375
	 * @return array
376
	 */
377
	public function intercept_validate_tokens_request_valid_tokens( $response, $args, $url ) {
378
		if ( false === strpos( $url, 'jetpack-token-health' ) ) {
379
			return $response;
380
		}
381
382
		return $this->build_validate_tokens_response( null );
383
	}
384
385
	/**
386
	 * Build the response for a tokens validation request
387
	 *
388
	 * @param string $invalid_token Accepted values: 'blog_token', 'user_token'.
389
	 *
390
	 * @return array
391
	 */
392
	private function build_validate_tokens_response( $invalid_token ) {
393
		$body = array(
394
			'blog_token' => array(
395
				'is_healthy' => true,
396
			),
397
			'user_token' => array(
398
				'is_healthy'     => true,
399
				'is_master_user' => true,
400
			),
401
		);
402
403
		switch ( $invalid_token ) {
404
			case 'blog_token':
405
				$body['blog_token'] = array(
406
					'is_healthy' => false,
407
					'code'       => 'unknown_token',
408
				);
409
				break;
410
			case 'user_token':
411
				$body['user_token'] = array(
412
					'is_healthy' => false,
413
					'code'       => 'unknown_token',
414
				);
415
				break;
416
		}
417
418
		return array(
419
			'headers'  => new Requests_Utility_CaseInsensitiveDictionary( array( 'content-type' => 'application/json' ) ),
420
			'body'     => wp_json_encode( $body ),
421
			'response' => array(
422
				'code'    => 200,
423
				'message' => 'OK',
424
			),
425
		);
426
	}
427
428
	/**
429
	 * Intercept the `jetpack-refresh-blog-token` API request sent to WP.com, and mock the success response.
430
	 *
431
	 * @param bool|array $response The existing response.
432
	 * @param array      $args The request arguments.
433
	 * @param string     $url The request URL.
434
	 *
435
	 * @return array
436
	 */
437 View Code Duplication
	public function intercept_refresh_blog_token_request( $response, $args, $url ) {
438
		if ( false === strpos( $url, 'jetpack-refresh-blog-token' ) ) {
439
			return $response;
440
		}
441
442
		return array(
443
			'headers'  => new Requests_Utility_CaseInsensitiveDictionary( array( 'content-type' => 'application/json' ) ),
444
			'body'     => wp_json_encode( array( 'jetpack_secret' => self::BLOG_TOKEN ) ),
445
			'response' => array(
446
				'code'    => 200,
447
				'message' => 'OK',
448
			),
449
		);
450
	}
451
452
	/**
453
	 * Intercept the `jetpack-refresh-blog-token` API request sent to WP.com, and mock the failure response.
454
	 *
455
	 * @param bool|array $response The existing response.
456
	 * @param array      $args The request arguments.
457
	 * @param string     $url The request URL.
458
	 *
459
	 * @return array
460
	 */
461 View Code Duplication
	public function intercept_refresh_blog_token_request_fail( $response, $args, $url ) {
462
		if ( false === strpos( $url, 'jetpack-refresh-blog-token' ) ) {
463
			return $response;
464
		}
465
466
		return array(
467
			'headers'  => new Requests_Utility_CaseInsensitiveDictionary( array( 'content-type' => 'application/json' ) ),
468
			'body'     => wp_json_encode( array( 'jetpack_secret_missing' => true ) ), // Meaningless body.
469
			'response' => array(
470
				'code'    => 200,
471
				'message' => 'OK',
472
			),
473
		);
474
	}
475
476
	/**
477
	 * Intercept the `jetpack-token-health` API request sent to WP.com, and mock the "invalid blog token" response.
478
	 *
479
	 * @param bool|array $response The existing response.
480
	 * @param array      $args The request arguments.
481
	 * @param string     $url The request URL.
482
	 *
483
	 * @return array
484
	 */
485
	public function intercept_auth_token_request( $response, $args, $url ) {
486
		if ( false === strpos( $url, '/jetpack.token/' ) ) {
487
			return $response;
488
		}
489
490
		return array(
491
			'headers'  => new Requests_Utility_CaseInsensitiveDictionary( array( 'content-type' => 'application/json' ) ),
492
			'body'     => wp_json_encode(
493
				array(
494
					'access_token' => 'mock.token',
495
					'token_type'   => 'X_JETPACK',
496
					'scope'        => ( new Manager() )->sign_role( 'administrator' ),
497
				)
498
			),
499
			'response' => array(
500
				'code'    => 200,
501
				'message' => 'OK',
502
			),
503
		);
504
	}
505
506
	/**
507
	 * Intercept the `Jetpack_Options` call and mock the values.
508
	 * Site level / user-less connection set-up.
509
	 *
510
	 * @param mixed  $value The current option value.
511
	 * @param string $name Option name.
512
	 *
513
	 * @return mixed
514
	 */
515
	public function mock_jetpack_userless_options( $value, $name ) {
516
		switch ( $name ) {
517
			case 'blog_token':
518
				return self::BLOG_TOKEN;
519
			case 'id':
520
				return self::BLOG_ID;
521
		}
522
523
		return $value;
524
	}
525
526
	/**
527
	 * Intercept the `Jetpack_Options` call and mock the values.
528
	 * Full connection set-up.
529
	 *
530
	 * @param mixed  $value The current option value.
531
	 * @param string $name Option name.
532
	 *
533
	 * @return mixed
534
	 */
535
	public function mock_jetpack_options( $value, $name ) {
536
		switch ( $name ) {
537
			case 'blog_token':
538
				return self::BLOG_TOKEN;
539
			case 'id':
540
				return self::BLOG_ID;
541
			case 'master_user':
542
				return self::USER_ID;
543
			case 'user_tokens':
544
				return array(
545
					self::USER_ID => 'new.usertoken.' . self::USER_ID,
546
				);
547
		}
548
549
		return $value;
550
	}
551
552
	/**
553
	 * Build the `connection/reconnect` request object.
554
	 *
555
	 * @return WP_REST_Request
556
	 */
557
	private function build_reconnect_request() {
558
		$this->request = new WP_REST_Request( 'POST', '/jetpack/v4/connection/reconnect' );
559
		$this->request->set_header( 'Content-Type', 'application/json' );
560
561
		return $this->request;
562
	}
563
564
	/**
565
	 * Setup the environment to test the reconnection process.
566
	 *
567
	 * @param string|null $invalid_token The invalid token to be returned in the response. Null if the tokens should be valid.
568
	 */
569 View Code Duplication
	private function setup_reconnect_test( $invalid_token ) {
570
		switch ( $invalid_token ) {
571
			case 'blog_token':
572
				add_filter(
573
					'pre_http_request',
574
					array(
575
						$this,
576
						'intercept_validate_tokens_request_invalid_blog_token',
577
					),
578
					10,
579
					3
580
				);
581
				break;
582
			case 'user_token':
583
				add_filter(
584
					'pre_http_request',
585
					array(
586
						$this,
587
						'intercept_validate_tokens_request_invalid_user_token',
588
					),
589
					10,
590
					3
591
				);
592
				break;
593
			case null:
0 ignored issues
show
Bug introduced by
It seems like you are loosely comparing $invalid_token of type string|null against null; this is ambiguous if the string can be empty. Consider using a strict comparison === instead.
Loading history...
594
				add_filter(
595
					'pre_http_request',
596
					array(
597
						$this,
598
						'intercept_validate_tokens_request_valid_tokens',
599
					),
600
					10,
601
					3
602
				);
603
				break;
604
		}
605
606
		add_filter( 'jetpack_options', array( $this, 'mock_jetpack_options' ), 10, 2 );
607
	}
608
609
	/**
610
	 * Restore the environment after the `reconnect` test has been run.
611
	 *
612
	 * @param string|null $invalid_token The invalid token to be returned in the response. Null if the tokens should be valid.
613
	 */
614 View Code Duplication
	private function shutdown_reconnect_test( $invalid_token ) {
615
		switch ( $invalid_token ) {
616
			case 'blog_token':
617
				remove_filter(
618
					'pre_http_request',
619
					array(
620
						$this,
621
						'intercept_validate_tokens_request_invalid_blog_token',
622
					),
623
					10
624
				);
625
				break;
626
			case 'user_token':
627
				remove_filter(
628
					'pre_http_request',
629
					array(
630
						$this,
631
						'intercept_validate_tokens_request_invalid_user_token',
632
					),
633
					10
634
				);
635
				break;
636
			case null:
0 ignored issues
show
Bug introduced by
It seems like you are loosely comparing $invalid_token of type string|null against null; this is ambiguous if the string can be empty. Consider using a strict comparison === instead.
Loading history...
637
				remove_filter(
638
					'pre_http_request',
639
					array(
640
						$this,
641
						'intercept_validate_tokens_request_valid_tokens',
642
					),
643
					10
644
				);
645
				break;
646
		}
647
648
		remove_filter( 'jetpack_options', array( $this, 'mock_jetpack_options' ), 10 );
649
		remove_filter( 'pre_http_request', array( $this, 'intercept_validate_tokens_request' ), 10 );
650
	}
651
652
}
653