@webex/internal-plugin-mobius-socket 0.0.0-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.
@@ -0,0 +1,492 @@
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, {config as mercuryConfig, Socket} from '@webex/internal-plugin-mercury';
7
+ import sinon from 'sinon';
8
+ import MockWebex from '@webex/test-helper-mock-webex';
9
+ import MockWebSocket from '@webex/test-helper-mock-web-socket';
10
+ import uuid from 'uuid';
11
+ import FakeTimers from '@sinonjs/fake-timers';
12
+ import {wrap} from 'lodash';
13
+
14
+ import promiseTick from '../lib/promise-tick';
15
+
16
+ describe('plugin-mercury', () => {
17
+ describe('Mercury', () => {
18
+ describe('Events', () => {
19
+ let clock, mercury, mockWebSocket, socketOpenStub, webex;
20
+
21
+ const fakeTestMessage = {
22
+ id: uuid.v4(),
23
+ data: {
24
+ eventType: 'fake.test',
25
+ },
26
+ timestamp: Date.now(),
27
+ trackingId: `suffix_${uuid.v4()}_${Date.now()}`,
28
+ };
29
+
30
+ const statusStartTypingMessage = {
31
+ id: uuid.v4(),
32
+ data: {
33
+ eventType: 'status.start_typing',
34
+ actor: {
35
+ id: 'actorId',
36
+ },
37
+ conversationId: uuid.v4(),
38
+ },
39
+ timestamp: Date.now(),
40
+ trackingId: `suffix_${uuid.v4()}_${Date.now()}`,
41
+ sessionId: 'mercury-default-session',
42
+ };
43
+
44
+ beforeEach(() => {
45
+ clock = FakeTimers.install({now: Date.now()});
46
+ });
47
+
48
+ afterEach(async () => {
49
+ clock.uninstall();
50
+ // Clean up mercury socket and mockWebSocket
51
+ if (mercury && mercury.socket) {
52
+ try {
53
+ await mercury.socket.close();
54
+ } catch (e) {}
55
+ }
56
+ if (mockWebSocket && typeof mockWebSocket.close === 'function') {
57
+ mockWebSocket.close();
58
+ }
59
+ // Restore stubs
60
+ if (Socket.getWebSocketConstructor.restore) {
61
+ Socket.getWebSocketConstructor.restore();
62
+ }
63
+ if (socketOpenStub && socketOpenStub.restore) {
64
+ socketOpenStub.restore();
65
+ }
66
+ });
67
+
68
+ beforeEach(() => {
69
+ webex = new MockWebex({
70
+ children: {
71
+ mercury: Mercury,
72
+ },
73
+ });
74
+
75
+ webex.internal.metrics.submitClientMetrics = sinon.stub();
76
+ webex.internal.newMetrics.callDiagnosticMetrics.setMercuryConnectedStatus = sinon.stub();
77
+ webex.trackingId = 'fakeTrackingId';
78
+ webex.config.mercury = mercuryConfig.mercury;
79
+
80
+ webex.logger = console;
81
+
82
+ mockWebSocket = new MockWebSocket('ws://example.com');
83
+ sinon.stub(Socket, 'getWebSocketConstructor').returns(() => mockWebSocket);
84
+
85
+ const origOpen = Socket.prototype.open;
86
+
87
+ socketOpenStub = sinon.stub(Socket.prototype, 'open').callsFake(function (...args) {
88
+ const promise = Reflect.apply(origOpen, this, args);
89
+
90
+ process.nextTick(() => mockWebSocket.open());
91
+
92
+ return promise;
93
+ });
94
+
95
+ mercury = webex.internal.mercury;
96
+ mercury.defaultSessionId = 'mercury-default-session';
97
+ });
98
+
99
+ afterEach(() => {
100
+ if (socketOpenStub) {
101
+ socketOpenStub.restore();
102
+ }
103
+
104
+ if (Socket.getWebSocketConstructor.restore) {
105
+ Socket.getWebSocketConstructor.restore();
106
+ }
107
+ });
108
+
109
+ describe('when connected', () => {
110
+ it('emits the `online` event', () => {
111
+ const spy = sinon.spy();
112
+
113
+ mercury.on('online', spy);
114
+ const promise = mercury.connect();
115
+
116
+ mockWebSocket.open();
117
+
118
+ return promise.then(() => assert.called(spy));
119
+ });
120
+ });
121
+
122
+ describe('when disconnected', () => {
123
+ it('emits the `offline` event', () => {
124
+ const spy = sinon.spy();
125
+
126
+ mercury.on('offline', spy);
127
+ const promise = mercury.connect();
128
+
129
+ mockWebSocket.open();
130
+
131
+ return promise
132
+ .then(() => {
133
+ const promise = mercury.disconnect();
134
+
135
+ mockWebSocket.emit('close', {
136
+ code: 1000,
137
+ reason: 'Done',
138
+ });
139
+
140
+ return promise;
141
+ })
142
+ .then(() => assert.calledOnce(spy));
143
+ });
144
+
145
+ describe('when reconnected', () => {
146
+ it('emits the `online` event', () => {
147
+ const spy = sinon.spy();
148
+
149
+ mercury.on('online', spy);
150
+
151
+ const promise = mercury.connect();
152
+
153
+ mockWebSocket.open();
154
+
155
+ return promise
156
+ .then(() => assert.calledOnce(spy))
157
+ .then(() => mockWebSocket.emit('close', {code: 1000, reason: 'Idle'}))
158
+ .then(() => mercury.connect())
159
+ .then(() => assert.calledTwice(spy));
160
+ });
161
+ });
162
+ });
163
+
164
+ describe('when `mercury.buffer_state` is received', () => {
165
+ // This test is here because the buffer states message may arrive before
166
+ // the mercury Promise resolves.
167
+ it('gets emitted', (done) => {
168
+ const spy = mockWebSocket.send;
169
+
170
+ assert.notCalled(spy);
171
+ const bufferStateSpy = sinon.spy();
172
+ const onlineSpy = sinon.spy();
173
+
174
+ mercury.on('event:mercury.buffer_state', bufferStateSpy);
175
+ mercury.on('online', onlineSpy);
176
+
177
+ Socket.getWebSocketConstructor.returns(() => {
178
+ process.nextTick(() => {
179
+ assert.isTrue(mercury.connecting, 'Mercury is still connecting');
180
+ assert.isFalse(mercury.connected, 'Mercury has not yet connected');
181
+ assert.notCalled(onlineSpy);
182
+ assert.lengthOf(spy.args, 0, 'The client has not yet sent the auth message');
183
+ // set websocket readystate to 1 to allow a successful send message
184
+ mockWebSocket.readyState = 1;
185
+ mockWebSocket.emit('open');
186
+ mockWebSocket.emit('message', {
187
+ data: JSON.stringify({
188
+ id: uuid.v4(),
189
+ data: {
190
+ eventType: 'mercury.buffer_state',
191
+ },
192
+ }),
193
+ });
194
+ // using lengthOf because notCalled doesn't allow the helpful
195
+ // string assertion
196
+ assert.lengthOf(spy.args, 0, 'The client has not acked the buffer_state message');
197
+
198
+ promiseTick(1)
199
+ .then(() => {
200
+ assert.calledOnce(bufferStateSpy);
201
+
202
+ return mercury.connect().then(done);
203
+ })
204
+ .catch(done);
205
+ });
206
+
207
+ return mockWebSocket;
208
+ });
209
+
210
+ // Delay send for a tick to ensure the buffer message comes before
211
+ // auth completes.
212
+ mockWebSocket.send = wrap(mockWebSocket.send, function (fn, ...args) {
213
+ process.nextTick(() => {
214
+ Reflect.apply(fn, this, args);
215
+ });
216
+ });
217
+ mercury.connect();
218
+ assert.lengthOf(spy.args, 0);
219
+ });
220
+ });
221
+
222
+ describe('when a CloseEvent is received', () => {
223
+ const events = [
224
+ {
225
+ code: 1000,
226
+ reason: 'idle',
227
+ action: 'reconnect',
228
+ },
229
+ {
230
+ code: 1000,
231
+ reason: 'done (forced)',
232
+ action: 'reconnect',
233
+ },
234
+ {
235
+ code: 1000,
236
+ reason: 'pong not received',
237
+ action: 'reconnect',
238
+ },
239
+ {
240
+ code: 1000,
241
+ reason: 'pong mismatch',
242
+ action: 'reconnect',
243
+ },
244
+ {
245
+ code: 1000,
246
+ action: 'close',
247
+ },
248
+ {
249
+ code: 1003,
250
+ action: 'close',
251
+ },
252
+ {
253
+ code: 1001,
254
+ action: 'reconnect',
255
+ },
256
+ {
257
+ code: 1005,
258
+ action: 'reconnect',
259
+ },
260
+ {
261
+ code: 1006,
262
+ action: 'reconnect',
263
+ },
264
+ {
265
+ code: 1011,
266
+ action: 'reconnect',
267
+ },
268
+ {
269
+ code: 4000,
270
+ action: 'replace',
271
+ },
272
+ {
273
+ action: 'close',
274
+ },
275
+ ];
276
+
277
+ events.forEach((def) => {
278
+ const {action, reason, code} = def;
279
+ let description;
280
+
281
+ if (code && reason) {
282
+ description = `with code \`${code}\` and reason \`${reason}\``;
283
+ } else if (code) {
284
+ description = `with code \`${code}\``;
285
+ } else if (reason) {
286
+ description = `with reason \`${reason}\``;
287
+ }
288
+
289
+ describe(`when an event ${description} is received`, () => {
290
+ it(`takes the ${action} action`, () => {
291
+ if (mercury._reconnect.restore) {
292
+ mercury._reconnect.restore();
293
+ }
294
+
295
+ sinon.spy(mercury, 'connect');
296
+
297
+ const offlineSpy = sinon.spy();
298
+ const permanentSpy = sinon.spy();
299
+ const transientSpy = sinon.spy();
300
+ const replacedSpy = sinon.spy();
301
+
302
+ mercury.on('offline', offlineSpy);
303
+ mercury.on('offline.permanent', permanentSpy);
304
+ mercury.on('offline.transient', transientSpy);
305
+ mercury.on('offline.replaced', replacedSpy);
306
+
307
+ const promise = mercury.connect();
308
+
309
+ mockWebSocket.open();
310
+
311
+ return promise
312
+ .then(() => {
313
+ // Make sure mercury.connect has a call count of zero
314
+ mercury.connect.resetHistory();
315
+
316
+ mockWebSocket.emit('close', {code, reason});
317
+
318
+ return promiseTick(1);
319
+ })
320
+ .then(() => {
321
+ assert.called(offlineSpy);
322
+ assert.calledWith(offlineSpy, {code, reason, sessionId: 'mercury-default-session'});
323
+ switch (action) {
324
+ case 'close':
325
+ assert.called(permanentSpy);
326
+ assert.notCalled(transientSpy);
327
+ assert.notCalled(replacedSpy);
328
+ break;
329
+ case 'reconnect':
330
+ assert.notCalled(permanentSpy);
331
+ assert.called(transientSpy);
332
+ assert.notCalled(replacedSpy);
333
+ break;
334
+ case 'replace':
335
+ assert.notCalled(permanentSpy);
336
+ assert.notCalled(transientSpy);
337
+ assert.called(replacedSpy);
338
+ break;
339
+ default:
340
+ assert(false, 'unreachable code reached');
341
+ }
342
+ assert.isFalse(mercury.connected, 'Mercury is not connected');
343
+ if (action === 'reconnect') {
344
+ assert.called(mercury.connect);
345
+ assert.calledWith(mercury.connect, mockWebSocket.url);
346
+ assert.isTrue(mercury.connecting, 'Mercury is connecting');
347
+
348
+ // Block until reconnect completes so logs don't overlap
349
+ return mercury.connect();
350
+ }
351
+
352
+ assert.notCalled(mercury.connect);
353
+ assert.isFalse(mercury.connecting, 'Mercury is not connecting');
354
+
355
+ return Promise.resolve();
356
+ });
357
+ });
358
+ });
359
+ });
360
+ });
361
+
362
+ describe('when a MessageEvent is received', () => {
363
+ it('processes the Event via any autowired event handlers', () => {
364
+ webex.fake = {
365
+ processTestEvent: sinon.spy(),
366
+ };
367
+
368
+ const promise = mercury.connect();
369
+
370
+ mockWebSocket.open();
371
+
372
+ return promise
373
+ .then(() => {
374
+ mockWebSocket.emit('message', {data: JSON.stringify(fakeTestMessage)});
375
+
376
+ return promiseTick(1);
377
+ })
378
+ .then(() => {
379
+ assert.called(webex.fake.processTestEvent);
380
+ });
381
+ });
382
+
383
+ it('emits the Mercury envelope', () => {
384
+ const startSpy = sinon.spy();
385
+ const stopSpy = sinon.spy();
386
+
387
+ mercury.on('event:status.start_typing', startSpy);
388
+ mercury.on('event:status.stop_typing', stopSpy);
389
+
390
+ const promise = mercury.connect();
391
+
392
+ mockWebSocket.open();
393
+
394
+ return promise
395
+ .then(() => {
396
+ mockWebSocket.emit('message', {data: JSON.stringify(statusStartTypingMessage)});
397
+
398
+ return promiseTick(1);
399
+ })
400
+ .then(() => {
401
+ assert.calledOnce(startSpy);
402
+ assert.notCalled(stopSpy);
403
+ assert.calledWith(startSpy, statusStartTypingMessage);
404
+ });
405
+ });
406
+
407
+ it("emits the Mercury envelope named by the Mercury event's eventType", () => {
408
+ const startSpy = sinon.spy();
409
+ const stopSpy = sinon.spy();
410
+
411
+ mercury.on('event:status.start_typing', startSpy);
412
+ mercury.on('event:status.stop_typing', stopSpy);
413
+
414
+ const promise = mercury.connect();
415
+
416
+ mockWebSocket.open();
417
+
418
+ return promise
419
+ .then(() => {
420
+ mockWebSocket.emit('message', {data: JSON.stringify(statusStartTypingMessage)});
421
+
422
+ return promiseTick(1);
423
+ })
424
+ .then(() => {
425
+ assert.calledOnce(startSpy);
426
+ assert.notCalled(stopSpy);
427
+ assert.calledWith(startSpy, statusStartTypingMessage);
428
+ });
429
+ });
430
+ });
431
+
432
+ describe('when a sequence number is skipped', () => {
433
+ it('emits an event', () => {
434
+ const spy = sinon.spy();
435
+
436
+ mercury.on('sequence-mismatch', spy);
437
+ const promise = mercury.connect();
438
+
439
+ mockWebSocket.open();
440
+
441
+ return promise.then(() => {
442
+ mockWebSocket.emit('message', {
443
+ data: JSON.stringify({
444
+ sequenceNumber: 2,
445
+ id: 'mockid',
446
+ data: {
447
+ eventType: 'mercury.buffer_state',
448
+ },
449
+ }),
450
+ });
451
+ mockWebSocket.emit('message', {
452
+ data: JSON.stringify({
453
+ sequenceNumber: 4,
454
+ id: 'mockid',
455
+ data: {
456
+ eventType: 'mercury.buffer_state',
457
+ },
458
+ }),
459
+ });
460
+ assert.called(spy);
461
+ });
462
+ });
463
+ });
464
+ });
465
+ });
466
+
467
+ /*
468
+ // On mercury:
469
+ online
470
+ offline
471
+ offline.transient
472
+ offline.permanent
473
+ offline.replaced
474
+ event
475
+ event:locus.participant_joined
476
+ mockWebSocket.connection-failed
477
+ mockWebSocket.sequence-mismatch
478
+
479
+ // On webex:
480
+ mercury.online
481
+ mercury.offline
482
+ mercury.offline.transient
483
+ mercury.offline.permanent
484
+ mercury.offline.replaced
485
+ mercury.event
486
+ mercury.event:locus.participant_joined
487
+ mercury.mockWebSocket.connection-failed
488
+ mercury.mockWebSocket.sequence-mismatch
489
+
490
+ // TODO go through all it(`emits...`) and make sure corresponding tests are here
491
+ */
492
+ });