Completed
Push — fix/modal-close-button-element... ( 950c6c...5604b8 )
by
unknown
34:58 queued 24:57
created

  A

Complexity

Conditions 1
Paths 1

Size

Total Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
nc 1
nop 0
dl 0
loc 12
rs 9.8666
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\Connection\Rest_Authentication as Connection_Rest_Authentication;
8
use Automattic\Jetpack\Constants;
9
use Automattic\Jetpack\Redirect;
10
use Jetpack_Options;
11
use PHPUnit\Framework\TestCase;
12
use Requests_Utility_CaseInsensitiveDictionary;
13
use WorDBless\Options as WorDBless_Options;
14
use WorDBless\Users as WorDBless_Users;
15
use WP_REST_Request;
16
use WP_REST_Server;
17
use WP_User;
18
19
/**
20
 * Unit tests for the REST API endpoints.
21
 *
22
 * @package automattic/jetpack-connection
23
 * @see \Automattic\Jetpack\Connection\REST_Connector
24
 */
25
class Test_REST_Endpoints extends TestCase {
26
27
	const BLOG_TOKEN = 'new.blogtoken';
28
	const BLOG_ID    = 42;
29
	const USER_ID    = 111;
30
31
	/**
32
	 * REST Server object.
33
	 *
34
	 * @var WP_REST_Server
35
	 */
36
	private $server;
37
38
	/**
39
	 * The original hostname to restore after tests are finished.
40
	 *
41
	 * @var string
42
	 */
43
	private $api_host_original;
44
45
	/**
46
	 * The secondary user id.
47
	 *
48
	 * @var int
49
	 */
50
	private static $secondary_user_id;
51
52
	/**
53
	 * Setting up the test.
54
	 *
55
	 * @before
56
	 */
57
	public function set_up() {
58
		global $wp_rest_server;
59
60
		$wp_rest_server = new WP_REST_Server();
61
		$this->server   = $wp_rest_server;
62
63
		do_action( 'rest_api_init' );
64
		new REST_Connector( new Manager() );
65
66
		add_action( 'jetpack_disabled_raw_options', array( $this, 'bypass_raw_options' ) );
67
68
		$user = wp_get_current_user();
69
		$user->add_cap( 'jetpack_reconnect' );
70
		$user->add_cap( 'jetpack_connect' );
71
		$user->add_cap( 'jetpack_disconnect' );
72
73
		self::$secondary_user_id = wp_insert_user(
74
			array(
75
				'user_login' => 'test_is_user_connected_with_user_id_logged_in',
76
				'user_pass'  => '123',
77
				'role'       => 'administrator',
78
			)
79
		);
80
81
		$this->api_host_original                                  = Constants::get_constant( 'JETPACK__WPCOM_JSON_API_BASE' );
82
		Constants::$set_constants['JETPACK__WPCOM_JSON_API_BASE'] = 'https://public-api.wordpress.com';
83
84
		Constants::$set_constants['JETPACK__API_BASE'] = 'https://jetpack.wordpress.com/jetpack.';
85
86
		set_transient( 'jetpack_assumed_site_creation_date', '2020-02-28 01:13:27' );
87
	}
88
89
	/**
90
	 * Returning the environment into its initial state.
91
	 *
92
	 * @after
93
	 */
94
	public function tear_down() {
95
		remove_action( 'jetpack_disabled_raw_options', array( $this, 'bypass_raw_options' ) );
96
97
		$user = wp_get_current_user();
98
		$user->remove_cap( 'jetpack_reconnect' );
99
		$user->remove_cap( 'jetpack_connect' );
100
		$user->remove_cap( 'jetpack_disconnect' );
101
102
		Constants::$set_constants['JETPACK__WPCOM_JSON_API_BASE'] = $this->api_host_original;
103
104
		delete_transient( 'jetpack_assumed_site_creation_date' );
105
106
		WorDBless_Options::init()->clear_options();
107
		WorDBless_Users::init()->clear_all_users();
108
109
		unset( $_SERVER['REQUEST_METHOD'] );
110
		$_GET = array();
111
	}
112
113
	/**
114
	 * Testing the `/jetpack/v4/remote_authorize` endpoint.
115
	 */
116
	public function test_remote_authorize() {
117
		add_filter( 'jetpack_options', array( $this, 'mock_jetpack_site_connection_options' ), 10, 2 );
118
		add_filter( 'pre_http_request', array( $this, 'intercept_auth_token_request' ), 10, 3 );
119
120
		wp_cache_set(
121
			self::USER_ID,
122
			(object) array(
123
				'ID'         => self::USER_ID,
124
				'user_email' => '[email protected]',
125
			),
126
			'users'
127
		);
128
129
		$secret_1 = 'Az0g39toGWlYiTJ4NnDuAz0g39toGWlY';
130
131
		$secrets = array(
132
			'jetpack_authorize_' . self::USER_ID => array(
133
				'secret_1' => $secret_1,
134
				'secret_2' => 'zfIFcym2Jlzd8AVgzfIFcym2Jlzd8AVg',
135
				'exp'      => time() + 60,
136
			),
137
		);
138
139
		// phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
140
		$options_filter = function ( $value ) use ( $secrets ) {
141
			return $secrets;
142
		};
143
		add_filter( 'pre_option_' . Secrets::LEGACY_SECRETS_OPTION_NAME, $options_filter );
144
145
		$user_caps_filter = function ( $allcaps, $caps, $args, $user ) {
146
			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...
147
				$allcaps['manage_options'] = true;
148
				$allcaps['administrator']  = true;
149
			}
150
151
			return $allcaps;
152
		};
153
		add_filter( 'user_has_cap', $user_caps_filter, 10, 4 );
154
155
		$this->request = new WP_REST_Request( 'POST', '/jetpack/v4/remote_authorize' );
156
		$this->request->set_header( 'Content-Type', 'application/json' );
157
		$this->request->set_body( '{ "state": "' . self::USER_ID . '", "secret": "' . $secret_1 . '", "redirect_uri": "https://example.org", "code": "54321" }' );
158
159
		$response = $this->server->dispatch( $this->request );
160
		$data     = $response->get_data();
161
162
		remove_filter( 'user_has_cap', $user_caps_filter );
163
		remove_filter( 'pre_option_' . Secrets::LEGACY_SECRETS_OPTION_NAME, $options_filter );
164
		remove_filter( 'pre_http_request', array( $this, 'intercept_auth_token_request' ) );
165
		remove_filter( 'jetpack_options', array( $this, 'mock_jetpack_site_connection_options' ) );
166
167
		wp_cache_delete( self::USER_ID, 'users' );
168
169
		wp_set_current_user( 0 );
170
171
		$this->assertEquals( 200, $response->get_status() );
172
		$this->assertEquals( 'authorized', $data['result'] );
173
	}
