@webex/internal-plugin-mercury 2.59.1 → 2.59.3-next.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,709 +1,709 @@
1
- /*!
2
- * Copyright (c) 2015-2020 Cisco Systems, Inc. See LICENSE file.
3
- */
4
-
5
- import {assert} from '@webex/test-helper-chai';
6
- import Mercury, {
7
- BadRequest,
8
- NotAuthorized,
9
- Forbidden,
10
- UnknownResponse,
11
- // NotFound,
12
- config as mercuryConfig,
13
- ConnectionError,
14
- Socket,
15
- } from '@webex/internal-plugin-mercury';
16
- import sinon from 'sinon';
17
- import MockWebex from '@webex/test-helper-mock-webex';
18
- import MockWebSocket from '@webex/test-helper-mock-web-socket';
19
- import uuid from 'uuid';
20
- import FakeTimers from '@sinonjs/fake-timers';
21
- import {skipInBrowser} from '@webex/test-helper-mocha';
22
-
23
- import promiseTick from '../lib/promise-tick';
24
-
25
- describe('plugin-mercury', () => {
26
- describe('Mercury', () => {
27
- let clock, mercury, mockWebSocket, socketOpenStub, webex;
28
-
29
- const statusStartTypingMessage = JSON.stringify({
30
- id: uuid.v4(),
31
- data: {
32
- eventType: 'status.start_typing',
33
- actor: {
34
- id: 'actorId',
35
- },
36
- conversationId: uuid.v4(),
37
- },
38
- timestamp: Date.now(),
39
- trackingId: `suffix_${uuid.v4()}_${Date.now()}`,
40
- });
41
-
42
- beforeEach(() => {
43
- clock = FakeTimers.install({now: Date.now()});
44
- });
45
-
46
- afterEach(() => {
47
- clock.uninstall();
48
- });
49
-
50
- beforeEach(() => {
51
- webex = new MockWebex({
52
- children: {
53
- mercury: Mercury,
54
- },
55
- });
56
- webex.credentials = {
57
- refresh: sinon.stub().returns(Promise.resolve()),
58
- getUserToken: sinon.stub().returns(
59
- Promise.resolve({
60
- toString() {
61
- return 'Bearer FAKE';
62
- },
63
- })
64
- ),
65
- };
66
- webex.internal.device = {
67
- register: sinon.stub().returns(Promise.resolve()),
68
- refresh: sinon.stub().returns(Promise.resolve()),
69
- webSocketUrl: 'ws://example.com',
70
- getWebSocketUrl: sinon.stub().returns(Promise.resolve('ws://example-2.com')),
71
- useServiceCatalogUrl: sinon
72
- .stub()
73
- .returns(Promise.resolve('https://service-catalog-url.com')),
74
- };
75
- webex.internal.services = {
76
- convertUrlToPriorityHostUrl: sinon.stub().returns(Promise.resolve('ws://example-2.com')),
77
- markFailedUrl: sinon.stub().returns(Promise.resolve()),
78
- };
79
- webex.internal.metrics.submitClientMetrics = sinon.stub();
80
- webex.trackingId = 'fakeTrackingId';
81
- webex.config.mercury = mercuryConfig.mercury;
82
-
83
- webex.logger = console;
84
-
85
- mockWebSocket = new MockWebSocket();
86
- sinon.stub(Socket, 'getWebSocketConstructor').returns(() => mockWebSocket);
87
-
88
- const origOpen = Socket.prototype.open;
89
-
90
- socketOpenStub = sinon.stub(Socket.prototype, 'open').callsFake(function (...args) {
91
- const promise = Reflect.apply(origOpen, this, args);
92
-
93
- process.nextTick(() => mockWebSocket.open());
94
-
95
- return promise;
96
- });
97
-
98
- mercury = webex.internal.mercury;
99
- });
100
-
101
- afterEach(() => {
102
- if (socketOpenStub) {
103
- socketOpenStub.restore();
104
- }
105
-
106
- if (Socket.getWebSocketConstructor.restore) {
107
- Socket.getWebSocketConstructor.restore();
108
- }
109
- });
110
-
111
- describe('#listen()', () => {
112
- it('proxies to #connect()', () => {
113
- sinon.stub(mercury, 'connect');
114
- mercury.listen();
115
- assert.called(mercury.connect);
116
- });
117
- });
118
-
119
- describe('#stopListening()', () => {
120
- it('proxies to #disconnect()', () => {
121
- sinon.stub(mercury, 'connect');
122
- mercury.listen();
123
- assert.called(mercury.connect);
124
- });
125
- });
126
-
127
- describe('#connect()', () => {
128
- it('lazily registers the device', () => {
129
- webex.internal.device.registered = false;
130
- assert.notCalled(webex.internal.device.register);
131
- const promise = mercury.connect();
132
-
133
- mockWebSocket.open();
134
-
135
- return promise.then(() => {
136
- assert.calledOnce(webex.internal.device.register);
137
- });
138
- });
139
-
140
- it('connects to Mercury using default url', () => {
141
- const promise = mercury.connect();
142
-
143
- assert.isFalse(mercury.connected, 'Mercury is not connected');
144
- assert.isTrue(mercury.connecting, 'Mercury is connecting');
145
- mockWebSocket.open();
146
-
147
- return promise.then(() => {
148
- assert.isTrue(mercury.connected, 'Mercury is connected');
149
- assert.isFalse(mercury.connecting, 'Mercury is not connecting');
150
- assert.calledWith(socketOpenStub, sinon.match(/ws:\/\/example.com/), sinon.match.any);
151
- });
152
- });
153
-
154
- describe('when `maxRetries` is set', () => {
155
- // skipping due to apparent bug with lolex in all browsers but Chrome.
156
- skipInBrowser(it)('fails after `maxRetries` attempts', () => {
157
- mercury.config.maxRetries = 2;
158
- socketOpenStub.restore();
159
- socketOpenStub = sinon.stub(Socket.prototype, 'open');
160
- socketOpenStub.returns(Promise.reject(new ConnectionError()));
161
- assert.notCalled(Socket.prototype.open);
162
-
163
- const promise = mercury.connect();
164
-
165
- return promiseTick(5)
166
- .then(() => {
167
- assert.calledOnce(Socket.prototype.open);
168
-
169
- return promiseTick(5);
170
- })
171
- .then(() => {
172
- clock.tick(mercury.config.backoffTimeReset);
173
-
174
- return promiseTick(5);
175
- })
176
- .then(() => {
177
- assert.calledTwice(Socket.prototype.open);
178
- clock.tick(2 * mercury.config.backoffTimeReset);
179
-
180
- return promiseTick(5);
181
- })
182
- .then(() => {
183
- assert.calledThrice(Socket.prototype.open);
184
- clock.tick(5 * mercury.config.backoffTimeReset);
185
-
186
- return assert.isRejected(promise);
187
- })
188
- .then(() => {
189
- assert.calledThrice(Socket.prototype.open);
190
- });
191
- });
192
- });
193
-
194
- it('can safely be called multiple times', () => {
195
- const promise = Promise.all([
196
- mercury.connect(),
197
- mercury.connect(),
198
- mercury.connect(),
199
- mercury.connect(),
200
- ]);
201
-
202
- mockWebSocket.open();
203
-
204
- return promise.then(() => {
205
- assert.calledOnce(Socket.prototype.open);
206
- });
207
- });
208
-
209
- // skipping due to apparent bug with lolex in all browsers but Chrome.
210
- skipInBrowser(describe)('when the connection fails', () => {
211
- it('backs off exponentially', () => {
212
- socketOpenStub.restore();
213
- socketOpenStub = sinon.stub(Socket.prototype, 'open');
214
- socketOpenStub.returns(Promise.reject(new ConnectionError({code: 4001})));
215
- // Note: onCall is zero-based
216
- socketOpenStub.onCall(2).returns(Promise.resolve(new MockWebSocket()));
217
- assert.notCalled(Socket.prototype.open);
218
-
219
- const promise = mercury.connect();
220
-
221
- return promiseTick(5)
222
- .then(() => {
223
- assert.calledOnce(Socket.prototype.open);
224
-
225
- // I'm not sure why, but it's important the clock doesn't advance
226
- // until a tick happens
227
- return promiseTick(5);
228
- })
229
- .then(() => {
230
- clock.tick(mercury.config.backoffTimeReset);
231
-
232
- return promiseTick(5);
233
- })
234
- .then(() => {
235
- assert.calledTwice(Socket.prototype.open);
236
- clock.tick(2 * mercury.config.backoffTimeReset);
237
-
238
- return promiseTick(5);
239
- })
240
- .then(() => {
241
- assert.calledThrice(Socket.prototype.open);
242
- clock.tick(5 * mercury.config.backoffTimeReset);
243
-
244
- return promise;
245
- })
246
- .then(() => {
247
- assert.calledThrice(Socket.prototype.open);
248
- clock.tick(8 * mercury.config.backoffTimeReset);
249
-
250
- return promiseTick(5);
251
- })
252
- .then(() => {
253
- assert.calledThrice(Socket.prototype.open);
254
- });
255
- });
256
-
257
- describe('with `BadRequest`', () => {
258
- it('fails permanently', () => {
259
- clock.uninstall();
260
- socketOpenStub.restore();
261
- socketOpenStub = sinon
262
- .stub(Socket.prototype, 'open')
263
- .returns(Promise.reject(new BadRequest({code: 4400})));
264
-
265
- return assert.isRejected(mercury.connect());
266
- });
267
- });
268
-
269
- describe('with `UnknownResponse`', () => {
270
- it('triggers a device refresh', () => {
271
- socketOpenStub.restore();
272
- socketOpenStub = sinon.stub(Socket.prototype, 'open').returns(Promise.resolve());
273
- socketOpenStub.onCall(0).returns(Promise.reject(new UnknownResponse({code: 4444})));
274
- assert.notCalled(webex.credentials.refresh);
275
- assert.notCalled(webex.internal.device.refresh);
276
- const promise = mercury.connect();
277
-
278
- return promiseTick(7).then(() => {
279
- assert.notCalled(webex.credentials.refresh);
280
- assert.called(webex.internal.device.refresh);
281
- clock.tick(1000);
282
-
283
- return promise;
284
- });
285
- });
286
- });
287
-
288
- describe('with `NotAuthorized`', () => {
289
- it('triggers a token refresh', () => {
290
- socketOpenStub.restore();
291
- socketOpenStub = sinon.stub(Socket.prototype, 'open').returns(Promise.resolve());
292
- socketOpenStub.onCall(0).returns(Promise.reject(new NotAuthorized({code: 4401})));
293
- assert.notCalled(webex.credentials.refresh);
294
- assert.notCalled(webex.internal.device.refresh);
295
- const promise = mercury.connect();
296
-
297
- return promiseTick(7).then(() => {
298
- assert.called(webex.credentials.refresh);
299
- assert.notCalled(webex.internal.device.refresh);
300
- clock.tick(1000);
301
-
302
- return promise;
303
- });
304
- });
305
- });
306
-
307
- describe('with `Forbidden`', () => {
308
- it('fails permanently', () => {
309
- clock.uninstall();
310
- socketOpenStub.restore();
311
- socketOpenStub = sinon
312
- .stub(Socket.prototype, 'open')
313
- .returns(Promise.reject(new Forbidden({code: 4403})));
314
-
315
- return assert.isRejected(mercury.connect());
316
- });
317
- });
318
-
319
- // describe(`with \`NotFound\``, () => {
320
- // it(`triggers a device refresh`, () => {
321
- // socketOpenStub.restore();
322
- // socketOpenStub = sinon.stub(Socket.prototype, `open`).returns(Promise.resolve());
323
- // socketOpenStub.onCall(0).returns(Promise.reject(new NotFound({code: 4404})));
324
- // assert.notCalled(webex.credentials.refresh);
325
- // assert.notCalled(webex.internal.device.refresh);
326
- // const promise = mercury.connect();
327
- // return promiseTick(6)
328
- // .then(() => {
329
- // assert.notCalled(webex.credentials.refresh);
330
- // assert.called(webex.internal.device.refresh);
331
- // clock.tick(1000);
332
- // return assert.isFulfilled(promise);
333
- // });
334
- // });
335
- // });
336
-
337
- describe('when web-high-availability feature is enabled', () => {
338
- it('marks current socket url as failed and get new one on Connection Error', () => {
339
- webex.internal.feature.getFeature.returns(Promise.resolve(true));
340
- socketOpenStub.restore();
341
- socketOpenStub = sinon.stub(Socket.prototype, 'open').returns(Promise.resolve());
342
- socketOpenStub.onCall(0).returns(Promise.reject(new ConnectionError({code: 4001})));
343
- const promise = mercury.connect();
344
-
345
- return promiseTick(7).then(() => {
346
- assert.calledOnce(webex.internal.services.markFailedUrl);
347
- clock.tick(1000);
348
-
349
- return promise;
350
- });
351
- });
352
- });
353
- });
354
-
355
- describe('when connected', () => {
356
- it('resolves immediately', () =>
357
- mercury.connect().then(() => {
358
- assert.isTrue(mercury.connected, 'Mercury is connected');
359
- assert.isFalse(mercury.connecting, 'Mercury is not connecting');
360
- const promise = mercury.connect();
361
-
362
- assert.isTrue(mercury.connected, 'Mercury is connected');
363
- assert.isFalse(mercury.connecting, 'Mercury is not connecting');
364
-
365
- return promise;
366
- }));
367
-
368
- // skipping due to apparent bug with lolex in all browsers but Chrome.
369
- skipInBrowser(it)('does not continue attempting to connect', () => {
370
- mercury.connect();
371
-
372
- return promiseTick(2)
373
- .then(() => {
374
- clock.tick(6 * webex.internal.mercury.config.backoffTimeReset);
375
-
376
- return promiseTick(2);
377
- })
378
- .then(() => {
379
- assert.calledOnce(Socket.prototype.open);
380
- });
381
- });
382
- });
383
-
384
- describe.skip('when webSocketUrl is provided', () => {
385
- it('connects to Mercury with provided url', () => {
386
- const webSocketUrl = 'ws://providedurl.com';
387
- const promise = mercury.connect(webSocketUrl);
388
-
389
- assert.isFalse(mercury.connected, 'Mercury is not connected');
390
- assert.isTrue(mercury.connecting, 'Mercury is connecting');
391
- mockWebSocket.open();
392
-
393
- return promise.then(() => {
394
- assert.isTrue(mercury.connected, 'Mercury is connected');
395
- assert.isFalse(mercury.connecting, 'Mercury is not connecting');
396
- assert.calledWith(
397
- Socket.prototype.open,
398
- sinon.match(/ws:\/\/providedurl.com/),
399
- sinon.match.any
400
- );
401
- });
402
- });
403
- });
404
- });
405
-
406
- describe.skip('Websocket proxy agent', () => {
407
- afterEach(() => {
408
- delete webex.config.defaultMercuryOptions;
409
- });
410
-
411
- it('connects to Mercury using proxy agent', () => {
412
- const testProxyUrl = 'http://proxyurl.com:80';
413
-
414
- webex.config.defaultMercuryOptions = {agent: {proxy: {href: testProxyUrl}}};
415
- const promise = mercury.connect();
416
-
417
- assert.isFalse(mercury.connected, 'Mercury is not connected');
418
- assert.isTrue(mercury.connecting, 'Mercury is connecting');
419
- mockWebSocket.open();
420
-
421
- return promise.then(() => {
422
- assert.isTrue(mercury.connected, 'Mercury is connected');
423
- assert.isFalse(mercury.connecting, 'Mercury is not connecting');
424
- assert.calledWith(
425
- socketOpenStub,
426
- sinon.match(/ws:\/\/example.com/),
427
- sinon.match.has(
428
- 'agent',
429
- sinon.match.has('proxy', sinon.match.has('href', testProxyUrl))
430
- )
431
- );
432
- });
433
- });
434
-
435
- it('connects to Mercury without proxy agent', () => {
436
- const promise = mercury.connect();
437
-
438
- assert.isFalse(mercury.connected, 'Mercury is not connected');
439
- assert.isTrue(mercury.connecting, 'Mercury is connecting');
440
- mockWebSocket.open();
441
-
442
- return promise.then(() => {
443
- assert.isTrue(mercury.connected, 'Mercury is connected');
444
- assert.isFalse(mercury.connecting, 'Mercury is not connecting');
445
- assert.calledWith(
446
- socketOpenStub,
447
- sinon.match(/ws:\/\/example.com/),
448
- sinon.match({agent: undefined})
449
- );
450
- });
451
- });
452
- });
453
-
454
- describe.skip('#disconnect()', () => {
455
- it('disconnects the WebSocket', () => mercury.connect()
456
- .then(() => {
457
- assert.isTrue(mercury.connected, 'Mercury is connected');
458
- assert.isFalse(mercury.connecting, 'Mercury is not connecting');
459
- const promise = mercury.disconnect();
460
-
461
- mockWebSocket.emit('close', {
462
- code: 1000,
463
- reason: 'Done',
464
- });
465
-
466
- return promise;
467
- })
468
- .then(() => {
469
- assert.isFalse(mercury.connected, 'Mercury is not connected');
470
- assert.isFalse(mercury.connecting, 'Mercury is not connecting');
471
- assert.isUndefined(mercury.mockWebSocket, 'Mercury does not have a mockWebSocket');
472
- }));
473
-
474
- it('stops emitting message events', () => {
475
- const spy = sinon.spy();
476
-
477
- mercury.on('event:status.start_typing', spy);
478
-
479
- return mercury
480
- .connect()
481
- .then(() => {
482
- assert.isTrue(mercury.connected, 'Mercury is connected');
483
- assert.isFalse(mercury.connecting, 'Mercury is not connecting');
484
-
485
- assert.notCalled(spy);
486
- mockWebSocket.readyState = 1;
487
- mockWebSocket.emit('open');
488
- mockWebSocket.emit('message', {data: statusStartTypingMessage});
489
- })
490
- .then(() => {
491
- assert.calledOnce(spy);
492
-
493
- const promise = mercury.disconnect();
494
-
495
- mockWebSocket.readyState = 1;
496
- mockWebSocket.emit('open');
497
- mockWebSocket.emit('message', {data: statusStartTypingMessage});
498
- mockWebSocket.emit('close', {
499
- code: 1000,
500
- reason: 'Done',
501
- });
502
- mockWebSocket.emit('message', {data: statusStartTypingMessage});
503
-
504
- return promise;
505
- })
506
-
507
- .then(() => {
508
- mockWebSocket.readyState = 1;
509
- mockWebSocket.emit('open');
510
- mockWebSocket.emit('message', {data: statusStartTypingMessage});
511
- assert.calledOnce(spy);
512
- });
513
- });
514
-
515
- describe('when there is a connection attempt inflight', () => {
516
- it('stops the attempt when disconnect called', () => {
517
- socketOpenStub.restore();
518
- socketOpenStub = sinon.stub(Socket.prototype, 'open');
519
- socketOpenStub.onCall(0).returns(
520
- // Delay the opening of the socket so that disconnect is called while open
521
- // is in progress
522
- promiseTick(2 * webex.internal.mercury.config.backoffTimeReset)
523
- // Pretend the socket opened successfully. Failing should be fine too but
524
- // it generates more console output.
525
- .then(() => Promise.resolve())
526
- );
527
- const promise = mercury.connect();
528
-
529
- // Wait for the connect call to setup
530
- return promiseTick(webex.internal.mercury.config.backoffTimeReset).then(() => {
531
- // By this time backoffCall and mercury socket should be defined by the
532
- // 'connect' call
533
- assert.isDefined(mercury.backoffCall, 'Mercury backoffCall is not defined');
534
- assert.isDefined(mercury.socket, 'Mercury socket is not defined');
535
- // Calling disconnect will abort the backoffCall, close the socket, and
536
- // reject the connect
537
- mercury.disconnect();
538
- assert.isUndefined(mercury.backoffCall, 'Mercury backoffCall is still defined');
539
- // The socket will never be unset (which seems bad)
540
- assert.isDefined(mercury.socket, 'Mercury socket is not defined');
541
-
542
- return assert.isRejected(promise);
543
- });
544
- });
545
-
546
- it('stops the attempt when backoffCall is undefined', () => {
547
- socketOpenStub.restore();
548
- socketOpenStub = sinon.stub(Socket.prototype, 'open');
549
- socketOpenStub.returns(Promise.resolve());
550
-
551
- let reason;
552
-
553
- mercury.backoffCall = undefined;
554
- mercury._attemptConnection('ws://example.com', (_reason) => {
555
- reason = _reason;
556
- });
557
-
558
- return promiseTick(webex.internal.mercury.config.backoffTimeReset).then(() => {
559
- assert.equal(
560
- reason.message,
561
- 'mercury: prevent socket open when backoffCall no longer defined'
562
- );
563
- });
564
- });
565
- });
566
- });
567
-
568
- describe('#_emit()', () => {
569
- it('emits Error-safe events', () => {
570
- mercury.on('break', () => {
571
- throw new Error();
572
- });
573
-
574
- return Promise.resolve(mercury._emit('break'));
575
- });
576
- });
577
-
578
- describe('#_applyOverrides()', () => {
579
- const lastSeenActivityDate = 'Some date';
580
- const lastReadableActivityDate = 'Some other date';
581
-
582
- it('merges a single header field with data', () => {
583
- const envelope = {
584
- headers: {
585
- 'data.activity.target.lastSeenActivityDate': lastSeenActivityDate,
586
- },
587
- data: {
588
- activity: {},
589
- },
590
- };
591
-
592
- mercury._applyOverrides(envelope);
593
-
594
- assert.equal(envelope.data.activity.target.lastSeenActivityDate, lastSeenActivityDate);
595
- });
596
-
597
- it('merges a multiple header fields with data', () => {
598
- const envelope = {
599
- headers: {
600
- 'data.activity.target.lastSeenActivityDate': lastSeenActivityDate,
601
- 'data.activity.target.lastReadableActivityDate': lastReadableActivityDate,
602
- },
603
- data: {
604
- activity: {},
605
- },
606
- };
607
-
608
- mercury._applyOverrides(envelope);
609
-
610
- assert.equal(envelope.data.activity.target.lastSeenActivityDate, lastSeenActivityDate);
611
- assert.equal(
612
- envelope.data.activity.target.lastReadableActivityDate,
613
- lastReadableActivityDate
614
- );
615
- });
616
-
617
- it('merges headers when Mercury messages arrive', () => {
618
- const envelope = {
619
- headers: {
620
- 'data.activity.target.lastSeenActivityDate': lastSeenActivityDate,
621
- },
622
- data: {
623
- activity: {},
624
- },
625
- };
626
-
627
- mercury._applyOverrides(envelope);
628
-
629
- assert.equal(envelope.data.activity.target.lastSeenActivityDate, lastSeenActivityDate);
630
- });
631
- });
632
-
633
- describe('#_prepareUrl()', () => {
634
- beforeEach(() => {
635
- webex.internal.device.webSocketUrl = 'ws://example.com';
636
- });
637
-
638
- it('uses device default webSocketUrl', () =>
639
- webex.internal.mercury._prepareUrl().then((wsUrl) => assert.match(wsUrl, /example.com/)));
640
- it('uses provided webSocketUrl', () =>
641
- webex.internal.mercury
642
- ._prepareUrl('ws://provided.com')
643
- .then((wsUrl) => assert.match(wsUrl, /provided.com/)));
644
- it('requests text-mode WebSockets', () =>
645
- webex.internal.mercury
646
- ._prepareUrl()
647
- .then((wsUrl) => assert.match(wsUrl, /outboundWireFormat=text/)));
648
-
649
- it('requests the buffer state message', () =>
650
- webex.internal.mercury
651
- ._prepareUrl()
652
- .then((wsUrl) => assert.match(wsUrl, /bufferStates=true/)));
653
-
654
- it('does not add conditional properties', () =>
655
- webex.internal.mercury._prepareUrl().then((wsUrl) => {
656
- assert.notMatch(wsUrl, /mercuryRegistrationStatus/);
657
- assert.notMatch(wsUrl, /mercuryRegistrationStatus/);
658
- assert.notMatch(wsUrl, /isRegistrationRefreshEnabled/);
659
- assert.notMatch(wsUrl, /multipleConnections/);
660
- }));
661
-
662
- describe('when web-high-availability is enabled', () => {
663
- it('uses webSocketUrl provided by device', () => {
664
- webex.internal.device.useServiceCatalogUrl = sinon
665
- .stub()
666
- .returns(Promise.resolve('ws://example-2.com'));
667
- webex.internal.feature.getFeature.onCall(0).returns(Promise.resolve(true));
668
-
669
- return webex.internal.mercury
670
- ._prepareUrl()
671
- .then((wsUrl) => assert.match(wsUrl, /example-2.com/));
672
- });
673
- });
674
-
675
- describe("when 'web-shared-socket' is enabled", () => {
676
- beforeEach(() => {
677
- webex.internal.feature.getFeature.returns(Promise.resolve(true));
678
- });
679
-
680
- it('requests shared socket support', () =>
681
- webex.internal.mercury
682
- ._prepareUrl()
683
- .then((wsUrl) => assert.match(wsUrl, /isRegistrationRefreshEnabled=true/)));
684
-
685
- it('requests the registration banner', () =>
686
- webex.internal.mercury
687
- ._prepareUrl()
688
- .then((wsUrl) => assert.match(wsUrl, /mercuryRegistrationStatus=true/)));
689
-
690
- it('does not request the buffer state message', () =>
691
- webex.internal.mercury._prepareUrl().then((wsUrl) => {
692
- assert.match(wsUrl, /mercuryRegistrationStatus=true/);
693
- assert.notMatch(wsUrl, /bufferStates/);
694
- }));
695
- });
696
-
697
- describe('when using an ephemeral device', () => {
698
- beforeEach(() => {
699
- webex.config.device.ephemeral = true;
700
- });
701
-
702
- it('indicates multiple connections may be coming from this user', () =>
703
- webex.internal.mercury
704
- ._prepareUrl()
705
- .then((wsUrl) => assert.match(wsUrl, /multipleConnections/)));
706
- });
707
- });
708
- });
709
- });
1
+ /*!
2
+ * Copyright (c) 2015-2020 Cisco Systems, Inc. See LICENSE file.
3
+ */
4
+
5
+ import {assert} from '@webex/test-helper-chai';
6
+ import Mercury, {
7
+ BadRequest,
8
+ NotAuthorized,
9
+ Forbidden,
10
+ UnknownResponse,
11
+ // NotFound,
12
+ config as mercuryConfig,
13
+ ConnectionError,
14
+ Socket,
15
+ } from '@webex/internal-plugin-mercury';
16
+ import sinon from 'sinon';
17
+ import MockWebex from '@webex/test-helper-mock-webex';
18
+ import MockWebSocket from '@webex/test-helper-mock-web-socket';
19
+ import uuid from 'uuid';
20
+ import FakeTimers from '@sinonjs/fake-timers';
21
+ import {skipInBrowser} from '@webex/test-helper-mocha';
22
+
23
+ import promiseTick from '../lib/promise-tick';
24
+
25
+ describe('plugin-mercury', () => {
26
+ describe('Mercury', () => {
27
+ let clock, mercury, mockWebSocket, socketOpenStub, webex;
28
+
29
+ const statusStartTypingMessage = JSON.stringify({
30
+ id: uuid.v4(),
31
+ data: {
32
+ eventType: 'status.start_typing',
33
+ actor: {
34
+ id: 'actorId',
35
+ },
36
+ conversationId: uuid.v4(),
37
+ },
38
+ timestamp: Date.now(),
39
+ trackingId: `suffix_${uuid.v4()}_${Date.now()}`,
40
+ });
41
+
42
+ beforeEach(() => {
43
+ clock = FakeTimers.install({now: Date.now()});
44
+ });
45
+
46
+ afterEach(() => {
47
+ clock.uninstall();
48
+ });
49
+
50
+ beforeEach(() => {
51
+ webex = new MockWebex({
52
+ children: {
53
+ mercury: Mercury,
54
+ },
55
+ });
56
+ webex.credentials = {
57
+ refresh: sinon.stub().returns(Promise.resolve()),
58
+ getUserToken: sinon.stub().returns(
59
+ Promise.resolve({
60
+ toString() {
61
+ return 'Bearer FAKE';
62
+ },
63
+ })
64
+ ),
65
+ };
66
+ webex.internal.device = {
67
+ register: sinon.stub().returns(Promise.resolve()),
68
+ refresh: sinon.stub().returns(Promise.resolve()),
69
+ webSocketUrl: 'ws://example.com',
70
+ getWebSocketUrl: sinon.stub().returns(Promise.resolve('ws://example-2.com')),
71
+ useServiceCatalogUrl: sinon
72
+ .stub()
73
+ .returns(Promise.resolve('https://service-catalog-url.com')),
74
+ };
75
+ webex.internal.services = {
76
+ convertUrlToPriorityHostUrl: sinon.stub().returns(Promise.resolve('ws://example-2.com')),
77
+ markFailedUrl: sinon.stub().returns(Promise.resolve()),
78
+ };
79
+ webex.internal.metrics.submitClientMetrics = sinon.stub();
80
+ webex.trackingId = 'fakeTrackingId';
81
+ webex.config.mercury = mercuryConfig.mercury;
82
+
83
+ webex.logger = console;
84
+
85
+ mockWebSocket = new MockWebSocket();
86
+ sinon.stub(Socket, 'getWebSocketConstructor').returns(() => mockWebSocket);
87
+
88
+ const origOpen = Socket.prototype.open;
89
+
90
+ socketOpenStub = sinon.stub(Socket.prototype, 'open').callsFake(function (...args) {
91
+ const promise = Reflect.apply(origOpen, this, args);
92
+
93
+ process.nextTick(() => mockWebSocket.open());
94
+
95
+ return promise;
96
+ });
97
+
98
+ mercury = webex.internal.mercury;
99
+ });
100
+
101
+ afterEach(() => {
102
+ if (socketOpenStub) {
103
+ socketOpenStub.restore();
104
+ }
105
+
106
+ if (Socket.getWebSocketConstructor.restore) {
107
+ Socket.getWebSocketConstructor.restore();
108
+ }
109
+ });
110
+
111
+ describe('#listen()', () => {
112
+ it('proxies to #connect()', () => {
113
+ sinon.stub(mercury, 'connect');
114
+ mercury.listen();
115
+ assert.called(mercury.connect);
116
+ });
117
+ });
118
+
119
+ describe('#stopListening()', () => {
120
+ it('proxies to #disconnect()', () => {
121
+ sinon.stub(mercury, 'connect');
122
+ mercury.listen();
123
+ assert.called(mercury.connect);
124
+ });
125
+ });
126
+
127
+ describe('#connect()', () => {
128
+ it('lazily registers the device', () => {
129
+ webex.internal.device.registered = false;
130
+ assert.notCalled(webex.internal.device.register);
131
+ const promise = mercury.connect();
132
+
133
+ mockWebSocket.open();
134
+
135
+ return promise.then(() => {
136
+ assert.calledOnce(webex.internal.device.register);
137
+ });
138
+ });
139
+
140
+ it('connects to Mercury using default url', () => {
141
+ const promise = mercury.connect();
142
+
143
+ assert.isFalse(mercury.connected, 'Mercury is not connected');
144
+ assert.isTrue(mercury.connecting, 'Mercury is connecting');
145
+ mockWebSocket.open();
146
+
147
+ return promise.then(() => {
148
+ assert.isTrue(mercury.connected, 'Mercury is connected');
149
+ assert.isFalse(mercury.connecting, 'Mercury is not connecting');
150
+ assert.calledWith(socketOpenStub, sinon.match(/ws:\/\/example.com/), sinon.match.any);
151
+ });
152
+ });
153
+
154
+ describe('when `maxRetries` is set', () => {
155
+ // skipping due to apparent bug with lolex in all browsers but Chrome.
156
+ skipInBrowser(it)('fails after `maxRetries` attempts', () => {
157
+ mercury.config.maxRetries = 2;
158
+ socketOpenStub.restore();
159
+ socketOpenStub = sinon.stub(Socket.prototype, 'open');
160
+ socketOpenStub.returns(Promise.reject(new ConnectionError()));
161
+ assert.notCalled(Socket.prototype.open);
162
+
163
+ const promise = mercury.connect();
164
+
165
+ return promiseTick(5)
166
+ .then(() => {
167
+ assert.calledOnce(Socket.prototype.open);
168
+
169
+ return promiseTick(5);
170
+ })
171
+ .then(() => {
172
+ clock.tick(mercury.config.backoffTimeReset);
173
+
174
+ return promiseTick(5);
175
+ })
176
+ .then(() => {
177
+ assert.calledTwice(Socket.prototype.open);
178
+ clock.tick(2 * mercury.config.backoffTimeReset);
179
+
180
+ return promiseTick(5);
181
+ })
182
+ .then(() => {
183
+ assert.calledThrice(Socket.prototype.open);
184
+ clock.tick(5 * mercury.config.backoffTimeReset);
185
+
186
+ return assert.isRejected(promise);
187
+ })
188
+ .then(() => {
189
+ assert.calledThrice(Socket.prototype.open);
190
+ });
191
+ });
192
+ });
193
+
194
+ it('can safely be called multiple times', () => {
195
+ const promise = Promise.all([
196
+ mercury.connect(),
197
+ mercury.connect(),
198
+ mercury.connect(),
199
+ mercury.connect(),
200
+ ]);
201
+
202
+ mockWebSocket.open();
203
+
204
+ return promise.then(() => {
205
+ assert.calledOnce(Socket.prototype.open);
206
+ });
207
+ });
208
+
209
+ // skipping due to apparent bug with lolex in all browsers but Chrome.
210
+ skipInBrowser(describe)('when the connection fails', () => {
211
+ it('backs off exponentially', () => {
212
+ socketOpenStub.restore();
213
+ socketOpenStub = sinon.stub(Socket.prototype, 'open');
214
+ socketOpenStub.returns(Promise.reject(new ConnectionError({code: 4001})));
215
+ // Note: onCall is zero-based
216
+ socketOpenStub.onCall(2).returns(Promise.resolve(new MockWebSocket()));
217
+ assert.notCalled(Socket.prototype.open);
218
+
219
+ const promise = mercury.connect();
220
+
221
+ return promiseTick(5)
222
+ .then(() => {
223
+ assert.calledOnce(Socket.prototype.open);
224
+
225
+ // I'm not sure why, but it's important the clock doesn't advance
226
+ // until a tick happens
227
+ return promiseTick(5);
228
+ })
229
+ .then(() => {
230
+ clock.tick(mercury.config.backoffTimeReset);
231
+
232
+ return promiseTick(5);
233
+ })
234
+ .then(() => {
235
+ assert.calledTwice(Socket.prototype.open);
236
+ clock.tick(2 * mercury.config.backoffTimeReset);
237
+
238
+ return promiseTick(5);
239
+ })
240
+ .then(() => {
241
+ assert.calledThrice(Socket.prototype.open);
242
+ clock.tick(5 * mercury.config.backoffTimeReset);
243
+
244
+ return promise;
245
+ })
246
+ .then(() => {
247
+ assert.calledThrice(Socket.prototype.open);
248
+ clock.tick(8 * mercury.config.backoffTimeReset);
249
+
250
+ return promiseTick(5);
251
+ })
252
+ .then(() => {
253
+ assert.calledThrice(Socket.prototype.open);
254
+ });
255
+ });
256
+
257
+ describe('with `BadRequest`', () => {
258
+ it('fails permanently', () => {
259
+ clock.uninstall();
260
+ socketOpenStub.restore();
261
+ socketOpenStub = sinon
262
+ .stub(Socket.prototype, 'open')
263
+ .returns(Promise.reject(new BadRequest({code: 4400})));
264
+
265
+ return assert.isRejected(mercury.connect());
266
+ });
267
+ });
268
+
269
+ describe('with `UnknownResponse`', () => {
270
+ it('triggers a device refresh', () => {
271
+ socketOpenStub.restore();
272
+ socketOpenStub = sinon.stub(Socket.prototype, 'open').returns(Promise.resolve());
273
+ socketOpenStub.onCall(0).returns(Promise.reject(new UnknownResponse({code: 4444})));
274
+ assert.notCalled(webex.credentials.refresh);
275
+ assert.notCalled(webex.internal.device.refresh);
276
+ const promise = mercury.connect();
277
+
278
+ return promiseTick(7).then(() => {
279
+ assert.notCalled(webex.credentials.refresh);
280
+ assert.called(webex.internal.device.refresh);
281
+ clock.tick(1000);
282
+
283
+ return promise;
284
+ });
285
+ });
286
+ });
287
+
288
+ describe('with `NotAuthorized`', () => {
289
+ it('triggers a token refresh', () => {
290
+ socketOpenStub.restore();
291
+ socketOpenStub = sinon.stub(Socket.prototype, 'open').returns(Promise.resolve());
292
+ socketOpenStub.onCall(0).returns(Promise.reject(new NotAuthorized({code: 4401})));
293
+ assert.notCalled(webex.credentials.refresh);
294
+ assert.notCalled(webex.internal.device.refresh);
295
+ const promise = mercury.connect();
296
+
297
+ return promiseTick(7).then(() => {
298
+ assert.called(webex.credentials.refresh);
299
+ assert.notCalled(webex.internal.device.refresh);
300
+ clock.tick(1000);
301
+
302
+ return promise;
303
+ });
304
+ });
305
+ });
306
+
307
+ describe('with `Forbidden`', () => {
308
+ it('fails permanently', () => {
309
+ clock.uninstall();
310
+ socketOpenStub.restore();
311
+ socketOpenStub = sinon
312
+ .stub(Socket.prototype, 'open')
313
+ .returns(Promise.reject(new Forbidden({code: 4403})));
314
+
315
+ return assert.isRejected(mercury.connect());
316
+ });
317
+ });
318
+
319
+ // describe(`with \`NotFound\``, () => {
320
+ // it(`triggers a device refresh`, () => {
321
+ // socketOpenStub.restore();
322
+ // socketOpenStub = sinon.stub(Socket.prototype, `open`).returns(Promise.resolve());
323
+ // socketOpenStub.onCall(0).returns(Promise.reject(new NotFound({code: 4404})));
324
+ // assert.notCalled(webex.credentials.refresh);
325
+ // assert.notCalled(webex.internal.device.refresh);
326
+ // const promise = mercury.connect();
327
+ // return promiseTick(6)
328
+ // .then(() => {
329
+ // assert.notCalled(webex.credentials.refresh);
330
+ // assert.called(webex.internal.device.refresh);
331
+ // clock.tick(1000);
332
+ // return assert.isFulfilled(promise);
333
+ // });
334
+ // });
335
+ // });
336
+
337
+ describe('when web-high-availability feature is enabled', () => {
338
+ it('marks current socket url as failed and get new one on Connection Error', () => {
339
+ webex.internal.feature.getFeature.returns(Promise.resolve(true));
340
+ socketOpenStub.restore();
341
+ socketOpenStub = sinon.stub(Socket.prototype, 'open').returns(Promise.resolve());
342
+ socketOpenStub.onCall(0).returns(Promise.reject(new ConnectionError({code: 4001})));
343
+ const promise = mercury.connect();
344
+
345
+ return promiseTick(7).then(() => {
346
+ assert.calledOnce(webex.internal.services.markFailedUrl);
347
+ clock.tick(1000);
348
+
349
+ return promise;
350
+ });
351
+ });
352
+ });
353
+ });
354
+
355
+ describe('when connected', () => {
356
+ it('resolves immediately', () =>
357
+ mercury.connect().then(() => {
358
+ assert.isTrue(mercury.connected, 'Mercury is connected');
359
+ assert.isFalse(mercury.connecting, 'Mercury is not connecting');
360
+ const promise = mercury.connect();
361
+
362
+ assert.isTrue(mercury.connected, 'Mercury is connected');
363
+ assert.isFalse(mercury.connecting, 'Mercury is not connecting');
364
+
365
+ return promise;
366
+ }));
367
+
368
+ // skipping due to apparent bug with lolex in all browsers but Chrome.
369
+ skipInBrowser(it)('does not continue attempting to connect', () => {
370
+ mercury.connect();
371
+
372
+ return promiseTick(2)
373
+ .then(() => {
374
+ clock.tick(6 * webex.internal.mercury.config.backoffTimeReset);
375
+
376
+ return promiseTick(2);
377
+ })
378
+ .then(() => {
379
+ assert.calledOnce(Socket.prototype.open);
380
+ });
381
+ });
382
+ });
383
+
384
+ describe.skip('when webSocketUrl is provided', () => {
385
+ it('connects to Mercury with provided url', () => {
386
+ const webSocketUrl = 'ws://providedurl.com';
387
+ const promise = mercury.connect(webSocketUrl);
388
+
389
+ assert.isFalse(mercury.connected, 'Mercury is not connected');
390
+ assert.isTrue(mercury.connecting, 'Mercury is connecting');
391
+ mockWebSocket.open();
392
+
393
+ return promise.then(() => {
394
+ assert.isTrue(mercury.connected, 'Mercury is connected');
395
+ assert.isFalse(mercury.connecting, 'Mercury is not connecting');
396
+ assert.calledWith(
397
+ Socket.prototype.open,
398
+ sinon.match(/ws:\/\/providedurl.com/),
399
+ sinon.match.any
400
+ );
401
+ });
402
+ });
403
+ });
404
+ });
405
+
406
+ describe.skip('Websocket proxy agent', () => {
407
+ afterEach(() => {
408
+ delete webex.config.defaultMercuryOptions;
409
+ });
410
+
411
+ it('connects to Mercury using proxy agent', () => {
412
+ const testProxyUrl = 'http://proxyurl.com:80';
413
+
414
+ webex.config.defaultMercuryOptions = {agent: {proxy: {href: testProxyUrl}}};
415
+ const promise = mercury.connect();
416
+
417
+ assert.isFalse(mercury.connected, 'Mercury is not connected');
418
+ assert.isTrue(mercury.connecting, 'Mercury is connecting');
419
+ mockWebSocket.open();
420
+
421
+ return promise.then(() => {
422
+ assert.isTrue(mercury.connected, 'Mercury is connected');
423
+ assert.isFalse(mercury.connecting, 'Mercury is not connecting');
424
+ assert.calledWith(
425
+ socketOpenStub,
426
+ sinon.match(/ws:\/\/example.com/),
427
+ sinon.match.has(
428
+ 'agent',
429
+ sinon.match.has('proxy', sinon.match.has('href', testProxyUrl))
430
+ )
431
+ );
432
+ });
433
+ });
434
+
435
+ it('connects to Mercury without proxy agent', () => {
436
+ const promise = mercury.connect();
437
+
438
+ assert.isFalse(mercury.connected, 'Mercury is not connected');
439
+ assert.isTrue(mercury.connecting, 'Mercury is connecting');
440
+ mockWebSocket.open();
441
+
442
+ return promise.then(() => {
443
+ assert.isTrue(mercury.connected, 'Mercury is connected');
444
+ assert.isFalse(mercury.connecting, 'Mercury is not connecting');
445
+ assert.calledWith(
446
+ socketOpenStub,
447
+ sinon.match(/ws:\/\/example.com/),
448
+ sinon.match({agent: undefined})
449
+ );
450
+ });
451
+ });
452
+ });
453
+
454
+ describe.skip('#disconnect()', () => {
455
+ it('disconnects the WebSocket', () => mercury.connect()
456
+ .then(() => {
457
+ assert.isTrue(mercury.connected, 'Mercury is connected');
458
+ assert.isFalse(mercury.connecting, 'Mercury is not connecting');
459
+ const promise = mercury.disconnect();
460
+
461
+ mockWebSocket.emit('close', {
462
+ code: 1000,
463
+ reason: 'Done',
464
+ });
465
+
466
+ return promise;
467
+ })
468
+ .then(() => {
469
+ assert.isFalse(mercury.connected, 'Mercury is not connected');
470
+ assert.isFalse(mercury.connecting, 'Mercury is not connecting');
471
+ assert.isUndefined(mercury.mockWebSocket, 'Mercury does not have a mockWebSocket');
472
+ }));
473
+
474
+ it('stops emitting message events', () => {
475
+ const spy = sinon.spy();
476
+
477
+ mercury.on('event:status.start_typing', spy);
478
+
479
+ return mercury
480
+ .connect()
481
+ .then(() => {
482
+ assert.isTrue(mercury.connected, 'Mercury is connected');
483
+ assert.isFalse(mercury.connecting, 'Mercury is not connecting');
484
+
485
+ assert.notCalled(spy);
486
+ mockWebSocket.readyState = 1;
487
+ mockWebSocket.emit('open');
488
+ mockWebSocket.emit('message', {data: statusStartTypingMessage});
489
+ })
490
+ .then(() => {
491
+ assert.calledOnce(spy);
492
+
493
+ const promise = mercury.disconnect();
494
+
495
+ mockWebSocket.readyState = 1;
496
+ mockWebSocket.emit('open');
497
+ mockWebSocket.emit('message', {data: statusStartTypingMessage});
498
+ mockWebSocket.emit('close', {
499
+ code: 1000,
500
+ reason: 'Done',
501
+ });
502
+ mockWebSocket.emit('message', {data: statusStartTypingMessage});
503
+
504
+ return promise;
505
+ })
506
+
507
+ .then(() => {
508
+ mockWebSocket.readyState = 1;
509
+ mockWebSocket.emit('open');
510
+ mockWebSocket.emit('message', {data: statusStartTypingMessage});
511
+ assert.calledOnce(spy);
512
+ });
513
+ });
514
+
515
+ describe('when there is a connection attempt inflight', () => {
516
+ it('stops the attempt when disconnect called', () => {
517
+ socketOpenStub.restore();
518
+ socketOpenStub = sinon.stub(Socket.prototype, 'open');
519
+ socketOpenStub.onCall(0).returns(
520
+ // Delay the opening of the socket so that disconnect is called while open
521
+ // is in progress
522
+ promiseTick(2 * webex.internal.mercury.config.backoffTimeReset)
523
+ // Pretend the socket opened successfully. Failing should be fine too but
524
+ // it generates more console output.
525
+ .then(() => Promise.resolve())
526
+ );
527
+ const promise = mercury.connect();
528
+
529
+ // Wait for the connect call to setup
530
+ return promiseTick(webex.internal.mercury.config.backoffTimeReset).then(() => {
531
+ // By this time backoffCall and mercury socket should be defined by the
532
+ // 'connect' call
533
+ assert.isDefined(mercury.backoffCall, 'Mercury backoffCall is not defined');
534
+ assert.isDefined(mercury.socket, 'Mercury socket is not defined');
535
+ // Calling disconnect will abort the backoffCall, close the socket, and
536
+ // reject the connect
537
+ mercury.disconnect();
538
+ assert.isUndefined(mercury.backoffCall, 'Mercury backoffCall is still defined');
539
+ // The socket will never be unset (which seems bad)
540
+ assert.isDefined(mercury.socket, 'Mercury socket is not defined');
541
+
542
+ return assert.isRejected(promise);
543
+ });
544
+ });
545
+
546
+ it('stops the attempt when backoffCall is undefined', () => {
547
+ socketOpenStub.restore();
548
+ socketOpenStub = sinon.stub(Socket.prototype, 'open');
549
+ socketOpenStub.returns(Promise.resolve());
550
+
551
+ let reason;
552
+
553
+ mercury.backoffCall = undefined;
554
+ mercury._attemptConnection('ws://example.com', (_reason) => {
555
+ reason = _reason;
556
+ });
557
+
558
+ return promiseTick(webex.internal.mercury.config.backoffTimeReset).then(() => {
559
+ assert.equal(
560
+ reason.message,
561
+ 'mercury: prevent socket open when backoffCall no longer defined'
562
+ );
563
+ });
564
+ });
565
+ });
566
+ });
567
+
568
+ describe('#_emit()', () => {
569
+ it('emits Error-safe events', () => {
570
+ mercury.on('break', () => {
571
+ throw new Error();
572
+ });
573
+
574
+ return Promise.resolve(mercury._emit('break'));
575
+ });
576
+ });
577
+
578
+ describe('#_applyOverrides()', () => {
579
+ const lastSeenActivityDate = 'Some date';
580
+ const lastReadableActivityDate = 'Some other date';
581
+
582
+ it('merges a single header field with data', () => {
583
+ const envelope = {
584
+ headers: {
585
+ 'data.activity.target.lastSeenActivityDate': lastSeenActivityDate,
586
+ },
587
+ data: {
588
+ activity: {},
589
+ },
590
+ };
591
+
592
+ mercury._applyOverrides(envelope);
593
+
594
+ assert.equal(envelope.data.activity.target.lastSeenActivityDate, lastSeenActivityDate);
595
+ });
596
+
597
+ it('merges a multiple header fields with data', () => {
598
+ const envelope = {
599
+ headers: {
600
+ 'data.activity.target.lastSeenActivityDate': lastSeenActivityDate,
601
+ 'data.activity.target.lastReadableActivityDate': lastReadableActivityDate,
602
+ },
603
+ data: {
604
+ activity: {},
605
+ },
606
+ };
607
+
608
+ mercury._applyOverrides(envelope);
609
+
610
+ assert.equal(envelope.data.activity.target.lastSeenActivityDate, lastSeenActivityDate);
611
+ assert.equal(
612
+ envelope.data.activity.target.lastReadableActivityDate,
613
+ lastReadableActivityDate
614
+ );
615
+ });
616
+
617
+ it('merges headers when Mercury messages arrive', () => {
618
+ const envelope = {
619
+ headers: {
620
+ 'data.activity.target.lastSeenActivityDate': lastSeenActivityDate,
621
+ },
622
+ data: {
623
+ activity: {},
624
+ },
625
+ };
626
+
627
+ mercury._applyOverrides(envelope);
628
+
629
+ assert.equal(envelope.data.activity.target.lastSeenActivityDate, lastSeenActivityDate);
630
+ });
631
+ });
632
+
633
+ describe('#_prepareUrl()', () => {
634
+ beforeEach(() => {
635
+ webex.internal.device.webSocketUrl = 'ws://example.com';
636
+ });
637
+
638
+ it('uses device default webSocketUrl', () =>
639
+ webex.internal.mercury._prepareUrl().then((wsUrl) => assert.match(wsUrl, /example.com/)));
640
+ it('uses provided webSocketUrl', () =>
641
+ webex.internal.mercury
642
+ ._prepareUrl('ws://provided.com')
643
+ .then((wsUrl) => assert.match(wsUrl, /provided.com/)));
644
+ it('requests text-mode WebSockets', () =>
645
+ webex.internal.mercury
646
+ ._prepareUrl()
647
+ .then((wsUrl) => assert.match(wsUrl, /outboundWireFormat=text/)));
648
+
649
+ it('requests the buffer state message', () =>
650
+ webex.internal.mercury
651
+ ._prepareUrl()
652
+ .then((wsUrl) => assert.match(wsUrl, /bufferStates=true/)));
653
+
654
+ it('does not add conditional properties', () =>
655
+ webex.internal.mercury._prepareUrl().then((wsUrl) => {
656
+ assert.notMatch(wsUrl, /mercuryRegistrationStatus/);
657
+ assert.notMatch(wsUrl, /mercuryRegistrationStatus/);
658
+ assert.notMatch(wsUrl, /isRegistrationRefreshEnabled/);
659
+ assert.notMatch(wsUrl, /multipleConnections/);
660
+ }));
661
+
662
+ describe('when web-high-availability is enabled', () => {
663
+ it('uses webSocketUrl provided by device', () => {
664
+ webex.internal.device.useServiceCatalogUrl = sinon
665
+ .stub()
666
+ .returns(Promise.resolve('ws://example-2.com'));
667
+ webex.internal.feature.getFeature.onCall(0).returns(Promise.resolve(true));
668
+
669
+ return webex.internal.mercury
670
+ ._prepareUrl()
671
+ .then((wsUrl) => assert.match(wsUrl, /example-2.com/));
672
+ });
673
+ });
674
+
675
+ describe("when 'web-shared-socket' is enabled", () => {
676
+ beforeEach(() => {
677
+ webex.internal.feature.getFeature.returns(Promise.resolve(true));
678
+ });
679
+
680
+ it('requests shared socket support', () =>
681
+ webex.internal.mercury
682
+ ._prepareUrl()
683
+ .then((wsUrl) => assert.match(wsUrl, /isRegistrationRefreshEnabled=true/)));
684
+
685
+ it('requests the registration banner', () =>
686
+ webex.internal.mercury
687
+ ._prepareUrl()
688
+ .then((wsUrl) => assert.match(wsUrl, /mercuryRegistrationStatus=true/)));
689
+
690
+ it('does not request the buffer state message', () =>
691
+ webex.internal.mercury._prepareUrl().then((wsUrl) => {
692
+ assert.match(wsUrl, /mercuryRegistrationStatus=true/);
693
+ assert.notMatch(wsUrl, /bufferStates/);
694
+ }));
695
+ });
696
+
697
+ describe('when using an ephemeral device', () => {
698
+ beforeEach(() => {
699
+ webex.config.device.ephemeral = true;
700
+ });
701
+
702
+ it('indicates multiple connections may be coming from this user', () =>
703
+ webex.internal.mercury
704
+ ._prepareUrl()
705
+ .then((wsUrl) => assert.match(wsUrl, /multipleConnections/)));
706
+ });
707
+ });
708
+ });
709
+ });