Completed
Push — add/partial-reconnect ( cf917b...9b87ef )
by
unknown
123:52 queued 116:16
created

intercept_refresh_blog_token_request()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 14

Duplication

Lines 14
Ratio 100 %

Importance

Changes 0
Metric Value
cc 2
nc 2
nop 3
dl 14
loc 14
rs 9.7998
c 0
b 0
f 0
1
<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
2
3
namespace Automattic\Jetpack\Connection;
4
5
require_once ABSPATH . WPINC . '/class-IXR.php';
6
7
use Automattic\Jetpack\Connection\Plugin as Connection_Plugin;
8
use Automattic\Jetpack\Connection\Plugin_Storage as Connection_Plugin_Storage;
9
use Automattic\Jetpack\Constants;
10
use phpmock\MockBuilder;
11
use PHPUnit\Framework\TestCase;
12
use WP_REST_Request;
13
use WP_REST_Server;
14
use Requests_Utility_CaseInsensitiveDictionary;
15
16
/**
17
 * Unit tests for the REST API endpoints.
18
 *
19
 * @package automattic/jetpack-connection
20
 * @see \Automattic\Jetpack\Connection\REST_Connector
21
 */
22
class Test_REST_Endpoints extends TestCase {
23
24
	const BLOG_TOKEN = 'new.blogtoken';
25
26
	/**
27
	 * REST Server object.
28
	 *
29
	 * @var WP_REST_Server
30
	 */
31
	private $server;
32
33
	/**
34
	 * The original hostname to restore after tests are finished.
35
	 *
36
	 * @var string
37
	 */
38
	private $api_host_original;
39
40
	/**
41
	 * Setting up the test.
42
	 */
43
	public function setUp() {
44
		parent::setUp();
45
46
		global $wp_rest_server;
47
48
		$wp_rest_server = new WP_REST_Server();
49
		$this->server   = $wp_rest_server;
50
51
		do_action( 'rest_api_init' );
52
		new REST_Connector( new Manager() );
53
54
		add_action( 'jetpack_disabled_raw_options', array( $this, 'bypass_raw_options' ) );
55
56
		$user = wp_get_current_user();
57
		$user->add_cap( 'jetpack_connect' );
58
		$user->add_cap( 'jetpack_disconnect' );
59
60
		$this->api_host_original                                  = Constants::get_constant( 'JETPACK__WPCOM_JSON_API_HOST' );
61
		Constants::$set_constants['JETPACK__WPCOM_JSON_API_HOST'] = 'public-api.wordpress.com';
62
63
		set_transient( 'jetpack_assumed_site_creation_date', '2020-02-28 01:13:27' );
64
	}
65
66
	/**
67
	 * Returning the environment into its initial state.
68
	 */
69
	public function tearDown() {
70
		parent::tearDown();
71
72
		remove_action( 'jetpack_disabled_raw_options', array( $this, 'bypass_raw_options' ) );
73
74
		$user = wp_get_current_user();
75
		$user->remove_cap( 'jetpack_disconnect' );
76
		$user->remove_cap( 'jetpack_connect' );
77
78
		Constants::$set_constants['JETPACK__WPCOM_JSON_API_HOST'] = $this->api_host_original;
79
80
		delete_transient( 'jetpack_assumed_site_creation_date' );
81
	}
82
83
	/**
84
	 * Testing the `/jetpack/v4/remote_authorize` endpoint.
85
	 */
86
	public function test_remote_authorize() {
87
		$this->request = new WP_REST_Request( 'POST', '/jetpack/v4/remote_authorize' );
88
		$this->request->set_header( 'Content-Type', 'application/json' );
89
		$this->request->set_body( '{ "state": 111, "secret": "12345", "redirect_uri": "https://example.org", "code": "54321" }' );
90
91
		$response = $this->server->dispatch( $this->request );
92
		$data     = $response->get_data();
93
94
		$this->assertEquals( 400, $data['code'] );
95
		$this->assertContains( '[verify_secrets_missing]', $data['message'] );
96
	}
97
98
	/**
99
	 * Testing the `/jetpack/v4/connection` endpoint.
100
	 */
101
	public function test_connection() {
102
		$builder = new MockBuilder();
103
		$builder->setNamespace( 'Automattic\Jetpack' )
104
				->setName( 'apply_filters' )
105
				->setFunction(
106
					function( $hook, $value ) {
107
						return 'jetpack_offline_mode' === $hook ? true : $value;
108
					}
109
				);
110
111
		$mock = $builder->build();
112
		$mock->enable();
113
114
		$this->request = new WP_REST_Request( 'GET', '/jetpack/v4/connection' );
115
116
		$response = $this->server->dispatch( $this->request );
117
		$data     = $response->get_data();
118
119
		$this->assertFalse( $data['isActive'] );
120
		$this->assertFalse( $data['isRegistered'] );
121
		$this->assertTrue( $data['offlineMode']['isActive'] );
122
	}
123
124
	/**
125
	 * Testing the `/jetpack/v4/connection/plugins` endpoint.
126
	 */
127
	public function test_connection_plugins() {
128
		$user = wp_get_current_user();
129
		$user->add_cap( 'activate_plugins' );
130
131
		$plugins = array(
132
			array(
133
				'name' => 'Plugin Name 1',
134
				'slug' => 'plugin-slug-1',
135
			),
136
			array(
137
				'name' => 'Plugin Name 2',
138
				'slug' => 'plugin-slug-2',
139
			),
140
		);
141
142
		array_walk(
143
			$plugins,
144
			function( $plugin ) {
145
				( new Connection_Plugin( $plugin['slug'] ) )->add( $plugin['name'] );
146
			}
147
		);
148
149
		Connection_Plugin_Storage::configure();
150
151
		$this->request = new WP_REST_Request( 'GET', '/jetpack/v4/connection/plugins' );
152
153
		$response = $this->server->dispatch( $this->request );
154
155
		$this->assertEquals( $plugins, $response->get_data() );
156
157
		$user->remove_cap( 'activate_plugins' );
158
	}
159
160
	/**
161
	 * Testing the `connection/reconnect` endpoint, full reconnect.
162
	 */
163
	public function test_connection_reconnect_full() {
164
		$this->setup_reconnect_test( null );
165
		add_filter( 'pre_http_request', array( $this, 'intercept_register_request' ), 10, 3 );
166
167
		$response = $this->server->dispatch( $this->build_reconnect_request() );
168
		$this->assertEquals( 200, $response->get_status() );
169
170
		$data = $response->get_data();
171
		$this->assertEquals( 'in_progress', $data['status'] );
172
		$this->assertSame( 0, strpos( $data['authorizeUrl'], 'https://jetpack.wordpress.com/jetpack.authorize/1' ) );
173
174
		remove_filter( 'pre_http_request', array( $this, 'intercept_register_request' ), 10 );
175
		$this->setup_reconnect_test( null );
176
	}
177
178
	/**
179
	 * Testing the `connection/reconnect` endpoint, successful partial reconnect (blog token).
180
	 */
181 View Code Duplication
	public function test_connection_reconnect_partial_blog_token_success() {
182
		$this->setup_reconnect_test( 'blog_token' );
183
		add_filter( 'pre_http_request', array( $this, 'intercept_refresh_blog_token_request' ), 10, 3 );
184
185
		$response = $this->server->dispatch( $this->build_reconnect_request() );
186
		$this->assertEquals( 200, $response->get_status() );
187
188
		$data = $response->get_data();
189
		$this->assertEquals( 'completed', $data['status'] );
190
191
		remove_filter( 'pre_http_request', array( $this, 'intercept_refresh_blog_token_request' ), 10 );
192
		$this->shutdown_reconnect_test( 'blog_token' );
193
	}
194
195
	/**
196
	 * Testing the `connection/reconnect` endpoint, failed partial reconnect (blog token).
197
	 */
198 View Code Duplication
	public function test_connection_reconnect_partial_blog_token_fail() {
199
		$this->setup_reconnect_test( 'blog_token' );
200
		add_filter( 'pre_http_request', array( $this, 'intercept_refresh_blog_token_request_fail' ), 10, 3 );
201
202
		$response = $this->server->dispatch( $this->build_reconnect_request() );
203
		$this->assertEquals( 500, $response->get_status() );
204
205
		$data = $response->get_data();
206
		$this->assertEquals( 'jetpack_secret', $data['code'] );
207
208
		remove_filter( 'pre_http_request', array( $this, 'intercept_refresh_blog_token_request_fail' ), 10 );
209
		$this->shutdown_reconnect_test( 'blog_token' );
210
	}
211
212
	/**
213
	 * Testing the `connection/reconnect` endpoint, successful partial reconnect (user token).
214
	 */
215
	public function test_connection_reconnect_partial_user_token_success() {
216
		$this->setup_reconnect_test( 'user_token' );
217
218
		$response = $this->server->dispatch( $this->build_reconnect_request() );
219
		$this->assertEquals( 200, $response->get_status() );
220
221
		$data = $response->get_data();
222
		$this->assertEquals( 'in_progress', $data['status'] );
223
		$this->assertSame( 0, strpos( $data['authorizeUrl'], 'https://jetpack.wordpress.com/jetpack.authorize/1' ) );
224
225
		$this->shutdown_reconnect_test( 'user_token' );
226
	}
227
228
	/**
229
	 * This filter callback allow us to skip the database query by `Jetpack_Options` to retrieve the option.
230
	 *
231
	 * @param array $options List of options already skipping the database request.
232
	 *
233
	 * @return array
234
	 */
235
	public function bypass_raw_options( array $options ) {
236
		$options[ Manager::SECRETS_OPTION_NAME ] = true;
237
238
		return $options;
239
	}
240
241
	/**
242
	 * Intercept the `jetpack.register` API request sent to WP.com, and mock the response.
243
	 *
244
	 * @param bool|array $response The existing response.
245
	 * @param array      $args The request arguments.
246
	 * @param string     $url The request URL.
247
	 *
248
	 * @return array
249
	 */
250
	public function intercept_register_request( $response, $args, $url ) {
251
		if ( false === strpos( $url, 'jetpack.register' ) ) {
252
			return $response;
253
		}
254
255
		return array(
256
			'headers'  => new Requests_Utility_CaseInsensitiveDictionary( array( 'content-type' => 'application/json' ) ),
257
			'body'     => wp_json_encode(
258
				array(
259
					'jetpack_id'     => '12345',
260
					'jetpack_secret' => 'sample_secret',
261
				)
262
			),
263
			'response' => array(
264
				'code'    => 200,
265
				'message' => 'OK',
266
			),
267
		);
268
	}
269
270
	/**
271
	 * Intercept the `jetpack-token-health` API request sent to WP.com, and mock the "invalid blog token" response.
272
	 *
273
	 * @param bool|array $response The existing response.
274
	 * @param array      $args The request arguments.
275
	 * @param string     $url The request URL.
276
	 *
277
	 * @return array
278
	 */
279
	public function intercept_validate_tokens_request_invalid_blog_token( $response, $args, $url ) {
280
		if ( false === strpos( $url, 'jetpack-token-health' ) ) {
281
			return $response;
282
		}
283
284
		return $this->build_validate_tokens_response( 'blog_token' );
285
	}
286
287
	/**
288
	 * Intercept the `jetpack-token-health` API request sent to WP.com, and mock the "invalid user token" response.
289
	 *
290
	 * @param bool|array $response The existing response.
291
	 * @param array      $args The request arguments.
292
	 * @param string     $url The request URL.
293
	 *
294
	 * @return array
295
	 */
296
	public function intercept_validate_tokens_request_invalid_user_token( $response, $args, $url ) {
297
		if ( false === strpos( $url, 'jetpack-token-health' ) ) {
298
			return $response;
299
		}
300
301
		return $this->build_validate_tokens_response( 'user_token' );
302
	}
303
304
	/**
305
	 * Intercept the `jetpack-token-health` API request sent to WP.com, and mock the "valid tokens" response.
306
	 *
307
	 * @param bool|array $response The existing response.
308
	 * @param array      $args The request arguments.
309
	 * @param string     $url The request URL.
310
	 *
311
	 * @return array
312
	 */
313
	public function intercept_validate_tokens_request_valid_tokens( $response, $args, $url ) {
314
		if ( false === strpos( $url, 'jetpack-token-health' ) ) {
315
			return $response;
316
		}
317
318
		return $this->build_validate_tokens_response( null );
319
	}
320
321
	/**
322
	 * Build the response for a tokens validation request
323
	 *
324
	 * @param string $invalid_token Accepted values: 'blog_token', 'user_token'.
325
	 *
326
	 * @return array
327
	 */
328
	private function build_validate_tokens_response( $invalid_token ) {
329
		$body = array(
330
			'blog_token' => array(
331
				'is_healthy' => true,
332
			),
333
			'user_token' => array(
334
				'is_healthy'     => true,
335
				'is_master_user' => true,
336
			),
337
		);
338
339
		switch ( $invalid_token ) {
340
			case 'blog_token':
341
				$body['blog_token'] = array(
342
					'is_healthy' => false,
343
					'code'       => 'unknown_token',
344
				);
345
				break;
346
			case 'user_token':
347
				$body['user_token'] = array(
348
					'is_healthy' => false,
349
					'code'       => 'unknown_token',
350
				);
351
				break;
352
		}
353
354
		return array(
355
			'headers'  => new Requests_Utility_CaseInsensitiveDictionary( array( 'content-type' => 'application/json' ) ),
356
			'body'     => wp_json_encode( $body ),
357
			'response' => array(
358
				'code'    => 200,
359
				'message' => 'OK',
360
			),
361
		);
362
	}
363
364
	/**
365
	 * Intercept the `jetpack-refresh-blog-token` API request sent to WP.com, and mock the success response.
366
	 *
367
	 * @param bool|array $response The existing response.
368
	 * @param array      $args The request arguments.
369
	 * @param string     $url The request URL.
370
	 *
371
	 * @return array
372
	 */
373 View Code Duplication
	public function intercept_refresh_blog_token_request( $response, $args, $url ) {
374
		if ( false === strpos( $url, 'jetpack-refresh-blog-token' ) ) {
375
			return $response;
376
		}
377
378
		return array(
379
			'headers'  => new Requests_Utility_CaseInsensitiveDictionary( array( 'content-type' => 'application/json' ) ),
380
			'body'     => wp_json_encode( array( 'jetpack_secret' => self::BLOG_TOKEN ) ),
381
			'response' => array(
382
				'code'    => 200,
383
				'message' => 'OK',
384
			),
385
		);
386
	}
387
388
	/**
389
	 * Intercept the `jetpack-refresh-blog-token` API request sent to WP.com, and mock the failure response.
390
	 *
391
	 * @param bool|array $response The existing response.
392
	 * @param array      $args The request arguments.
393
	 * @param string     $url The request URL.
394
	 *
395
	 * @return array
396
	 */
397 View Code Duplication
	public function intercept_refresh_blog_token_request_fail( $response, $args, $url ) {
398
		if ( false === strpos( $url, 'jetpack-refresh-blog-token' ) ) {
399
			return $response;
400
		}
401
402
		return array(
403
			'headers'  => new Requests_Utility_CaseInsensitiveDictionary( array( 'content-type' => 'application/json' ) ),
404
			'body'     => wp_json_encode( array( 'jetpack_secret_missing' => true ) ), // Meaningless body.
405
			'response' => array(
406
				'code'    => 200,
407
				'message' => 'OK',
408
			),
409
		);
410
	}
411
412
	/**
413
	 * Intercept the `Jetpack_Options` call to get `blog_id`, and set a random value.
414
	 *
415
	 * @param mixed  $value The current option value.
416
	 * @param string $name Option name.
417
	 *
418
	 * @return int
419
	 */
420
	public function mock_blog_id( $value, $name ) {
421
		if ( 'id' !== $name ) {
422
			return $value;
423
		}
424
425
		return 42;
426
	}
427
428
	/**
429
	 * Intercept the `Jetpack_Options` call to get `user_tokens`, and set a mock value.
430
	 *
431
	 * @param mixed  $value The current option value.
432
	 * @param string $name Option name.
433
	 *
434
	 * @return int
435
	 */
436
	public function mock_access_tokens( $value, $name ) {
437
		if ( 'blog_token' !== $name ) {
438
			return $value;
439
		}
440
441
		return self::BLOG_TOKEN;
442
	}
443
444
	/**
445
	 * Build the `connection/reconnect` request object.
446
	 *
447
	 * @return WP_REST_Request
448
	 */
449
	private function build_reconnect_request() {
450
		$this->request = new WP_REST_Request( 'POST', '/jetpack/v4/connection/reconnect' );
451
		$this->request->set_header( 'Content-Type', 'application/json' );
452
		$this->request->set_body( wp_json_encode( array( 'action' => 'reconnect' ) ) );
453
454
		return $this->request;
455
	}
456
457
	/**
458
	 * Setup the environment to test the reconnection process.
459
	 *
460
	 * @param string|null $invalid_token The invalid token to be returned in the response. Null if the tokens should be valid.
461
	 */
462
	private function setup_reconnect_test( $invalid_token ) {
463 View Code Duplication
		switch ( $invalid_token ) {
464
			case 'blog_token':
465
				add_filter(
466
					'pre_http_request',
467
					array(
468
						$this,
469
						'intercept_validate_tokens_request_invalid_blog_token',
470
					),
471
					10,
472
					3
473
				);
474
				break;
475
			case 'user_token':
476
				add_filter(
477
					'pre_http_request',
478
					array(
479
						$this,
480
						'intercept_validate_tokens_request_invalid_user_token',
481
					),
482
					10,
483
					3
484
				);
485
				break;
486
			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...
487
				add_filter(
488
					'pre_http_request',
489
					array(
490
						$this,
491
						'intercept_validate_tokens_request_valid_tokens',
492
					),
493
					10,
494
					3
495
				);
496
				break;
497
		}
498
499
		add_filter( 'jetpack_options', array( $this, 'mock_access_tokens' ), 10, 2 );
500
		add_filter( 'jetpack_options', array( $this, 'mock_blog_id' ), 10, 2 );
501
	}
502
503
	/**
504
	 * Restore the environment after the `reconnect` test has been run.
505
	 *
506
	 * @param string|null $invalid_token The invalid token to be returned in the response. Null if the tokens should be valid.
507
	 */
508
	private function shutdown_reconnect_test( $invalid_token ) {
509 View Code Duplication
		switch ( $invalid_token ) {
510
			case 'blog_token':
511
				remove_filter(
512
					'pre_http_request',
513
					array(
514
						$this,
515
						'intercept_validate_tokens_request_invalid_blog_token',
516
					),
517
					10
518
				);
519
				break;
520
			case 'user_token':
521
				remove_filter(
522
					'pre_http_request',
523
					array(
524
						$this,
525
						'intercept_validate_tokens_request_invalid_user_token',
526
					),
527
					10
528
				);
529
				break;
530
			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...
531
				remove_filter(
532
					'pre_http_request',
533
					array(
534
						$this,
535
						'intercept_validate_tokens_request_valid_tokens',
536
					),
537
					10
538
				);
539
				break;
540
		}
541
542
		remove_filter( 'jetpack_options', array( $this, 'mock_blog_id' ), 10 );
543
		remove_filter( 'jetpack_options', array( $this, 'mock_access_tokens' ), 10 );
544
		remove_filter( 'pre_http_request', array( $this, 'intercept_validate_tokens_request' ), 10 );
545
	}
546
547
}
548