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} */
69 var unregisterHostByIdSpy;
71 /** @type {sinon.Spy} */
72 var onLocalHostStartedSpy;
74 /** @type {remoting.MockHostListApi} */
77 QUnit.module('host_controller', {
78 beforeEach: function(/** QUnit.Assert */ assert) {
79 chromeMocks.identity.mock$setToken(FAKE_IDENTITY_TOKEN);
80 remoting.settings = new remoting.Settings();
81 remoting.identity = new remoting.Identity();
82 mockHostListApi = new remoting.MockHostListApi;
83 mockHostListApi.authCodeFromRegister = FAKE_AUTH_CODE;
84 mockHostListApi.emailFromRegister = '';
85 remoting.HostListApi.setInstance(mockHostListApi);
86 base.debug.assert(remoting.oauth2 === null);
87 remoting.oauth2 = new remoting.OAuth2();
88 base.debug.assert(remoting.hostList === null);
89 remoting.hostList = /** @type {remoting.HostList} */
90 (Object.create(remoting.HostList.prototype));
92 // When the HostList's unregisterHostById method is called, make
93 // sure the argument is correct.
94 unregisterHostByIdSpy =
95 sinon.stub(remoting.hostList, 'unregisterHostById', function(
96 /** string */ hostId, /** Function */ onDone) {
97 assert.equal(hostId, FAKE_HOST_ID);
103 // When the HostList's onLocalHostStarted method is called, make
104 // sure the arguments are correct.
105 onLocalHostStartedSpy =
107 remoting.hostList, 'onLocalHostStarted', function(
108 /** string */ hostName,
109 /** string */ newHostId,
110 /** string */ publicKey) {
111 assert.equal(hostName, FAKE_HOST_NAME);
112 assert.equal(newHostId, FAKE_HOST_ID);
113 assert.equal(publicKey, FAKE_PUBLIC_KEY);
116 mockSignalStrategy = new remoting.MockSignalStrategy(
117 FAKE_CLIENT_JID + '/extra_junk',
118 remoting.SignalStrategy.Type.XMPP);
119 signalStrategyCreateStub = sinon.stub(remoting.SignalStrategy, 'create');
120 signalStrategyCreateStub.returns(mockSignalStrategy);
122 hostDaemonFacadeCtorStub = sinon.stub(remoting, 'HostDaemonFacade');
123 mockHostDaemonFacade = new remoting.MockHostDaemonFacade();
124 hostDaemonFacadeCtorStub.returns(mockHostDaemonFacade);
125 generateUuidStub = sinon.stub(base, 'generateUuid');
126 generateUuidStub.returns(FAKE_HOST_ID);
127 getCredentialsFromAuthCodeSpy = sinon.spy(
128 mockHostDaemonFacade, 'getCredentialsFromAuthCode');
129 getPinHashSpy = sinon.spy(mockHostDaemonFacade, 'getPinHash');
130 startDaemonSpy = sinon.spy(mockHostDaemonFacade, 'startDaemon');
131 updateDaemonConfigSpy =
132 sinon.spy(mockHostDaemonFacade, 'updateDaemonConfig');
134 // Set up successful responses from mockHostDaemonFacade.
135 // Individual tests override these values to create errors.
136 mockHostDaemonFacade.features =
137 [remoting.HostController.Feature.OAUTH_CLIENT];
138 mockHostDaemonFacade.daemonVersion = FAKE_DAEMON_VERSION;
139 mockHostDaemonFacade.hostName = FAKE_HOST_NAME;
140 mockHostDaemonFacade.privateKey = FAKE_PRIVATE_KEY;
141 mockHostDaemonFacade.publicKey = FAKE_PUBLIC_KEY;
142 mockHostDaemonFacade.hostClientId = FAKE_HOST_CLIENT_ID;
143 mockHostDaemonFacade.userEmail = FAKE_XMPP_LOGIN;
144 mockHostDaemonFacade.refreshToken = FAKE_REFRESH_TOKEN;
145 mockHostDaemonFacade.pinHashFunc = fakePinHashFunc;
146 mockHostDaemonFacade.startDaemonResult =
147 remoting.HostController.AsyncResult.OK;
148 mockHostDaemonFacade.stopDaemonResult =
149 remoting.HostController.AsyncResult.OK;
150 mockHostDaemonFacade.daemonConfig = {
151 host_id: FAKE_HOST_ID,
152 xmpp_login: FAKE_XMPP_LOGIN
154 mockHostDaemonFacade.updateDaemonConfigResult =
155 remoting.HostController.AsyncResult.OK;
156 mockHostDaemonFacade.daemonState =
157 remoting.HostController.State.STARTED;
159 sinon.stub(remoting.identity, 'getEmail').returns(
160 Promise.resolve(FAKE_USER_EMAIL));
161 sinon.stub(remoting.oauth2, 'getRefreshToken').returns(
164 controller = new remoting.HostController();
167 afterEach: function() {
169 getCredentialsFromAuthCodeSpy.restore();
170 generateUuidStub.restore();
171 hostDaemonFacadeCtorStub.restore();
172 signalStrategyCreateStub.restore();
173 remoting.hostList = null;
174 remoting.oauth2 = null;
175 remoting.HostListApi.setInstance(null);
176 remoting.identity = null;
181 * @param {string} hostId
182 * @param {string} pin
185 function fakePinHashFunc(hostId, pin) {
186 return '<FAKE_PIN:' + hostId + ':' + pin + '>';
190 * @param {boolean} successful
192 function stubSignalStrategyConnect(successful) {
193 sinon.stub(mockSignalStrategy, 'connect', function() {
194 Promise.resolve().then(function() {
195 mockSignalStrategy.setStateForTesting(
197 remoting.SignalStrategy.State.CONNECTED :
198 remoting.SignalStrategy.State.FAILED);
203 // Check what happens when the HostDaemonFacade's getHostName method
205 QUnit.test('start with getHostName failure', function(assert) {
206 mockHostDaemonFacade.hostName = null;
207 return controller.start(FAKE_HOST_PIN, true).then(function() {
209 }, function(/** remoting.Error */ e) {
210 assert.equal(e.getDetail(), 'getHostName');
211 assert.equal(e.getTag(), remoting.Error.Tag.UNEXPECTED);
212 assert.equal(unregisterHostByIdSpy.callCount, 0);
213 assert.equal(onLocalHostStartedSpy.callCount, 0);
214 assert.equal(startDaemonSpy.callCount, 0);
218 // Check what happens when the HostDaemonFacade's generateKeyPair
220 QUnit.test('start with generateKeyPair failure', function(assert) {
221 mockHostDaemonFacade.publicKey = null;
222 mockHostDaemonFacade.privateKey = null;
223 return controller.start(FAKE_HOST_PIN, true).then(function() {
225 }, function(/** remoting.Error */ e) {
226 assert.equal(e.getDetail(), 'generateKeyPair');
227 assert.equal(e.getTag(), remoting.Error.Tag.UNEXPECTED);
228 assert.equal(unregisterHostByIdSpy.callCount, 0);
229 assert.equal(onLocalHostStartedSpy.callCount, 0);
230 assert.equal(startDaemonSpy.callCount, 0);
234 // Check what happens when the HostDaemonFacade's getHostClientId
236 QUnit.test('start with getHostClientId failure', function(assert) {
237 mockHostDaemonFacade.hostClientId = null;
238 return controller.start(FAKE_HOST_PIN, true).then(function() {
240 }, function(/** remoting.Error */ e) {
241 assert.equal(e.getDetail(), 'getHostClientId');
242 assert.equal(e.getTag(), remoting.Error.Tag.UNEXPECTED);
243 assert.equal(unregisterHostByIdSpy.callCount, 0);
244 assert.equal(onLocalHostStartedSpy.callCount, 0);
245 assert.equal(startDaemonSpy.callCount, 0);
249 // Check what happens when the registry returns an HTTP when we try to
251 QUnit.test('start with host registration failure', function(assert) {
252 mockHostListApi.authCodeFromRegister = null;
253 return controller.start(FAKE_HOST_PIN, true).then(function() {
255 }, function(/** remoting.Error */ e) {
256 assert.equal(e.getTag(), remoting.Error.Tag.REGISTRATION_FAILED);
257 assert.equal(unregisterHostByIdSpy.callCount, 0);
258 assert.equal(onLocalHostStartedSpy.callCount, 0);
259 assert.equal(startDaemonSpy.callCount, 0);
263 // Check what happens when the HostDaemonFacade's
264 // getCredentialsFromAuthCode method fails.
265 QUnit.test('start with getCredentialsFromAuthCode failure', function(assert) {
266 mockHostDaemonFacade.useEmail = null;
267 mockHostDaemonFacade.refreshToken = null;
268 return controller.start(FAKE_HOST_PIN, true).then(function() {
270 }, function(/** remoting.Error */ e) {
271 assert.equal(e.getDetail(), 'getCredentialsFromAuthCode');
272 assert.equal(e.getTag(), remoting.Error.Tag.UNEXPECTED);
273 assert.equal(getCredentialsFromAuthCodeSpy.callCount, 1);
274 assert.equal(onLocalHostStartedSpy.callCount, 0);
275 assert.equal(startDaemonSpy.callCount, 0);
279 // Check what happens when the SignalStrategy fails to connect.
280 QUnit.test('start with signalStrategy failure', function(assert) {
281 stubSignalStrategyConnect(false);
282 return controller.start(FAKE_HOST_PIN, true).then(function() {
284 }, function(/** remoting.Error */ e) {
285 assert.equal(e.getDetail(), 'setStateForTesting');
286 assert.equal(e.getTag(), remoting.Error.Tag.UNEXPECTED);
287 assert.equal(unregisterHostByIdSpy.callCount, 1);
291 // Check what happens when the HostDaemonFacade's startDaemon method
292 // fails and calls its onError argument.
293 // TODO(jrw): Should startDaemon even have an onError callback?
294 QUnit.test('start with startDaemon failure', function(assert) {
295 stubSignalStrategyConnect(true);
296 mockHostDaemonFacade.startDaemonResult = null;
297 return controller.start(FAKE_HOST_PIN, true).then(function() {
299 }, function(/** remoting.Error */ e) {
300 assert.equal(e.getDetail(), 'startDaemon');
301 assert.equal(e.getTag(), remoting.Error.Tag.UNEXPECTED);
302 assert.equal(unregisterHostByIdSpy.callCount, 1);
303 assert.equal(unregisterHostByIdSpy.args[0][0], FAKE_HOST_ID);
304 assert.equal(onLocalHostStartedSpy.callCount, 0);
308 // Check what happens when the HostDaemonFacade's startDaemon method
309 // calls is onDone method with a CANCELLED error code.
310 QUnit.test('start with startDaemon cancelled', function(assert) {
311 stubSignalStrategyConnect(true);
312 mockHostDaemonFacade.startDaemonResult =
313 remoting.HostController.AsyncResult.CANCELLED;
314 return controller.start(FAKE_HOST_PIN, true).then(function() {
316 }, function(/** remoting.Error */ e) {
317 assert.equal(e.getTag(), remoting.Error.Tag.CANCELLED);
318 assert.equal(unregisterHostByIdSpy.callCount, 1);
319 assert.equal(unregisterHostByIdSpy.args[0][0], FAKE_HOST_ID);
320 assert.equal(onLocalHostStartedSpy.callCount, 0);
324 // Check what happens when the HostDaemonFacade's startDaemon method
325 // calls is onDone method with an async error code.
326 QUnit.test('start with startDaemon returning failure code', function(assert) {
327 stubSignalStrategyConnect(true);
328 mockHostDaemonFacade.startDaemonResult =
329 remoting.HostController.AsyncResult.FAILED;
330 return controller.start(FAKE_HOST_PIN, true).then(function() {
332 }, function(/** remoting.Error */ e) {
333 assert.equal(e.getTag(), remoting.Error.Tag.UNEXPECTED);
334 assert.equal(unregisterHostByIdSpy.callCount, 1);
335 assert.equal(onLocalHostStartedSpy.callCount, 0);
339 // Check what happens when the entire host registration process
341 [false, true].forEach(function(/** boolean */ consent) {
342 QUnit.test('start with consent=' + consent, function(assert) {
344 var fakePinHash = fakePinHashFunc(FAKE_HOST_ID, FAKE_HOST_PIN);
345 stubSignalStrategyConnect(true);
346 return controller.start(FAKE_HOST_PIN, consent).then(function() {
347 assert.equal(getCredentialsFromAuthCodeSpy.callCount, 1);
349 getCredentialsFromAuthCodeSpy.args[0][0],
351 assert.equal(getPinHashSpy.callCount, 1);
353 getPinHashSpy.args[0].slice(0, 2),
354 [FAKE_HOST_ID, FAKE_HOST_PIN]);
355 assert.equal(unregisterHostByIdSpy.callCount, 0);
356 assert.equal(onLocalHostStartedSpy.callCount, 1);
357 assert.equal(startDaemonSpy.callCount, 1);
359 startDaemonSpy.args[0].slice(0, 2),
361 xmpp_login: FAKE_XMPP_LOGIN,
362 oauth_refresh_token: FAKE_REFRESH_TOKEN,
363 host_owner: FAKE_CLIENT_JID.toLowerCase(),
364 host_owner_email: FAKE_USER_EMAIL,
365 host_id: FAKE_HOST_ID,
366 host_name: FAKE_HOST_NAME,
367 host_secret_hash: fakePinHash,
368 private_key: FAKE_PRIVATE_KEY
374 // Check what happens when stopDaemon calls onError.
375 // TODO(jrw): Should stopDaemon even have an onError callback?
376 QUnit.test('stop with stopDaemon failure', function(assert) {
377 mockHostDaemonFacade.stopDaemonResult = null;
378 return new Promise(function(resolve, reject) {
379 controller.stop(function() {
380 reject('test failed');
381 }, function(/** remoting.Error */ e) {
382 assert.equal(e.getDetail(), 'stopDaemon');
383 // TODO(jrw): Is it really desirable to leave the host registered?
384 assert.equal(unregisterHostByIdSpy.callCount, 0);
390 // Check what happens when stopDaemon returns FAILED.
391 QUnit.test('stop with stopDaemon cancelled', function(assert) {
392 mockHostDaemonFacade.stopDaemonResult =
393 remoting.HostController.AsyncResult.FAILED;
394 return new Promise(function(resolve, reject) {
395 controller.stop(function() {
396 reject('test failed');
397 }, function(/** remoting.Error */ e) {
398 // TODO(jrw): Is it really desirable to leave the host registered?
399 assert.equal(e.getTag(), remoting.Error.Tag.UNEXPECTED);
400 assert.equal(unregisterHostByIdSpy.callCount, 0);
406 // Check what happens when stopDaemon returns CANCELLED.
407 QUnit.test('stop with stopDaemon cancelled', function(assert) {
408 mockHostDaemonFacade.stopDaemonResult =
409 remoting.HostController.AsyncResult.CANCELLED;
410 return new Promise(function(resolve, reject) {
411 controller.stop(function() {
412 reject('test failed');
413 }, function(/** remoting.Error */ e) {
414 assert.equal(e.getTag(), remoting.Error.Tag.CANCELLED);
415 assert.equal(unregisterHostByIdSpy.callCount, 0);
421 // Check what happens when stopDaemon succeeds.
422 QUnit.test('stop succeeds', function(assert) {
423 sinon.stub(controller, 'getLocalHostId').callsArgWith(0, FAKE_HOST_ID);
424 return new Promise(function(resolve, reject) {
425 controller.stop(function() {
426 assert.equal(unregisterHostByIdSpy.callCount, 1);
427 assert.equal(unregisterHostByIdSpy.args[0][0], FAKE_HOST_ID);
433 // Check what happens when the host reports an invalid config.
434 QUnit.test('updatePin where config is invalid', function(assert) {
435 mockHostDaemonFacade.daemonConfig = {};
436 return new Promise(function(resolve, reject) {
437 controller.updatePin(FAKE_NEW_HOST_PIN, function() {
438 reject('test failed');
439 }, function(/** remoting.Error */ e) {
440 assert.equal(e.getTag(), remoting.Error.Tag.UNEXPECTED);
446 // Check what happens when getDaemonConfig calls onError.
447 QUnit.test('updatePin where getDaemonConfig fails', function(assert) {
448 mockHostDaemonFacade.daemonConfig = null;
449 return new Promise(function(resolve, reject) {
450 controller.updatePin(FAKE_NEW_HOST_PIN, function() {
451 reject('test failed');
452 }, function(/** remoting.Error */ e) {
453 assert.equal(e.getDetail(), 'getDaemonConfig');
454 assert.equal(e.getTag(), remoting.Error.Tag.UNEXPECTED);
460 // Check what happens when updateDaemonConfig calls onError.
461 // TODO(jrw): Should updateDaemonConfig even have an onError callback?
462 QUnit.test('updatePin where updateDaemonConfig calls onError', function(
464 mockHostDaemonFacade.updateDaemonConfigResult = null;
465 return new Promise(function(resolve, reject) {
466 controller.updatePin(FAKE_NEW_HOST_PIN, function() {
467 reject('test failed');
468 }, function(/** remoting.Error */ e) {
469 assert.equal(e.getDetail(), 'updateDaemonConfig');
470 assert.equal(e.getTag(), remoting.Error.Tag.UNEXPECTED);
476 // Check what happens when updateDaemonConfig returns CANCELLED.
477 QUnit.test('updatePin where updateDaemonConfig is cancelled', function(
479 mockHostDaemonFacade.updateDaemonConfigResult =
480 remoting.HostController.AsyncResult.CANCELLED;
481 return new Promise(function(resolve, reject) {
482 controller.updatePin(FAKE_NEW_HOST_PIN, function() {
483 reject('test failed');
484 }, function(/** remoting.Error */ e) {
485 assert.equal(e.getTag(), remoting.Error.Tag.CANCELLED);
491 // Check what happens when updateDaemonConfig returns FAILED.
492 QUnit.test('updatePin where updateDaemonConfig is returns failure', function(
494 mockHostDaemonFacade.updateDaemonConfigResult =
495 remoting.HostController.AsyncResult.FAILED;
496 return new Promise(function(resolve, reject) {
497 controller.updatePin(FAKE_NEW_HOST_PIN, function() {
498 reject('test failed');
499 }, function(/** remoting.Error */ e) {
500 assert.equal(e.getTag(), remoting.Error.Tag.UNEXPECTED);
506 // Check what happens when updatePin succeeds.
507 QUnit.test('updatePin succeeds', function(assert) {
509 var fakePinHash = fakePinHashFunc(FAKE_HOST_ID, FAKE_NEW_HOST_PIN);
510 return new Promise(function(resolve, reject) {
511 controller.updatePin(FAKE_NEW_HOST_PIN, function() {
512 assert.equal(getPinHashSpy.callCount, 1);
513 assert.equal(getPinHashSpy.args[0][0], FAKE_HOST_ID);
514 assert.equal(getPinHashSpy.args[0][1], FAKE_NEW_HOST_PIN);
515 assert.equal(updateDaemonConfigSpy.callCount, 1);
517 updateDaemonConfigSpy.args[0][0], {
518 host_secret_hash: fakePinHash
525 // Check what happens when getLocalHostState fails.
526 QUnit.test('getLocalHostState with error', function(assert) {
527 mockHostDaemonFacade.daemonState = null;
528 return new Promise(function(resolve, reject) {
529 controller.getLocalHostState(function(
530 /** remoting.HostController.State */ state) {
531 assert.equal(state, remoting.HostController.State.UNKNOWN);
537 // Check what happens when getLocalHostState reports no plugin.
538 QUnit.test('getLocalHostState with no plugin', function(assert) {
539 sinon.stub(mockHostDaemonFacade, 'getDaemonState').returns(
540 Promise.reject(new remoting.Error(remoting.Error.Tag.MISSING_PLUGIN)));
541 return new Promise(function(resolve, reject) {
542 controller.getLocalHostState(function(
543 /** remoting.HostController.State */ state) {
544 assert.equal(state, remoting.HostController.State.NOT_INSTALLED);
550 // Check what happens when getLocalHostState succeeds.
551 QUnit.test('getLocalHostState succeeds', function(assert) {
552 return new Promise(function(resolve, reject) {
553 controller.getLocalHostState(function(
554 /** remoting.HostController.State */ state) {
555 assert.equal(state, remoting.HostController.State.STARTED);
561 // Check what happens to getLocalHostId when getDaemonConfig
562 // returns an invalid config.
563 QUnit.test('getLocalHostId with invalid daemon config', function(assert) {
564 mockHostDaemonFacade.daemonConfig = {};
565 return new Promise(function(resolve, reject) {
566 controller.getLocalHostId(function(/** ?string */ id) {
567 assert.strictEqual(id, null);
573 // Check what happens to getLocalHostId when getDaemonConfig fails.
574 QUnit.test('getLocalHostId with getDaemonConfig failure', function(assert) {
575 mockHostDaemonFacade.daemonConfig = null;
576 return new Promise(function(resolve, reject) {
577 controller.getLocalHostId(function(/** ?string */ id) {
578 assert.strictEqual(id, null);
584 // Check what happens when getLocalHostId succeeds.
585 QUnit.test('getLocalHostId succeeds', function(assert) {
586 return new Promise(function(resolve, reject) {
587 controller.getLocalHostId(function(/** ?string */ id) {
588 assert.equal(id, FAKE_HOST_ID);
594 // Tests omitted for hasFeature, getPairedClients, deletePairedClient,
595 // and clearPairedClients because they simply call through to