174
175
	/**
176
	 * Testing the `/jetpack/v4/connection` endpoint.
177
	 */
178
	public function test_connection() {
179
		add_filter( 'jetpack_offline_mode', '__return_true' );
180
		try {
181
			$this->request = new WP_REST_Request( 'GET', '/jetpack/v4/connection' );
182
183
			$response = $this->server->dispatch( $this->request );
184
			$data     = $response->get_data();
185
186
			$this->assertFalse( $data['isActive'] );
187
			$this->assertFalse( $data['isRegistered'] );
188
			$this->assertTrue( $data['offlineMode']['isActive'] );
189
		} finally {
190
			remove_filter( 'jetpack_offline_mode', '__return_true' );
191
		}
192
	}
193
194
	/**
195
	 * Testing the `/jetpack/v4/connection` endpoint jetpack_connection_status filter.
196
	 */
197
	public function test_connection_jetpack_connection_status_filter() {
198
		add_filter(
199
			'jetpack_connection_status',
200
			function ( $status_data ) {
201
				$this->assertTrue( is_array( $status_data ) );
202
				return array();
203
			}
204
		);
205
		try {
206
			$this->request = new WP_REST_Request( 'GET', '/jetpack/v4/connection' );
207
208
			$response = $this->server->dispatch( $this->request );
209
			$data     = $response->get_data();
210
211
			$this->assertSame( array(), $data );
212
		} finally {
213
			remove_all_filters( 'jetpack_connection_status' );
214
		}
215
	}
216
217
	/**
218
	 * Testing the `/jetpack/v4/connection/plugins` endpoint.
219
	 */
220
	public function test_connection_plugins() {
221
		$user = wp_get_current_user();
222
		$user->add_cap( 'activate_plugins' );
223
224
		$plugins = array(
225
			array(
226
				'name' => 'Plugin Name 1',
227
				'slug' => 'plugin-slug-1',
228
			),
229
			array(
230
				'name' => 'Plugin Name 2',
231
				'slug' => 'plugin-slug-2',
232
			),
233
		);
234
235
		array_walk(
236
			$plugins,
237
			function ( $plugin ) {
238
				( new Connection_Plugin( $plugin['slug'] ) )->add( $plugin['name'] );
239
			}
240
		);
241
242
		Connection_Plugin_Storage::configure();
243
244
		$this->request = new WP_REST_Request( 'GET', '/jetpack/v4/connection/plugins' );
245
246
		$response = $this->server->dispatch( $this->request );
247
248
		$user->remove_cap( 'activate_plugins' );
249
250
		$this->assertEquals( $plugins, $response->get_data() );
251
	}
252
253
	/**
254
	 * Testing the `connection/reconnect` endpoint, full reconnect.
255
	 */
