Completed
Push — update/base-styles-210 ( 2e278b...ad767b )
by Jeremy
22:25 queued 13:15
created

Test_REST_Endpoints::test_connection_reconnect()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 23

Duplication

Lines 0
Ratio 0 %

Importance

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