Completed
Push — update/phpunit-php-8 ( e34c58...05aa04 )
by
unknown
137:27 queued 129:12
created

Test_REST_Endpoints::test_connection_plugins()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 32

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
nc 1
nop 0
dl 0
loc 32
rs 9.408
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_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_' . Manager::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_' . Manager::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_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
	 * This filter callback allow us to skip the database query by `Jetpack_Options` to retrieve the option.
275
	 *
276
	 * @param array $options List of options already skipping the database request.
277
	 *
278
	 * @return array
279
	 */
280
	public function bypass_raw_options( array $options ) {
281
		$options[ Manager::SECRETS_OPTION_NAME ] = true;
282
283
		return $options;
284
	}
285
286
	/**
287
	 * Intercept the `jetpack.register` API request sent to WP.com, and mock the response.
288
	 *
289
	 * @param bool|array $response The existing response.
290
	 * @param array      $args The request arguments.
291
	 * @param string     $url The request URL.
292
	 *
293
	 * @return array
294
	 */
295
	public function intercept_register_request( $response, $args, $url ) {
296
		if ( false === strpos( $url, 'jetpack.register' ) ) {
297
			return $response;
298
		}
299
300
		return array(
301
			'headers'  => new Requests_Utility_CaseInsensitiveDictionary( array( 'content-type' => 'application/json' ) ),
302
			'body'     => wp_json_encode(
303
				array(
304
					'jetpack_id'     => '12345',
305
					'jetpack_secret' => 'sample_secret',
306
				)
307
			),
308
			'response' => array(
309
				'code'    => 200,
310
				'message' => 'OK',
311
			),
312
		);
313
	}
314
315
	/**
316
	 * Intercept the `jetpack-token-health` API request sent to WP.com, and mock the "invalid blog token" response.
317
	 *
318
	 * @param bool|array $response The existing response.
319
	 * @param array      $args The request arguments.
320
	 * @param string     $url The request URL.
321
	 *
322
	 * @return array
323
	 */
324
	public function intercept_validate_tokens_request_invalid_blog_token( $response, $args, $url ) {
325
		if ( false === strpos( $url, 'jetpack-token-health' ) ) {
326
			return $response;
327
		}
328
329
		return $this->build_validate_tokens_response( 'blog_token' );
330
	}
331
332
	/**
333
	 * Intercept the `jetpack-token-health` API request sent to WP.com, and mock the "invalid user token" response.
334
	 *
335
	 * @param bool|array $response The existing response.
336
	 * @param array      $args The request arguments.
337
	 * @param string     $url The request URL.
338
	 *
339
	 * @return array
340
	 */
341
	public function intercept_validate_tokens_request_invalid_user_token( $response, $args, $url ) {
342
		if ( false === strpos( $url, 'jetpack-token-health' ) ) {
343
			return $response;
344
		}
345
346
		return $this->build_validate_tokens_response( 'user_token' );
347
	}
348
349
	/**
350
	 * Intercept the `jetpack-token-health` API request sent to WP.com, and mock the "valid tokens" response.
351
	 *
352
	 * @param bool|array $response The existing response.
353
	 * @param array      $args The request arguments.
354
	 * @param string     $url The request URL.
355
	 *
356
	 * @return array
357
	 */
358
	public function intercept_validate_tokens_request_valid_tokens( $response, $args, $url ) {
359
		if ( false === strpos( $url, 'jetpack-token-health' ) ) {
360
			return $response;
361
		}
362
363
		return $this->build_validate_tokens_response( null );
364
	}
365
366
	/**
367
	 * Build the response for a tokens validation request
368
	 *
369
	 * @param string $invalid_token Accepted values: 'blog_token', 'user_token'.
370
	 *
371
	 * @return array
372
	 */
373
	private function build_validate_tokens_response( $invalid_token ) {
374
		$body = array(
375
			'blog_token' => array(
376
				'is_healthy' => true,
377
			),
378
			'user_token' => array(
379
				'is_healthy'     => true,
380
				'is_master_user' => true,
381
			),
382
		);
383
384
		switch ( $invalid_token ) {
385
			case 'blog_token':
386
				$body['blog_token'] = array(
387
					'is_healthy' => false,
388
					'code'       => 'unknown_token',
389
				);
390
				break;
391
			case 'user_token':
392
				$body['user_token'] = array(
393
					'is_healthy' => false,
394
					'code'       => 'unknown_token',
395
				);
396
				break;
397
		}
398
399
		return array(
400
			'headers'  => new Requests_Utility_CaseInsensitiveDictionary( array( 'content-type' => 'application/json' ) ),
401
			'body'     => wp_json_encode( $body ),
402
			'response' => array(
403
				'code'    => 200,
404
				'message' => 'OK',
405
			),
406
		);
407
	}
408
409
	/**
410
	 * Intercept the `jetpack-refresh-blog-token` API request sent to WP.com, and mock the success response.
411
	 *
412
	 * @param bool|array $response The existing response.
413
	 * @param array      $args The request arguments.
414
	 * @param string     $url The request URL.
415
	 *
416
	 * @return array
417
	 */
418 View Code Duplication
	public function intercept_refresh_blog_token_request( $response, $args, $url ) {
419
		if ( false === strpos( $url, 'jetpack-refresh-blog-token' ) ) {
420
			return $response;
421
		}
422
423
		return array(
424
			'headers'  => new Requests_Utility_CaseInsensitiveDictionary( array( 'content-type' => 'application/json' ) ),
425
			'body'     => wp_json_encode( array( 'jetpack_secret' => self::BLOG_TOKEN ) ),
426
			'response' => array(
427
				'code'    => 200,
428
				'message' => 'OK',
429
			),
430
		);
431
	}
