1 // Copyright 2015 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
7 * Unit tests for host_controller.js.
14 /** @type {remoting.HostController} */
17 /** @type {sinon.Mock} */
18 var hostListMock = null;
20 /** @type {sinon.TestStub} */
23 /** @type {remoting.MockHostDaemonFacade} */
24 var mockHostDaemonFacade;
26 /** @type {sinon.TestStub} */
27 var hostDaemonFacadeCtorStub;
29 /** @type {remoting.MockSignalStrategy} */
30 var mockSignalStrategy;
32 /** @type {sinon.TestStub} */
33 var signalStrategyCreateStub;
35 /** @type {sinon.TestStub|Function} */
36 var signalStrategyConnectStub;
38 var FAKE_HOST_PIN = '<FAKE_HOST_PIN>';
39 var FAKE_PIN_HASH = '<FAKE_PIN_HASH>';
40 var FAKE_NEW_HOST_PIN = '<FAKE_NEW_HOST_PIN>';
41 var FAKE_USER_EMAIL = '<FAKE_USER_EMAIL>';
42 var FAKE_XMPP_LOGIN = '<FAKE_XMPP_LOGIN>';
43 var FAKE_USER_NAME = '<FAKE_USER_NAME>';
44 var FAKE_HOST_ID = '0bad0bad-0bad-0bad-0bad-0bad0bad0bad';
45 var FAKE_DAEMON_VERSION = '1.2.3.4';
46 var FAKE_HOST_NAME = '<FAKE_HOST_NAME>';
47 var FAKE_PUBLIC_KEY = '<FAKE_PUBLIC_KEY>';
48 var FAKE_PRIVATE_KEY = '<FAKE_PRIVATE_KEY>';
49 var FAKE_AUTH_CODE = '<FAKE_AUTH_CODE>';
50 var FAKE_REFRESH_TOKEN = '<FAKE_REFRESH_TOKEN>';
51 var FAKE_HOST_CLIENT_ID = '<FAKE_HOST_CLIENT_ID>';
52 var FAKE_CLIENT_JID = '<FAKE_CLIENT_JID>';
53 var FAKE_CLIENT_BASE_JID = '<FAKE_CLIENT_BASE_JID>';
54 var FAKE_IDENTITY_TOKEN = '<FAKE_IDENTITY_TOKEN>';
56 /** @type {sinon.Spy|Function} */
57 var getCredentialsFromAuthCodeSpy;
59 /** @type {sinon.Spy|Function} */
62 /** @type {sinon.Spy|Function} */
65 /** @type {sinon.Spy|Function} */
66 var updateDaemonConfigSpy;
68 /** @type {sinon.Spy|Function} */
69 var clearPairedClientsSpy;
71 /** @type {sinon.Spy} */
72 var unregisterHostByIdSpy;
74 /** @type {sinon.Spy} */
75 var onLocalHostStartedSpy;
77 /** @type {remoting.MockHostListApi} */
80 QUnit.module('host_controller', {
81 beforeEach: function(/** QUnit.Assert */ assert) {
82 chromeMocks.identity.mock$setToken(FAKE_IDENTITY_TOKEN);
83 remoting.settings = new remoting.Settings();
84 remoting.identity = new remoting.Identity();
85 mockHostListApi = new remoting.MockHostListApi;
86 mockHostListApi.authCodeFromRegister = FAKE_AUTH_CODE;
87 mockHostListApi.emailFromRegister = '';
88 mockHostListApi.hostIdFromRegister = FAKE_HOST_ID;
89 remoting.HostListApi.setInstance(mockHostListApi);
90 console.assert(remoting.oauth2 === null, '|oauth2| already exists.');
91 remoting.oauth2 = new remoting.OAuth2();
92 console.assert(remoting.hostList === null, '|hostList| already exists.');
93 remoting.hostList = /** @type {remoting.HostList} */
94 (Object.create(remoting.HostList.prototype));
96 // When the HostList's unregisterHostById method is called, make
97 // sure the argument is correct.
98 unregisterHostByIdSpy =
99 sinon.stub(remoting.hostList, 'unregisterHostById', function(
100 /** string */ hostId, /** Function */ onDone) {
101 assert.equal(hostId, FAKE_HOST_ID);
107 // When the HostList's onLocalHostStarted method is called, make
108 // sure the arguments are correct.
109 onLocalHostStartedSpy =
111 remoting.hostList, 'onLocalHostStarted', function(
112 /** string */ hostName,
113 /** string */ newHostId,
114 /** string */ publicKey) {
115 assert.equal(hostName, FAKE_HOST_NAME);
116 assert.equal(newHostId, FAKE_HOST_ID);
117 assert.equal(publicKey, FAKE_PUBLIC_KEY);
120 mockSignalStrategy = new remoting.MockSignalStrategy(
121 FAKE_CLIENT_JID + '/extra_junk',
122 remoting.SignalStrategy.Type.XMPP);
123 signalStrategyCreateStub = sinon.stub(remoting.SignalStrategy, 'create');
124 signalStrategyCreateStub.returns(mockSignalStrategy);
126 hostDaemonFacadeCtorStub = sinon.stub(remoting, 'HostDaemonFacade');
127 mockHostDaemonFacade = new remoting.MockHostDaemonFacade();
128 hostDaemonFacadeCtorStub.returns(mockHostDaemonFacade);
129 generateUuidStub = sinon.stub(base, 'generateUuid');
130 generateUuidStub.returns(FAKE_HOST_ID);
131 getCredentialsFromAuthCodeSpy = sinon.spy(
132 mockHostDaemonFacade, 'getCredentialsFromAuthCode');
133 getPinHashSpy = sinon.spy(mockHostDaemonFacade, 'getPinHash');
134 startDaemonSpy = sinon.spy(mockHostDaemonFacade, 'startDaemon');
135 updateDaemonConfigSpy =
136 sinon.spy(mockHostDaemonFacade, 'updateDaemonConfig');
137 clearPairedClientsSpy =
138 sinon.spy(mockHostDaemonFacade, 'clearPairedClients');
140 // Set up successful responses from mockHostDaemonFacade.
141 // Individual tests override these values to create errors.
142 mockHostDaemonFacade.features =
143 [remoting.HostController.Feature.OAUTH_CLIENT];
144 mockHostDaemonFacade.daemonVersion = FAKE_DAEMON_VERSION;
145 mockHostDaemonFacade.hostName = FAKE_HOST_NAME;
146 mockHostDaemonFacade.privateKey = FAKE_PRIVATE_KEY;
147 mockHostDaemonFacade.publicKey = FAKE_PUBLIC_KEY;
148 mockHostDaemonFacade.hostClientId = FAKE_HOST_CLIENT_ID;
149 mockHostDaemonFacade.userEmail = FAKE_XMPP_LOGIN;
150 mockHostDaemonFacade.refreshToken = FAKE_REFRESH_TOKEN;
151 mockHostDaemonFacade.pinHashFunc = fakePinHashFunc;
152 mockHostDaemonFacade.startDaemonResult =
153 remoting.HostController.AsyncResult.OK;
154 mockHostDaemonFacade.stopDaemonResult =
155 remoting.HostController.AsyncResult.OK;
156 mockHostDaemonFacade.daemonConfig = {
157 host_id: FAKE_HOST_ID,
158 xmpp_login: FAKE_XMPP_LOGIN
160 mockHostDaemonFacade.updateDaemonConfigResult =
161 remoting.HostController.AsyncResult.OK;
162 mockHostDaemonFacade.daemonState =
163 remoting.HostController.State.STARTED;
165 sinon.stub(remoting.identity, 'getEmail').returns(
166 Promise.resolve(FAKE_USER_EMAIL));
167 sinon.stub(remoting.oauth2, 'getRefreshToken').returns(
170 controller = new remoting.HostController();
173 afterEach: function() {
175 getCredentialsFromAuthCodeSpy.restore();
176 generateUuidStub.restore();
177 hostDaemonFacadeCtorStub.restore();
178 signalStrategyCreateStub.restore();
179 remoting.hostList = null;
180 remoting.oauth2 = null;
181 remoting.HostListApi.setInstance(null);
182 remoting.identity = null;
187 * @param {string} hostId
188 * @param {string} pin
191 function fakePinHashFunc(hostId, pin) {
192 return '<FAKE_PIN:' + hostId + ':' + pin + '>';
196 * @param {boolean} successful
198 function stubSignalStrategyConnect(successful) {
199 sinon.stub(mockSignalStrategy, 'connect', function() {
200 Promise.resolve().then(function() {
201 mockSignalStrategy.setStateForTesting(
203 remoting.SignalStrategy.State.CONNECTED :
204 remoting.SignalStrategy.State.FAILED);
209 // Check what happens when the HostDaemonFacade's getHostName method
211 QUnit.test('start with getHostName failure', function(assert) {
212 mockHostDaemonFacade.hostName = null;
213 return controller.start(FAKE_HOST_PIN, true).then(function() {
215 }, function(/** remoting.Error */ e) {
216 assert.equal(e.getDetail(), 'getHostName');
217 assert.equal(e.getTag(), remoting.Error.Tag.UNEXPECTED);
218 assert.equal(unregisterHostByIdSpy.callCount, 0);
219 assert.equal(onLocalHostStartedSpy.callCount, 0);
220 assert.equal(startDaemonSpy.callCount, 0);
224 // Check what happens when the HostDaemonFacade's generateKeyPair
226 QUnit.test('start with generateKeyPair failure', function(assert) {
227 mockHostDaemonFacade.publicKey = null;
228 mockHostDaemonFacade.privateKey = null;
229 return controller.start(FAKE_HOST_PIN, true).then(function() {
231 }, function(/** remoting.Error */ e) {
232 assert.equal(e.getDetail(), 'generateKeyPair');
233 assert.equal(e.getTag(), remoting.Error.Tag.UNEXPECTED);
234 assert.equal(unregisterHostByIdSpy.callCount, 0);
235 assert.equal(onLocalHostStartedSpy.callCount, 0);
236 assert.equal(startDaemonSpy.callCount, 0);
240 // Check what happens when the HostDaemonFacade's getHostClientId
242 QUnit.test('start with getHostClientId failure', function(assert) {
243 mockHostDaemonFacade.hostClientId = null;
244 return controller.start(FAKE_HOST_PIN, true).then(function() {
246 }, function(/** remoting.Error */ e) {
247 assert.equal(e.getDetail(), 'getHostClientId');
248 assert.equal(e.getTag(), remoting.Error.Tag.UNEXPECTED);
249 assert.equal(unregisterHostByIdSpy.callCount, 0);
250 assert.equal(onLocalHostStartedSpy.callCount, 0);
251 assert.equal(startDaemonSpy.callCount, 0);
255 // Check what happens when the registry returns an HTTP when we try to
257 QUnit.test('start with host registration failure', function(assert) {
258 mockHostListApi.authCodeFromRegister = null;
259 return controller.start(FAKE_HOST_PIN, true).then(function() {
261 }, function(/** remoting.Error */ e) {
262 assert.equal(e.getTag(), remoting.Error.Tag.REGISTRATION_FAILED);
263 assert.equal(unregisterHostByIdSpy.callCount, 0);
264 assert.equal(onLocalHostStartedSpy.callCount, 0);
265 assert.equal(startDaemonSpy.callCount, 0);
269 // Check what happens when the HostDaemonFacade's
270 // getCredentialsFromAuthCode method fails.
271 QUnit.test('start with getCredentialsFromAuthCode failure', function(assert) {
272 mockHostDaemonFacade.useEmail = null;
273 mockHostDaemonFacade.refreshToken = null;
274 return controller.start(FAKE_HOST_PIN, true).then(function(result) {
276 }, function(/** remoting.Error */ e) {
277 assert.equal(e.getDetail(), 'getCredentialsFromAuthCode');
278 assert.equal(e.getTag(), remoting.Error.Tag.UNEXPECTED);
279 assert.equal(getCredentialsFromAuthCodeSpy.callCount, 1);
280 assert.equal(onLocalHostStartedSpy.callCount, 0);
281 assert.equal(startDaemonSpy.callCount, 0);
285 // Check what happens when the SignalStrategy fails to connect.
286 QUnit.test('start with signalStrategy failure', function(assert) {
287 stubSignalStrategyConnect(false);
288 return controller.start(FAKE_HOST_PIN, true).then(function() {
290 }, function(/** remoting.Error */ e) {
291 assert.equal(e.getDetail(), 'setStateForTesting');
292 assert.equal(e.getTag(), remoting.Error.Tag.UNEXPECTED);
293 assert.equal(unregisterHostByIdSpy.callCount, 1);
297 // Check what happens when the HostDaemonFacade's startDaemon method
298 // fails and calls its onError argument.
299 // TODO(jrw): Should startDaemon even have an onError callback?
300 QUnit.test('start with startDaemon failure', function(assert) {
301 stubSignalStrategyConnect(true);
302 mockHostDaemonFacade.startDaemonResult = null;
303 return controller.start(FAKE_HOST_PIN, true).then(function() {
305 }, function(/** remoting.Error */ e) {
306 assert.equal(e.getDetail(), 'startDaemon');
307 assert.equal(e.getTag(), remoting.Error.Tag.UNEXPECTED);
308 assert.equal(unregisterHostByIdSpy.callCount, 1);
309 assert.equal(unregisterHostByIdSpy.args[0][0], FAKE_HOST_ID);
310 assert.equal(onLocalHostStartedSpy.callCount, 0);
314 // Check what happens when the HostDaemonFacade's startDaemon method
315 // calls is onDone method with a CANCELLED error code.
316 QUnit.test('start with startDaemon cancelled', function(assert) {
317 stubSignalStrategyConnect(true);
318 mockHostDaemonFacade.startDaemonResult =
319 remoting.HostController.AsyncResult.CANCELLED;
320 return controller.start(FAKE_HOST_PIN, true).then(function() {
322 }, function(/** remoting.Error */ e) {
323 assert.equal(e.getTag(), remoting.Error.Tag.CANCELLED);
324 assert.equal(unregisterHostByIdSpy.callCount, 1);
325 assert.equal(unregisterHostByIdSpy.args[0][0], FAKE_HOST_ID);
326 assert.equal(onLocalHostStartedSpy.callCount, 0);
330 // Check what happens when the HostDaemonFacade's startDaemon method
331 // calls is onDone method with an async error code.
332 QUnit.test('start with startDaemon returning failure code', function(assert) {
333 stubSignalStrategyConnect(true);
334 mockHostDaemonFacade.startDaemonResult =
335 remoting.HostController.AsyncResult.FAILED;
336 return controller.start(FAKE_HOST_PIN, true).then(function() {
338 }, function(/** remoting.Error */ e) {
339 assert.equal(e.getTag(), remoting.Error.Tag.UNEXPECTED);
340 assert.equal(unregisterHostByIdSpy.callCount, 1);
341 assert.equal(onLocalHostStartedSpy.callCount, 0);
345 // Check what happens when the entire host registration process
347 [false, true].forEach(function(/** boolean */ consent) {
348 QUnit.test('start with consent=' + consent, function(assert) {
350 var fakePinHash = fakePinHashFunc(FAKE_HOST_ID, FAKE_HOST_PIN);
351 stubSignalStrategyConnect(true);
352 return controller.start(FAKE_HOST_PIN, consent).then(function() {
353 assert.equal(getCredentialsFromAuthCodeSpy.callCount, 1);
355 getCredentialsFromAuthCodeSpy.args[0][0],
357 assert.equal(getPinHashSpy.callCount, 1);
359 getPinHashSpy.args[0].slice(0, 2),
360 [FAKE_HOST_ID, FAKE_HOST_PIN]);
361 assert.equal(unregisterHostByIdSpy.callCount, 0);
362 assert.equal(onLocalHostStartedSpy.callCount, 1);
363 assert.equal(startDaemonSpy.callCount, 1);
364 var expectedConfig = {
365 xmpp_login: FAKE_XMPP_LOGIN,
366 oauth_refresh_token: FAKE_REFRESH_TOKEN,
367 host_owner: FAKE_CLIENT_JID.toLowerCase(),
368 host_owner_email: FAKE_USER_EMAIL,
369 host_name: FAKE_HOST_NAME,
370 host_secret_hash: fakePinHash,
371 private_key: FAKE_PRIVATE_KEY
373 expectedConfig['host_id'] = FAKE_HOST_ID;
375 startDaemonSpy.args[0].slice(0, 2),
376 [expectedConfig, consent]);
381 // Check what happens when stopDaemon calls onError.
382 // TODO(jrw): Should stopDaemon even have an onError callback?
383 QUnit.test('stop with stopDaemon failure', function(assert) {
384 mockHostDaemonFacade.stopDaemonResult = null;
385 return new Promise(function(resolve, reject) {
386 controller.stop(function() {
387 reject('test failed');
388 }, function(/** remoting.Error */ e) {
389 assert.equal(e.getDetail(), 'stopDaemon');
390 // TODO(jrw): Is it really desirable to leave the host registered?
391 assert.equal(unregisterHostByIdSpy.callCount, 0);
397 // Check what happens when stopDaemon returns FAILED.
398 QUnit.test('stop with stopDaemon cancelled', function(assert) {
399 mockHostDaemonFacade.stopDaemonResult =
400 remoting.HostController.AsyncResult.FAILED;
401 return new Promise(function(resolve, reject) {
402 controller.stop(function() {
403 reject('test failed');
404 }, function(/** remoting.Error */ e) {
405 // TODO(jrw): Is it really desirable to leave the host registered?
406 assert.equal(e.getTag(), remoting.Error.Tag.UNEXPECTED);
407 assert.equal(unregisterHostByIdSpy.callCount, 0);
413 // Check what happens when stopDaemon returns CANCELLED.
414 QUnit.test('stop with stopDaemon cancelled', function(assert) {
415 mockHostDaemonFacade.stopDaemonResult =
416 remoting.HostController.AsyncResult.CANCELLED;
417 return new Promise(function(resolve, reject) {
418 controller.stop(function() {
419 reject('test failed');
420 }, function(/** remoting.Error */ e) {
421 assert.equal(e.getTag(), remoting.Error.Tag.CANCELLED);
422 assert.equal(unregisterHostByIdSpy.callCount, 0);
428 // Check what happens when stopDaemon succeeds.
429 QUnit.test('stop succeeds', function(assert) {
430 sinon.stub(controller, 'getLocalHostId').callsArgWith(0, FAKE_HOST_ID);
431 return new Promise(function(resolve, reject) {
432 controller.stop(function() {
433 assert.equal(unregisterHostByIdSpy.callCount, 1);
434 assert.equal(unregisterHostByIdSpy.args[0][0], FAKE_HOST_ID);
440 // Check what happens when the host reports an invalid config.
441 QUnit.test('updatePin where config is invalid', function(assert) {
442 mockHostDaemonFacade.daemonConfig = {};
443 return new Promise(function(resolve, reject) {
444 controller.updatePin(FAKE_NEW_HOST_PIN, function() {
445 reject('test failed');
446 }, function(/** remoting.Error */ e) {
447 assert.equal(e.getTag(), remoting.Error.Tag.UNEXPECTED);
448 assert.equal(clearPairedClientsSpy.callCount, 0);
454 // Check what happens when getDaemonConfig calls onError.
455 QUnit.test('updatePin where getDaemonConfig fails', function(assert) {
456 mockHostDaemonFacade.daemonConfig = null;
457 return new Promise(function(resolve, reject) {
458 controller.updatePin(FAKE_NEW_HOST_PIN, function() {
459 reject('test failed');
460 }, function(/** remoting.Error */ e) {
461 assert.equal(e.getDetail(), 'getDaemonConfig');
462 assert.equal(e.getTag(), remoting.Error.Tag.UNEXPECTED);
463 assert.equal(clearPairedClientsSpy.callCount, 0);
469 // Check what happens when updateDaemonConfig calls onError.
470 // TODO(jrw): Should updateDaemonConfig even have an onError callback?
471 QUnit.test('updatePin where updateDaemonConfig calls onError', function(
473 mockHostDaemonFacade.updateDaemonConfigResult = null;
474 return new Promise(function(resolve, reject) {
475 controller.updatePin(FAKE_NEW_HOST_PIN, function() {
476 reject('test failed');
477 }, function(/** remoting.Error */ e) {
478 assert.equal(e.getDetail(), 'updateDaemonConfig');
479 assert.equal(e.getTag(), remoting.Error.Tag.UNEXPECTED);
480 assert.equal(clearPairedClientsSpy.callCount, 0);
486 // Check what happens when updateDaemonConfig returns CANCELLED.
487 QUnit.test('updatePin where updateDaemonConfig is cancelled', function(
489 mockHostDaemonFacade.updateDaemonConfigResult =
490 remoting.HostController.AsyncResult.CANCELLED;
491 return new Promise(function(resolve, reject) {
492 controller.updatePin(FAKE_NEW_HOST_PIN, function() {
493 reject('test failed');
494 }, function(/** remoting.Error */ e) {
495 assert.equal(e.getTag(), remoting.Error.Tag.CANCELLED);
496 assert.equal(clearPairedClientsSpy.callCount, 0);
502 // Check what happens when updateDaemonConfig returns FAILED.
503 QUnit.test('updatePin where updateDaemonConfig is returns failure', function(
505 mockHostDaemonFacade.updateDaemonConfigResult =
506 remoting.HostController.AsyncResult.FAILED;
507 return new Promise(function(resolve, reject) {
508 controller.updatePin(FAKE_NEW_HOST_PIN, function() {
509 reject('test failed');
510 }, function(/** remoting.Error */ e) {
511 assert.equal(e.getTag(), remoting.Error.Tag.UNEXPECTED);
512 assert.equal(clearPairedClientsSpy.callCount, 0);
518 // Check what happens when updatePin succeeds.
519 QUnit.test('updatePin succeeds', function(assert) {
520 mockHostDaemonFacade.pairedClients = [];
522 var fakePinHash = fakePinHashFunc(FAKE_HOST_ID, FAKE_NEW_HOST_PIN);
523 return new Promise(function(resolve, reject) {
524 controller.updatePin(FAKE_NEW_HOST_PIN, function() {
525 assert.equal(getPinHashSpy.callCount, 1);
526 assert.equal(getPinHashSpy.args[0][0], FAKE_HOST_ID);
527 assert.equal(getPinHashSpy.args[0][1], FAKE_NEW_HOST_PIN);
528 assert.equal(updateDaemonConfigSpy.callCount, 1);
530 updateDaemonConfigSpy.args[0][0], {
531 host_secret_hash: fakePinHash
533 assert.equal(clearPairedClientsSpy.callCount, 1);
539 // Check what happens when getLocalHostState fails.
540 QUnit.test('getLocalHostState with error', function(assert) {
541 mockHostDaemonFacade.daemonState = null;
542 return new Promise(function(resolve, reject) {
543 controller.getLocalHostState(function(
544 /** remoting.HostController.State */ state) {
545 assert.equal(state, remoting.HostController.State.UNKNOWN);
551 // Check what happens when getLocalHostState reports no plugin.
552 QUnit.test('getLocalHostState with no plugin', function(assert) {
553 sinon.stub(mockHostDaemonFacade, 'getDaemonState').returns(
554 Promise.reject(new remoting.Error(remoting.Error.Tag.MISSING_PLUGIN)));
555 return new Promise(function(resolve, reject) {
556 controller.getLocalHostState(function(
557 /** remoting.HostController.State */ state) {
558 assert.equal(state, remoting.HostController.State.NOT_INSTALLED);
564 // Check what happens when getLocalHostState succeeds.
565 QUnit.test('getLocalHostState succeeds', function(assert) {
566 return new Promise(function(resolve, reject) {
567 controller.getLocalHostState(function(
568 /** remoting.HostController.State */ state) {
569 assert.equal(state, remoting.HostController.State.STARTED);
575 // Check what happens to getLocalHostId when getDaemonConfig
576 // returns an invalid config.
577 QUnit.test('getLocalHostId with invalid daemon config', function(assert) {
578 mockHostDaemonFacade.daemonConfig = {};
579 return new Promise(function(resolve, reject) {
580 controller.getLocalHostId(function(/** ?string */ id) {
581 assert.strictEqual(id, null);
587 // Check what happens to getLocalHostId when getDaemonConfig fails.
588 QUnit.test('getLocalHostId with getDaemonConfig failure', function(assert) {
589 mockHostDaemonFacade.daemonConfig = null;
590 return new Promise(function(resolve, reject) {
591 controller.getLocalHostId(function(/** ?string */ id) {
592 assert.strictEqual(id, null);
598 // Check what happens when getLocalHostId succeeds.
599 QUnit.test('getLocalHostId succeeds', function(assert) {
600 return new Promise(function(resolve, reject) {
601 controller.getLocalHostId(function(/** ?string */ id) {
602 assert.equal(id, FAKE_HOST_ID);
608 // Tests omitted for hasFeature, getPairedClients, deletePairedClient,
609 // and clearPairedClients because they simply call through to