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 if (remoting.settings.USE_GCD) {
374 expectedConfig['gcd_device_id'] = FAKE_HOST_ID;
376 expectedConfig['host_id'] = FAKE_HOST_ID;
379 startDaemonSpy.args[0].slice(0, 2),
380 [expectedConfig, consent]);
385 // Check what happens when stopDaemon calls onError.
386 // TODO(jrw): Should stopDaemon even have an onError callback?
387 QUnit.test('stop with stopDaemon failure', function(assert) {
388 mockHostDaemonFacade.stopDaemonResult = null;
389 return new Promise(function(resolve, reject) {
390 controller.stop(function() {
391 reject('test failed');
392 }, function(/** remoting.Error */ e) {
393 assert.equal(e.getDetail(), 'stopDaemon');
394 // TODO(jrw): Is it really desirable to leave the host registered?
395 assert.equal(unregisterHostByIdSpy.callCount, 0);
401 // Check what happens when stopDaemon returns FAILED.
402 QUnit.test('stop with stopDaemon cancelled', function(assert) {
403 mockHostDaemonFacade.stopDaemonResult =
404 remoting.HostController.AsyncResult.FAILED;
405 return new Promise(function(resolve, reject) {
406 controller.stop(function() {
407 reject('test failed');
408 }, function(/** remoting.Error */ e) {
409 // TODO(jrw): Is it really desirable to leave the host registered?
410 assert.equal(e.getTag(), remoting.Error.Tag.UNEXPECTED);
411 assert.equal(unregisterHostByIdSpy.callCount, 0);
417 // Check what happens when stopDaemon returns CANCELLED.
418 QUnit.test('stop with stopDaemon cancelled', function(assert) {
419 mockHostDaemonFacade.stopDaemonResult =
420 remoting.HostController.AsyncResult.CANCELLED;
421 return new Promise(function(resolve, reject) {
422 controller.stop(function() {
423 reject('test failed');
424 }, function(/** remoting.Error */ e) {
425 assert.equal(e.getTag(), remoting.Error.Tag.CANCELLED);
426 assert.equal(unregisterHostByIdSpy.callCount, 0);
432 // Check what happens when stopDaemon succeeds.
433 QUnit.test('stop succeeds', function(assert) {
434 sinon.stub(controller, 'getLocalHostId').callsArgWith(0, FAKE_HOST_ID);
435 return new Promise(function(resolve, reject) {
436 controller.stop(function() {
437 assert.equal(unregisterHostByIdSpy.callCount, 1);
438 assert.equal(unregisterHostByIdSpy.args[0][0], FAKE_HOST_ID);
444 // Check what happens when the host reports an invalid config.
445 QUnit.test('updatePin where config is invalid', function(assert) {
446 mockHostDaemonFacade.daemonConfig = {};
447 return new Promise(function(resolve, reject) {
448 controller.updatePin(FAKE_NEW_HOST_PIN, function() {
449 reject('test failed');
450 }, function(/** remoting.Error */ e) {
451 assert.equal(e.getTag(), remoting.Error.Tag.UNEXPECTED);
452 assert.equal(clearPairedClientsSpy.callCount, 0);
458 // Check what happens when getDaemonConfig calls onError.
459 QUnit.test('updatePin where getDaemonConfig fails', function(assert) {
460 mockHostDaemonFacade.daemonConfig = null;
461 return new Promise(function(resolve, reject) {
462 controller.updatePin(FAKE_NEW_HOST_PIN, function() {
463 reject('test failed');
464 }, function(/** remoting.Error */ e) {
465 assert.equal(e.getDetail(), 'getDaemonConfig');
466 assert.equal(e.getTag(), remoting.Error.Tag.UNEXPECTED);
467 assert.equal(clearPairedClientsSpy.callCount, 0);
473 // Check what happens when updateDaemonConfig calls onError.
474 // TODO(jrw): Should updateDaemonConfig even have an onError callback?
475 QUnit.test('updatePin where updateDaemonConfig calls onError', function(
477 mockHostDaemonFacade.updateDaemonConfigResult = null;
478 return new Promise(function(resolve, reject) {
479 controller.updatePin(FAKE_NEW_HOST_PIN, function() {
480 reject('test failed');
481 }, function(/** remoting.Error */ e) {
482 assert.equal(e.getDetail(), 'updateDaemonConfig');
483 assert.equal(e.getTag(), remoting.Error.Tag.UNEXPECTED);
484 assert.equal(clearPairedClientsSpy.callCount, 0);
490 // Check what happens when updateDaemonConfig returns CANCELLED.
491 QUnit.test('updatePin where updateDaemonConfig is cancelled', function(
493 mockHostDaemonFacade.updateDaemonConfigResult =
494 remoting.HostController.AsyncResult.CANCELLED;
495 return new Promise(function(resolve, reject) {
496 controller.updatePin(FAKE_NEW_HOST_PIN, function() {
497 reject('test failed');
498 }, function(/** remoting.Error */ e) {
499 assert.equal(e.getTag(), remoting.Error.Tag.CANCELLED);
500 assert.equal(clearPairedClientsSpy.callCount, 0);
506 // Check what happens when updateDaemonConfig returns FAILED.
507 QUnit.test('updatePin where updateDaemonConfig is returns failure', function(
509 mockHostDaemonFacade.updateDaemonConfigResult =
510 remoting.HostController.AsyncResult.FAILED;
511 return new Promise(function(resolve, reject) {
512 controller.updatePin(FAKE_NEW_HOST_PIN, function() {
513 reject('test failed');
514 }, function(/** remoting.Error */ e) {
515 assert.equal(e.getTag(), remoting.Error.Tag.UNEXPECTED);
516 assert.equal(clearPairedClientsSpy.callCount, 0);
522 // Check what happens when updatePin succeeds.
523 QUnit.test('updatePin succeeds', function(assert) {
524 mockHostDaemonFacade.pairedClients = [];
526 var fakePinHash = fakePinHashFunc(FAKE_HOST_ID, FAKE_NEW_HOST_PIN);
527 return new Promise(function(resolve, reject) {
528 controller.updatePin(FAKE_NEW_HOST_PIN, function() {
529 assert.equal(getPinHashSpy.callCount, 1);
530 assert.equal(getPinHashSpy.args[0][0], FAKE_HOST_ID);
531 assert.equal(getPinHashSpy.args[0][1], FAKE_NEW_HOST_PIN);
532 assert.equal(updateDaemonConfigSpy.callCount, 1);
534 updateDaemonConfigSpy.args[0][0], {
535 host_secret_hash: fakePinHash
537 assert.equal(clearPairedClientsSpy.callCount, 1);
543 // Check what happens when getLocalHostState fails.
544 QUnit.test('getLocalHostState with error', function(assert) {
545 mockHostDaemonFacade.daemonState = null;
546 return new Promise(function(resolve, reject) {
547 controller.getLocalHostState(function(
548 /** remoting.HostController.State */ state) {
549 assert.equal(state, remoting.HostController.State.UNKNOWN);
555 // Check what happens when getLocalHostState reports no plugin.
556 QUnit.test('getLocalHostState with no plugin', function(assert) {
557 sinon.stub(mockHostDaemonFacade, 'getDaemonState').returns(
558 Promise.reject(new remoting.Error(remoting.Error.Tag.MISSING_PLUGIN)));
559 return new Promise(function(resolve, reject) {
560 controller.getLocalHostState(function(
561 /** remoting.HostController.State */ state) {
562 assert.equal(state, remoting.HostController.State.NOT_INSTALLED);
568 // Check what happens when getLocalHostState succeeds.
569 QUnit.test('getLocalHostState succeeds', function(assert) {
570 return new Promise(function(resolve, reject) {
571 controller.getLocalHostState(function(
572 /** remoting.HostController.State */ state) {
573 assert.equal(state, remoting.HostController.State.STARTED);
579 // Check what happens to getLocalHostId when getDaemonConfig
580 // returns an invalid config.
581 QUnit.test('getLocalHostId with invalid daemon config', function(assert) {
582 mockHostDaemonFacade.daemonConfig = {};
583 return new Promise(function(resolve, reject) {
584 controller.getLocalHostId(function(/** ?string */ id) {
585 assert.strictEqual(id, null);
591 // Check what happens to getLocalHostId when getDaemonConfig fails.
592 QUnit.test('getLocalHostId with getDaemonConfig failure', function(assert) {
593 mockHostDaemonFacade.daemonConfig = null;
594 return new Promise(function(resolve, reject) {
595 controller.getLocalHostId(function(/** ?string */ id) {
596 assert.strictEqual(id, null);
602 // Check what happens when getLocalHostId succeeds.
603 QUnit.test('getLocalHostId succeeds', function(assert) {
604 return new Promise(function(resolve, reject) {
605 controller.getLocalHostId(function(/** ?string */ id) {
606 assert.equal(id, FAKE_HOST_ID);
612 // Tests omitted for hasFeature, getPairedClients, deletePairedClient,
613 // and clearPairedClients because they simply call through to