432
433
	/**
434
	 * Intercept the `jetpack-refresh-blog-token` API request sent to WP.com, and mock the failure response.
435
	 *
436
	 * @param bool|array $response The existing response.
437
	 * @param array      $args The request arguments.
438
	 * @param string     $url The request URL.
439
	 *
440
	 * @return array
441
	 */
442 View Code Duplication
	public function intercept_refresh_blog_token_request_fail( $response, $args, $url ) {
443
		if ( false === strpos( $url, 'jetpack-refresh-blog-token' ) ) {
444
			return $response;
445
		}
446
447
		return array(
448
			'headers'  => new Requests_Utility_CaseInsensitiveDictionary( array( 'content-type' => 'application/json' ) ),
449
			'body'     => wp_json_encode( array( 'jetpack_secret_missing' => true ) ), // Meaningless body.
450
			'response' => array(
451
				'code'    => 200,
452
				'message' => 'OK',
453
			),
454
		);
455
	}
456
457
	/**
458
	 * Intercept the `jetpack-token-health` API request sent to WP.com, and mock the "invalid blog token" response.
459
	 *
460
	 * @param bool|array $response The existing response.
461
	 * @param array      $args The request arguments.
462
	 * @param string     $url The request URL.
463
	 *
464
	 * @return array
465
	 */
466
	public function intercept_auth_token_request( $response, $args, $url ) {
467
		if ( false === strpos( $url, '/jetpack.token/' ) ) {
468
			return $response;
469
		}
470
471
		return array(
472
			'headers'  => new Requests_Utility_CaseInsensitiveDictionary( array( 'content-type' => 'application/json' ) ),
473
			'body'     => wp_json_encode(
474
				array(
475
					'access_token' => 'mock.token',
476
					'token_type'   => 'X_JETPACK',
477
					'scope'        => ( new Manager() )->sign_role( 'administrator' ),
478
				)
479
			),
480
			'response' => array(
481
				'code'    => 200,
482
				'message' => 'OK',
483
			),
484
		);
485
	}
486
487
	/**
488
	 * Intercept the `Jetpack_Options` call and mock the values.
489
	 *
490
	 * @param mixed  $value The current option value.
491
	 * @param string $name Option name.
492
	 *
493
	 * @return mixed
494
	 */
495
	public function mock_jetpack_options( $value, $name ) {
496
		switch ( $name ) {
497
			case 'blog_token':
498
				return self::BLOG_TOKEN;
499
			case 'id':
500
				return self::BLOG_ID;
501
		}
502
503
		return $value;
504
	}
505
506
	/**
507
	 * Build the `connection/reconnect` request object.
508
	 *
509
	 * @return WP_REST_Request
510
	 */
511
	private function build_reconnect_request() {
512
		$this->request = new WP_REST_Request( 'POST', '/jetpack/v4/connection/reconnect' );
513
		$this->request->set_header( 'Content-Type', 'application/json' );
514
515
		return $this->request;
516
	}
517
518
	/**
519
	 * Setup the environment to test the reconnection process.
520
	 *
521
	 * @param string|null $invalid_token The invalid token to be returned in the response. Null if the tokens should be valid.
522
	 */
523 View Code Duplication
	private function setup_reconnect_test( $invalid_token ) {
524
		switch ( $invalid_token ) {
525
			case 'blog_token':
526
				add_filter(
527
					'pre_http_request',
528
					array(
529
						$this,
530
						'intercept_validate_tokens_request_invalid_blog_token',
531
					),
532
					10,
533
					3
534
				);
535
				break;
536
			case 'user_token':
537
				add_filter(
538
					'pre_http_request',
539
					array(
540
						$this,
541
						'intercept_validate_tokens_request_invalid_user_token',
542
					),
543
					10,
544
					3
545
				);
546
				break;
547
			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...
548
				add_filter(
549
					'pre_http_request',
550
					array(
551
						$this,
552
						'intercept_validate_tokens_request_valid_tokens',
553
					),
554
					10,
555
					3
556
				);
557
				break;
558
		}
559
560
		add_filter( 'jetpack_options', array( $this, 'mock_jetpack_options' ), 10, 2 );
561
	}
562
563
	/**
564
	 * Restore the environment after the `reconnect` test has been run.
565
	 *
566
	 * @param string|null $invalid_token The invalid token to be returned in the response. Null if the tokens should be valid.
567
	 */
568 View Code Duplication
	private function shutdown_reconnect_test( $invalid_token ) {
569
		switch ( $invalid_token ) {
570
			case 'blog_token':
571
				remove_filter(
572
					'pre_http_request',
573
					array(
574
						$this,
575
						'intercept_validate_tokens_request_invalid_blog_token',
576
					),
577
					10
578
				);
579
				break;
580
			case 'user_token':
581
				remove_filter(
582
					'pre_http_request',
583
					array(
584
						$this,
585
						'intercept_validate_tokens_request_invalid_user_token',
586
					),
587
					10
588
				);
589
				break;
590
			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...
591
				remove_filter(
592
					'pre_http_request',
593
					array(
594
						$this,
595
						'intercept_validate_tokens_request_valid_tokens',
596
					),
597
					10
598
				);
599
				break;
600
		}
601
602
		remove_filter( 'jetpack_options', array( $this, 'mock_jetpack_options' ), 10 );
603
		remove_filter( 'pre_http_request', array( $this, 'intercept_validate_tokens_request' ), 10 );
604
	}
605
606
}
607