Completed
Push — improve/rename-anti-spam-in-si... ( e5a567...f08131 )
by
unknown
75:27 queued 67:29
created

connection/tests/php/test-rest-endpoints.php (2 issues)

Labels
Severity

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

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
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
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