256
	public function test_connection_reconnect_full() {
257
		$this->setup_reconnect_test( null );
258
		add_filter( 'jetpack_connection_disconnect_site_wpcom', '__return_false' );
259
		add_filter( 'pre_http_request', array( static::class, 'intercept_register_request' ), 10, 3 );
260
261
		$response = $this->server->dispatch( $this->build_reconnect_request() );
262
		$data     = $response->get_data();
263
264
		remove_filter( 'pre_http_request', array( static::class, 'intercept_register_request' ), 10 );
265
		remove_filter( 'jetpack_connection_disconnect_site_wpcom', '__return_false' );
266
		$this->shutdown_reconnect_test( null );
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, successful partial reconnect (blog token).
275
	 */
276 View Code Duplication
	public function test_connection_reconnect_partial_blog_token_success() {
277
		$this->setup_reconnect_test( 'blog_token' );
278
		add_filter( 'pre_http_request', array( $this, 'intercept_refresh_blog_token_request' ), 10, 3 );
279
280
		$response = $this->server->dispatch( $this->build_reconnect_request() );
281
		$data     = $response->get_data();
282
283
		remove_filter( 'pre_http_request', array( $this, 'intercept_refresh_blog_token_request' ), 10 );
284
		$this->shutdown_reconnect_test( 'blog_token' );
285
286
		$this->assertEquals( 200, $response->get_status() );
287
		$this->assertEquals( 'completed', $data['status'] );
288
	}
289
290
	/**
291
	 * Testing the `connection/reconnect` endpoint, failed partial reconnect (blog token).
292
	 */
293 View Code Duplication
	public function test_connection_reconnect_partial_blog_token_fail() {
294
		$this->setup_reconnect_test( 'blog_token' );
295
		add_filter( 'pre_http_request', array( $this, 'intercept_refresh_blog_token_request_fail' ), 10, 3 );
296
297
		$response = $this->server->dispatch( $this->build_reconnect_request() );
298
		$data     = $response->get_data();
299
300
		remove_filter( 'pre_http_request', array( $this, 'intercept_refresh_blog_token_request_fail' ), 10 );
301
		$this->shutdown_reconnect_test( 'blog_token' );
302
303
		$this->assertEquals( 500, $response->get_status() );
304
		$this->assertEquals( 'jetpack_secret', $data['code'] );
305
	}
306
307
	/**
308
	 * Testing the `connection/reconnect` endpoint, successful partial reconnect (user token).
309
	 */
310
	public function test_connection_reconnect_partial_user_token_success() {
311
		$this->setup_reconnect_test( 'user_token' );
312
313
		$response = $this->server->dispatch( $this->build_reconnect_request() );
314
		$data     = $response->get_data();
315
316
		$this->shutdown_reconnect_test( 'user_token' );
317
318
		$this->assertEquals( 200, $response->get_status() );
319
		$this->assertEquals( 'in_progress', $data['status'] );
320
		$this->assertSame( 0, strpos( $data['authorizeUrl'], 'https://jetpack.wordpress.com/jetpack.authorize/' ) );
321
	}
322
323
	/**
324
	 * Testing the `connection/reconnect` endpoint, site_connection (full reconnect).
325
	 */
326
	public function test_connection_reconnect_site_connection() {
327
		add_filter( 'jetpack_options', array( $this, 'mock_jetpack_site_connection_options' ), 10, 2 );
328
		add_filter( 'jetpack_connection_disconnect_site_wpcom', '__return_false' );
329
		add_filter( 'pre_http_request', array( static::class, 'intercept_register_request' ), 10, 3 );
330
331
		$response = $this->server->dispatch( $this->build_reconnect_request() );
332
		$data     = $response->get_data();
333
334
		remove_filter( 'pre_http_request', array( static::class, 'intercept_register_request' ), 10 );
335
		remove_filter( 'jetpack_connection_disconnect_site_wpcom', '__return_false' );
336
		remove_filter( 'jetpack_options', array( $this, 'mock_jetpack_site_connection_options' ) );
337
338
		$this->assertEquals( 200, $response->get_status() );
339
		$this->assertEquals( 'completed', $data['status'] );
340
	}
341
342
	/**
343
	 * Testing the `connection/reconnect` endpoint when the token validation request fails.
344
	 */
345
	public function test_connection_reconnect_when_token_validation_request_fails() {
346
		$this->setup_reconnect_test( 'token_validation_failed' );
347
		add_filter( 'jetpack_connection_disconnect_site_wpcom', '__return_false' );
348
		add_filter( 'pre_http_request', array( static::class, 'intercept_register_request' ), 10, 3 );
349
350
		$response = $this->server->dispatch( $this->build_reconnect_request() );
351
		$data     = $response->get_data();
352
353
		remove_filter( 'pre_http_request', array( static::class, 'intercept_register_request' ), 10 );
354
		remove_filter( 'jetpack_connection_disconnect_site_wpcom', '__return_false' );
355
		$this->shutdown_reconnect_test( 'token_validation_failed' );
356
357
		$this->assertEquals( 200, $response->get_status() );
358
		$this->assertEquals( 'in_progress', $data['status'] );
359
		$this->assertSame( 0, strpos( $data['authorizeUrl'], 'https://jetpack.wordpress.com/jetpack.authorize/' ) );
360
	}
361
362
	/**
363
	 * Testing the `connection/register` endpoint.
364
	 */
365 View Code Duplication
	public function test_connection_register() {
366
		add_filter( 'pre_http_request', array( static::class, 'intercept_register_request' ), 10, 3 );
367
368
		$this->request = new WP_REST_Request( 'POST', '/jetpack/v4/connection/register' );
369
		$this->request->set_header( 'Content-Type', 'application/json' );
370
371
		$this->request->set_body( wp_json_encode( array( 'registration_nonce' => wp_create_nonce( 'jetpack-registration-nonce' ) ) ) );
372
373
		$response = $this->server->dispatch( $this->request );
374
		$data     = $response->get_data();
375
376
		remove_filter( 'pre_http_request', array( static::class, 'intercept_register_request' ), 10 );
377
378
		// Manually clears filter added by Manager::register().
379
		remove_filter( 'jetpack_use_iframe_authorization_flow', '__return_false', 20 );
380
381
		$this->assertEquals( 200, $response->get_status() );
382
		$this->assertSame( 0, strpos( $data['authorizeUrl'], 'https://jetpack.wordpress.com/jetpack.authorize/' ) );
383
384
		// Asserts jetpack_register_site_rest_response filter is being properly hooked to add data from wpcom register endpoint response.
385
		$this->assertFalse( $data['allowInplaceAuthorization'] );
386
		$this->assertSame( '', $data['alternateAuthorizeUrl'] );
387
	}
388
389
	/**
390
	 * Testing the `connection/register` endpoint with allow_inplace_authorization as true.
391
	 */
392
	public function test_connection_register_allow_inplace() {
393
		add_filter( 'pre_http_request', array( static::class, 'intercept_register_request_with_allow_inplace' ), 10, 3 );
394
395
		$this->request = new WP_REST_Request( 'POST', '/jetpack/v4/connection/register' );
396
		$this->request->set_header( 'Content-Type', 'application/json' );
397
398
		$this->request->set_body( wp_json_encode( array( 'registration_nonce' => wp_create_nonce( 'jetpack-registration-nonce' ) ) ) );
399
400
		$response = $this->server->dispatch( $this->request );
401
		$data     = $response->get_data();
402
403
		remove_filter( 'pre_http_request', array( static::class, 'intercept_register_request_with_allow_inplace' ), 10 );
404
405
		$this->assertEquals( 200, $response->get_status() );
406
		$this->assertSame( 0, strpos( $data['authorizeUrl'], 'https://jetpack.wordpress.com/jetpack.authorize_iframe/' ) );
407
408
		// Asserts jetpack_register_site_rest_response filter is being properly hooked to add data from wpcom register endpoint response.
409
		$this->assertTrue( $data['allowInplaceAuthorization'] );
410
		$this->assertSame( '', $data['alternateAuthorizeUrl'] );
411
	}
412
413
	/**
414
	 * Testing the `connection/register` endpoint with alternate_authorization_url
415
	 */
416 View Code Duplication
	public function test_connection_register_with_alternate_auth_url() {
417
		add_filter( 'pre_http_request', array( static::class, 'intercept_register_request_with_alternate_auth_url' ), 10, 3 );
418
419
		$this->request = new WP_REST_Request( 'POST', '/jetpack/v4/connection/register' );
420
		$this->request->set_header( 'Content-Type', 'application/json' );
421
422
		$this->request->set_body( wp_json_encode( array( 'registration_nonce' => wp_create_nonce( 'jetpack-registration-nonce' ) ) ) );
423
424
		$response = $this->server->dispatch( $this->request );
425
		$data     = $response->get_data();
426
427
		remove_filter( 'pre_http_request', array( static::class, 'intercept_register_request_with_alternate_auth_url' ), 10 );
428
429
		// Manually clears filter added by Manager::register().
430
		remove_filter( 'jetpack_use_iframe_authorization_flow', '__return_false', 20 );
431
432
		$this->assertEquals( 200, $response->get_status() );
433
		$this->assertSame( 0, strpos( $data['authorizeUrl'], 'https://jetpack.wordpress.com/jetpack.authorize/' ) );
434
435
		// Asserts jetpack_register_site_rest_response filter is being properly hooked to add data from wpcom register endpoint response.
436
		$this->assertFalse( $data['allowInplaceAuthorization'] );
437
		$this->assertSame( Redirect::get_url( 'https://dummy.com' ), $data['alternateAuthorizeUrl'] );
438
	}
439
440
	/**
441
	 * Testing the `user-token` endpoint without authentication.
442
	 * Response: failed authorization.
443
	 */
444
	public function test_set_user_token_unauthenticated() {
445
		$this->request = new WP_REST_Request( 'POST', '/jetpack/v4/user-token' );
446
		$this->request->set_header( 'Content-Type', 'application/json' );
447
448
		$this->request->set_body( wp_json_encode( array( 'user_token' => 'test.test.1' ) ) );
449
450
		$response = $this->server->dispatch( $this->request );
451
		$data     = $response->get_data();
452
453
		static::assertEquals( 'invalid_permission_update_user_token', $data['code'] );
454
		static::assertEquals( 401, $data['data']['status'] );
455
	}
456
457
	/**
458
	 * Testing the `user-token` endpoint using blog token authorization.
459
	 * Response: user token updated.
460
	 */
461
	public function test_set_user_token_success() {
462
		add_filter( 'jetpack_options', array( $this, 'mock_jetpack_site_connection_options' ), 10, 2 );
463
464
		$action_hook_id    = null;
465
		$action_hook_token = null;
466
		$action_hook       = function ( $user_id, $user_token ) use ( &$action_hook_id, &$action_hook_token ) {
467
			$action_hook_id    = $user_id;
468
			$action_hook_token = $user_token;
469
		};
470
471
		add_action( 'jetpack_updated_user_token', $action_hook, 10, 2 );
472
473
		$token     = 'new:1:0';
474
		$timestamp = (string) time();
475
		$nonce     = 'testing123';
476
		$body_hash = '';
477
478
		wp_cache_set(
479
			1,
480
			(object) array(
481
				'ID'         => 1,
482
				'user_email' => '[email protected]',
483
			),
484
			'users'
485
		);
486
487
		$_SERVER['REQUEST_METHOD'] = 'POST';
488
489
		$_GET['_for']      = 'jetpack';
490
		$_GET['token']     = $token;
491
		$_GET['timestamp'] = $timestamp;
492
		$_GET['nonce']     = $nonce;
493
		$_GET['body-hash'] = $body_hash;
494
		// This is intentionally using base64_encode().
495
		// phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
496
		$_GET['signature'] = base64_encode(
497
			hash_hmac(
498
				'sha1',
499
				implode(
500
					"\n",
501
					$data  = array(
502
						$token,
503
						$timestamp,
504
						$nonce,
505
						$body_hash,
506
						'POST',
507
						'anything.example',
508
						'80',
509
						'',
510
					)
511
				) . "\n",
512
				'blogtoken',
513
				true
514
			)
515
		);
516
517
		Connection_Rest_Authentication::init()->wp_rest_authenticate( false );
518
519
		$this->request = new WP_REST_Request( 'POST', '/jetpack/v4/user-token' );
520
		$this->request->set_header( 'Content-Type', 'application/json' );
521
522
		$user_token = 'test.test.1';
523
524
		$this->request->set_body( wp_json_encode( array( 'user_token' => $user_token ) ) );
525
526
		$response = $this->server->dispatch( $this->request );
527
		$data     = $response->get_data();
528
529
		remove_action( 'jetpack_updated_user_token', $action_hook );
530
		remove_filter( 'jetpack_options', array( $this, 'mock_jetpack_site_connection_options' ) );
531
		wp_cache_delete( 1, 'users' );
532
533
		static::assertTrue( $data['success'] );
534
		static::assertEquals( 200, $response->status );
535
		static::assertEquals( array( 1 => $user_token ), Jetpack_Options::get_option( 'user_tokens' ) );
536
		static::assertSame( 1, $action_hook_id, "The 'jetpack_update_user_token_success' action was not properly executed." );
537
		static::assertEquals( $user_token, $action_hook_token, "The 'jetpack_update_user_token_success' action was not properly executed." );
538
	}
539
540
	/**
541
	 * Testing the `connection/owner` endpoint on failure.
542
	 */
543
	public function test_update_connection_owner_failures() {
544
		// Mock full connection established.
545
		add_filter( 'jetpack_options', array( $this, 'mock_jetpack_options' ), 10, 2 );
546
547
		$this->request = new WP_REST_Request( 'POST', '/jetpack/v4/connection/owner' );
548
		$this->request->set_header( 'Content-Type', 'application/json' );
549
550
		// Attempt owner change without setting an owner.
551
		$response = $this->server->dispatch( $this->request );
552
		$this->assertEquals( 400, $response->get_status() );
553
		$this->assertEquals( 'Missing parameter(s): owner', $response->get_data()['message'] );
554
555
		// Attempt owner change with bad user.
556
		$this->request->set_body( wp_json_encode( array( 'owner' => 999 ) ) );
557
		$response = $this->server->dispatch( $this->request );
558
		$this->assertEquals( 400, $response->get_status() );
559
		$this->assertEquals( 'New owner is not admin', $response->get_data()['message'] );
560
561
		// Change owner to valid user but XML-RPC request to WPCOM failed.
562
		add_filter( 'pre_http_request', array( $this, 'mock_xmlrpc_failure' ), 10, 3 );
563
564
		$this->request->set_body( wp_json_encode( array( 'owner' => self::$secondary_user_id ) ) );
565
		$response = $this->server->dispatch( $this->request );
566
567
		remove_filter( 'pre_http_request', array( $this, 'mock_xmlrpc_failure' ), 10 );
568
		remove_filter( 'jetpack_options', array( $this, 'mock_jetpack_options' ), 10 );
569
570
		$this->assertEquals( 500, $response->get_status() );
571
		$this->assertEquals( 'Could not confirm new owner.', $response->get_data()['message'] );
572
	}
573
574
	/**
575
	 * Testing the `connection/owner` endpoint on success.
576
	 */
577
	public function test_update_connection_owner_success() {
578
		// Change owner to valid user.
579
		$this->request = new WP_REST_Request( 'POST', '/jetpack/v4/connection/owner' );
580
		$this->request->set_header( 'Content-Type', 'application/json' );
581
		$this->request->set_body( wp_json_encode( array( 'owner' => self::$secondary_user_id ) ) );
582
583
		// Mock full connection established.
584
		add_filter( 'jetpack_options', array( $this, 'mock_jetpack_options' ), 10, 2 );
585
		// Mock owner successfully updated on WPCOM.
586
		add_filter( 'pre_http_request', array( $this, 'mock_xmlrpc_success' ), 10, 3 );
587
		$response = $this->server->dispatch( $this->request );
588
589
		remove_filter( 'pre_http_request', array( $this, 'mock_xmlrpc_success' ), 10 );
590
		remove_filter( 'jetpack_options', array( $this, 'mock_jetpack_options' ), 10 );
591
592
		$this->assertEquals( 200, $response->get_status() );
593
		$this->assertEquals( self::$secondary_user_id, Jetpack_Options::get_option( 'master_user' ), 'Connection owner should be updated.' );
594
	}
595
596
	/**
597
	 * This filter callback allows us to skip the database query by `Jetpack_Options` to retrieve the option.
598
	 *
599
	 * @param array $options List of options already skipping the database request.
600
	 *
601
	 * @return array
602
	 */
603
	public function bypass_raw_options( array $options ) {
604
		$options[ Secrets::LEGACY_SECRETS_OPTION_NAME ] = true;
605
606
		return $options;
607
	}
608
609
	/**
610
	 * Intercept the `jetpack.register` API request sent to WP.com, and mock the response.
611
	 *
612
	 * @param bool|array $response The existing response.
613
	 * @param array      $args The request arguments.
614
	 * @param string     $url The request URL.
615
	 *
616
	 * @return array
617
	 */
618
	public static function intercept_register_request( $response, $args, $url ) {
619
		if ( false === strpos( $url, 'jetpack.register' ) ) {
620
			return $response;
621
		}
622
623
		return self::get_register_request_mock_response();
624
	}
625
626
	/**
627
	 * Intercept the `jetpack.register` API request sent to WP.com, and mock the response with allow_inplace_authorization as true.
628
	 *
629
	 * @param bool|array $response The existing response.
630
	 * @param array      $args The request arguments.
631
	 * @param string     $url The request URL.
632
	 *
633
	 * @return array
634
	 */
635
	public static function intercept_register_request_with_allow_inplace( $response, $args, $url ) {
636
		if ( false === strpos( $url, 'jetpack.register' ) ) {
637
			return $response;
638
		}
639
640
		return self::get_register_request_mock_response( true );
641
	}
642
643
	/**
644
	 * Intercept the `jetpack.register` API request sent to WP.com, and mock the response with a value in alternate_authorization_url key.
645
	 *
646
	 * @param bool|array $response The existing response.
647
	 * @param array      $args The request arguments.
648
	 * @param string     $url The request URL.
649
	 *
650
	 * @return array
651
	 */
652
	public static function intercept_register_request_with_alternate_auth_url( $response, $args, $url ) {
653
		if ( false === strpos( $url, 'jetpack.register' ) ) {
654
			return $response;
655
		}
656
657
		return self::get_register_request_mock_response( false, 'https://dummy.com' );
658
	}
659
660
	/**
661
	 * Gets a mocked REST response from jetpack.register WPCOM endpoint
662
	 *
663
	 * @param boolean $allow_inplace_authorization the value of allow_inplace_authorization returned by the server.
664
	 * @param string  $alternate_authorization_url the value of alternate_authorization_url returned by the server.
665
	 * @return array
666
	 */
667
	private static function get_register_request_mock_response( $allow_inplace_authorization = false, $alternate_authorization_url = '' ) {
668
		return array(
669
			'headers'  => new Requests_Utility_CaseInsensitiveDictionary( array( 'content-type' => 'application/json' ) ),
670
			'body'     => wp_json_encode(
671
				array(
672
					'jetpack_id'                  => '12345',
673
					'jetpack_secret'              => 'sample_secret',
674
					'allow_inplace_authorization' => $allow_inplace_authorization,
675
					'alternate_authorization_url' => $alternate_authorization_url,
676
				)
677
			),
678
			'response' => array(
679
				'code'    => 200,
680
				'message' => 'OK',
681
			),
682
		);
683
	}
684
685
	/**
686
	 * Intercept the `jetpack-token-health` API request sent to WP.com, and mock the "invalid blog token" response.
687
	 *
688
	 * @param bool|array $response The existing response.
689
	 * @param array      $args The request arguments.
690
	 * @param string     $url The request URL.
691
	 *
692
	 * @return array
693
	 */
694
	public function intercept_validate_tokens_request_invalid_blog_token( $response, $args, $url ) {
695
		if ( false === strpos( $url, 'jetpack-token-health' ) ) {
696
			return $response;
697
		}
698
699
		return $this->build_validate_tokens_response( 'blog_token' );
700
	}
701
702
	/**
703
	 * Intercept the `jetpack-token-health` API request sent to WP.com, and mock the "invalid user token" response.
704
	 *
705
	 * @param bool|array $response The existing response.
706
	 * @param array      $args The request arguments.
707
	 * @param string     $url The request URL.
708
	 *
709
	 * @return array
710
	 */
711
	public function intercept_validate_tokens_request_invalid_user_token( $response, $args, $url ) {
712
		if ( false === strpos( $url, 'jetpack-token-health' ) ) {
713
			return $response;
714
		}
715
716
		return $this->build_validate_tokens_response( 'user_token' );
717
	}
718
719
	/**
720
	 * Intercept the `jetpack-token-health` API request sent to WP.com, and mock the "valid tokens" response.
721
	 *
722
	 * @param bool|array $response The existing response.
723
	 * @param array      $args The request arguments.
724
	 * @param string     $url The request URL.
725
	 *
726
	 * @return array
727
	 */
728
	public function intercept_validate_tokens_request_valid_tokens( $response, $args, $url ) {
729
		if ( false === strpos( $url, 'jetpack-token-health' ) ) {
730
			return $response;
731
		}
732
733
		return $this->build_validate_tokens_response( null );
734
	}
735
736
	/**
737
	 * Intercept the `jetpack-token-health` API request sent to WP.com, and mock failed response.
738
	 *
739
	 * @param bool|array $response The existing response.
740
	 * @param array      $args The request arguments.
741
	 * @param string     $url The request URL.
742
	 *
743
	 * @return array
744
	 */
745 View Code Duplication
	public function intercept_validate_tokens_request_failed( $response, $args, $url ) {
746
		if ( false === strpos( $url, 'jetpack-token-health' ) ) {
747
			return $response;
748
		}
749
750
		return array(
751
			'headers'  => new Requests_Utility_CaseInsensitiveDictionary( array( 'content-type' => 'application/json' ) ),
752
			'body'     => wp_json_encode( array( 'dummy_error' => true ) ),
753
			'response' => array(
754
				'code'    => 500,
755
				'message' => 'failed',
756
			),
757
		);
758
	}
759
760
	/**
761
	 * Build the response for a tokens validation request
762
	 *
763
	 * @param string $invalid_token Accepted values: 'blog_token', 'user_token'.
764
	 *
765
	 * @return array
766
	 */
767
	private function build_validate_tokens_response( $invalid_token ) {
768
		$body = array(
769
			'blog_token' => array(
770
				'is_healthy' => true,
771
			),
772
			'user_token' => array(
773
				'is_healthy'     => true,
774
				'is_master_user' => true,
775
			),
776
		);
777
778
		switch ( $invalid_token ) {
779
			case 'blog_token':
780
				$body['blog_token'] = array(
781
					'is_healthy' => false,
782
					'code'       => 'unknown_token',
783
				);
784
				break;
785
			case 'user_token':
786
				$body['user_token'] = array(
787
					'is_healthy' => false,
788
					'code'       => 'unknown_token',
789
				);
790
				break;
791
		}
792
793
		return array(
794
			'headers'  => new Requests_Utility_CaseInsensitiveDictionary( array( 'content-type' => 'application/json' ) ),
795
			'body'     => wp_json_encode( $body ),
796
			'response' => array(
797
				'code'    => 200,
798
				'message' => 'OK',
799
			),
800
		);
801
	}
802
803
	/**
804
	 * Intercept the `jetpack-refresh-blog-token` API request sent to WP.com, and mock the success response.
805
	 *
806
	 * @param bool|array $response The existing response.
807
	 * @param array      $args The request arguments.
808
	 * @param string     $url The request URL.
809
	 *
810
	 * @return array
811
	 */
812 View Code Duplication
	public function intercept_refresh_blog_token_request( $response, $args, $url ) {
813
		if ( false === strpos( $url, 'jetpack-refresh-blog-token' ) ) {
814
			return $response;
815
		}
816
817
		return array(
818
			'headers'  => new Requests_Utility_CaseInsensitiveDictionary( array( 'content-type' => 'application/json' ) ),
819
			'body'     => wp_json_encode( array( 'jetpack_secret' => self::BLOG_TOKEN ) ),
820
			'response' => array(
821
				'code'    => 200,
822
				'message' => 'OK',
823
			),
824
		);
825
	}
826
827
	/**
828
	 * Intercept the `jetpack-refresh-blog-token` API request sent to WP.com, and mock the failure response.
829
	 *
830
	 * @param bool|array $response The existing response.
831
	 * @param array      $args The request arguments.
832
	 * @param string     $url The request URL.
833
	 *
834
	 * @return array
835
	 */
836 View Code Duplication
	public function intercept_refresh_blog_token_request_fail( $response, $args, $url ) {
837
		if ( false === strpos( $url, 'jetpack-refresh-blog-token' ) ) {
838
			return $response;
839
		}
840
841
		return array(
842
			'headers'  => new Requests_Utility_CaseInsensitiveDictionary( array( 'content-type' => 'application/json' ) ),
843
			'body'     => wp_json_encode( array( 'jetpack_secret_missing' => true ) ), // Meaningless body.
844
			'response' => array(
845
				'code'    => 200,
846
				'message' => 'OK',
847
			),
848
		);
849
	}
850
851
	/**
852
	 * Intercept the `jetpack-token-health` API request sent to WP.com, and mock the "invalid blog token" response.
853
	 *
854
	 * @param bool|array $response The existing response.
855
	 * @param array      $args The request arguments.
856
	 * @param string     $url The request URL.
857
	 *
858
	 * @return array
859
	 */
860
	public function intercept_auth_token_request( $response, $args, $url ) {
861
		if ( false === strpos( $url, '/jetpack.token/' ) ) {
862
			return $response;
863
		}
864
865
		return array(
866
			'headers'  => new Requests_Utility_CaseInsensitiveDictionary( array( 'content-type' => 'application/json' ) ),
867
			'body'     => wp_json_encode(
868
				array(
869
					'access_token' => 'mock.token',
870
					'token_type'   => 'X_JETPACK',
871
					'scope'        => ( new Manager() )->sign_role( 'administrator' ),
872
				)
873
			),
874
			'response' => array(
875
				'code'    => 200,
876
				'message' => 'OK',
877
			),
878
		);
879
	}
880
881
	/**
882
	 * Used to simulate a successful response to any XML-RPC request.
883
	 * Should be hooked on the `pre_http_request` filter.
884
	 *
885
	 * @param false  $preempt A preemptive return value of an HTTP request.
0 ignored issues
show
Documentation introduced by
Should the type for parameter $preempt not be boolean?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
886
	 * @param array  $args    HTTP request arguments.
887
	 * @param string $url     The request URL.
888
	 *
889
	 * @return WP_REST_Response
890
	 */
891 View Code Duplication
	public function mock_xmlrpc_success( $preempt, $args, $url ) {
892
		if ( strpos( $url, 'https://jetpack.wordpress.com/xmlrpc.php' ) !== false ) {
893
			$response = array();
894
895
			$response['body'] = '
896
				<methodResponse>
897
					<params>
898
						<param>
899
							<value>1</value>
900
						</param>
901
					</params>
902
				</methodResponse>
903
			';
904
905
			$response['response']['code'] = 200;
906
			return $response;
907
		}
908
909
		return $preempt;
910
	}
911
912
	/**
913
	 * Used to simulate a failed response to any XML-RPC request.
914
	 * Should be hooked on the `pre_http_request` filter.
915
	 *
916
	 * @param false  $preempt A preemptive return value of an HTTP request.
0 ignored issues
show
Documentation introduced by
Should the type for parameter $preempt not be boolean?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
917
	 * @param array  $args    HTTP request arguments.
918
	 * @param string $url     The request URL.
919
	 *
920
	 * @return WP_REST_Response
921
	 */
922 View Code Duplication
	public function mock_xmlrpc_failure( $preempt, $args, $url ) {
923
		if ( strpos( $url, 'https://jetpack.wordpress.com/xmlrpc.php' ) !== false ) {
924
			$response = array();
925
926
			$response['body'] = '';
927
928
			$response['response']['code'] = 500;
929
			return $response;
930
		}
931
932
		return $preempt;
933
	}
934
935
	/**
936
	 * Intercept the `Jetpack_Options` call and mock the values.
937
	 * Site level / user-less connection set-up.
938
	 *
939
	 * @param mixed  $value The current option value.
940
	 * @param string $name Option name.
941
	 *
942
	 * @return mixed
943
	 */
944
	public function mock_jetpack_site_connection_options( $value, $name ) {
945
		switch ( $name ) {
946
			case 'blog_token':
947
				return self::BLOG_TOKEN;
948
			case 'id':
949
				return self::BLOG_ID;
950
		}
951
952
		return $value;
953
	}
954
955
	/**
956
	 * Intercept the `Jetpack_Options` call and mock the values.
957
	 * Full connection set-up.
958
	 *
959
	 * @param mixed  $value The current option value.
960
	 * @param string $name Option name.
961
	 *
962
	 * @return mixed
963
	 */
964
	public function mock_jetpack_options( $value, $name ) {
965
		switch ( $name ) {
966
			case 'blog_token':
967
				return self::BLOG_TOKEN;
968
			case 'id':
969
				return self::BLOG_ID;
970
			case 'master_user':
971
				return self::USER_ID;
972
			case 'user_tokens':
973
				return array(
974
					self::USER_ID            => 'new.usertoken.' . self::USER_ID,
975
					self::$secondary_user_id => 'new2.secondarytoken.' . self::$secondary_user_id,
976
				);
977
		}
978
979
		return $value;
980
	}
981
982
	/**
983
	 * Build the `connection/reconnect` request object.
984
	 *
985
	 * @return WP_REST_Request
986
	 */
987
	private function build_reconnect_request() {
988
		$this->request = new WP_REST_Request( 'POST', '/jetpack/v4/connection/reconnect' );
989
		$this->request->set_header( 'Content-Type', 'application/json' );
990
991
		return $this->request;
992
	}
993
994
	/**
995
	 * Setup the environment to test the reconnection process.
996
	 *
997
	 * @param string|null $invalid_token The invalid token to be returned in the response. Null if the tokens should be valid.
998
	 */
999
	private function setup_reconnect_test( $invalid_token ) {
1000
		switch ( $invalid_token ) {
1001
			case 'blog_token':
1002
				add_filter(
1003
					'pre_http_request',
1004
					array(
1005
						$this,
1006
						'intercept_validate_tokens_request_invalid_blog_token',
1007
					),
1008
					10,
1009
					3
1010
				);
1011
				break;
1012
			case 'user_token':
1013
				add_filter(
1014
					'pre_http_request',
1015
					array(
1016
						$this,
1017
						'intercept_validate_tokens_request_invalid_user_token',
1018
					),
1019
					10,
1020
					3
1021
				);
1022
				break;
1023
			case 'token_validation_failed':
1024
				add_filter(
1025
					'pre_http_request',
1026
					array(
1027
						$this,
1028
						'intercept_validate_tokens_request_failed',
1029
					),
1030
					10,
1031
					3
1032
				);
1033
				break;
1034
			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...
1035
				add_filter(
1036
					'pre_http_request',
1037
					array(
1038
						$this,
1039
						'intercept_validate_tokens_request_valid_tokens',
1040
					),
1041
					10,
1042
					3
1043
				);
1044
				break;
1045
		}
1046
1047
		add_filter( 'jetpack_options', array( $this, 'mock_jetpack_options' ), 10, 2 );
1048
	}
1049
1050
	/**
1051
	 * Restore the environment after the `reconnect` test has been run.
1052
	 *
1053
	 * @param string|null $invalid_token The invalid token to be returned in the response. Null if the tokens should be valid.
1054
	 */
1055
	private function shutdown_reconnect_test( $invalid_token ) {
1056
		switch ( $invalid_token ) {
1057
			case 'blog_token':
1058
				remove_filter(
1059
					'pre_http_request',
1060
					array(
1061
						$this,
1062
						'intercept_validate_tokens_request_invalid_blog_token',
1063
					),
1064
					10
1065
				);
1066
				break;
1067
			case 'user_token':
1068
				remove_filter(
1069
					'pre_http_request',
1070
					array(
1071
						$this,
1072
						'intercept_validate_tokens_request_invalid_user_token',
1073
					),
1074
					10
1075
				);
1076
				break;
1077
			case 'token_validation_failed':
1078
				remove_filter(
1079
					'pre_http_request',
1080
					array(
1081
						$this,
1082
						'intercept_validate_tokens_request_failed',
1083
					),
1084
					10
1085
				);
1086
				break;
1087
			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...
1088
				remove_filter(
1089
					'pre_http_request',
1090
					array(
1091
						$this,
1092
						'intercept_validate_tokens_request_valid_tokens',
1093
					),
1094
					10
1095
				);
1096
				break;
1097
		}
1098
1099
		remove_filter( 'jetpack_options', array( $this, 'mock_jetpack_options' ), 10 );
1100
		remove_filter( 'pre_http_request', array( $this, 'intercept_validate_tokens_request' ), 10 );
1101
	}
1102
1103
}
1104