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

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,812 +1,812 @@
1
- /*!
2
- * Copyright (c) 2015-2020 Cisco Systems, Inc. See LICENSE file.
3
- */
4
-
5
- import {forEach} from 'lodash';
6
- import {assert} from '@webex/test-helper-chai';
7
- import MockWebSocket from '@webex/test-helper-mock-web-socket';
8
- import sinon from 'sinon';
9
- import {
10
- BadRequest,
11
- NotAuthorized,
12
- Forbidden,
13
- // NotFound,
14
- config,
15
- ConnectionError,
16
- Socket,
17
- } from '@webex/internal-plugin-mercury';
18
- import uuid from 'uuid';
19
- import FakeTimers from '@sinonjs/fake-timers';
20
-
21
- describe('plugin-mercury', () => {
22
- describe('Socket', () => {
23
- let clock, mockWebSocket, socket;
24
-
25
- const mockoptions = Object.assign(
26
- {
27
- logger: console,
28
- token: 'mocktoken',
29
- trackingId: 'mocktrackingid',
30
- },
31
- config.mercury
32
- );
33
-
34
- beforeEach(() => {
35
- clock = FakeTimers.install({now: Date.now()});
36
- });
37
-
38
- afterEach(() => {
39
- clock.uninstall();
40
- });
41
-
42
- beforeEach(() => {
43
- sinon.stub(Socket, 'getWebSocketConstructor').callsFake(() => function (...args) {
44
- mockWebSocket = new MockWebSocket(...args);
45
-
46
- return mockWebSocket;
47
- }
48
- );
49
-
50
- sinon.spy(Socket.prototype, '_ping');
51
-
52
- socket = new Socket();
53
- const promise = socket.open('ws://example.com', mockoptions);
54
-
55
- mockWebSocket.open();
56
-
57
- return promise;
58
- });
59
-
60
- afterEach(() => {
61
- Socket.getWebSocketConstructor.restore();
62
- if (Socket.prototype._ping.restore) {
63
- Socket.prototype._ping.restore();
64
- }
65
-
66
- return Promise.resolve(socket && socket.close()).then(() => {
67
- mockWebSocket = undefined;
68
- socket = undefined;
69
- });
70
- });
71
-
72
- describe.skip('#open()', () => {
73
- let socket;
74
-
75
- beforeEach(() => {
76
- socket = new Socket();
77
- });
78
-
79
- afterEach(() => socket.close().catch(() => console.log()));
80
-
81
- it('requires a url', () => assert.isRejected(socket.open(), /`url` is required/));
82
-
83
- it('requires a forceCloseDelay option', () =>
84
- assert.isRejected(
85
- socket.open('ws://example.com'),
86
- /missing required property forceCloseDelay/
87
- ));
88
-
89
- it('requires a pingInterval option', () =>
90
- assert.isRejected(
91
- socket.open('ws://example.com', {
92
- forceCloseDelay: mockoptions.forceCloseDelay,
93
- }),
94
- /missing required property pingInterval/
95
- ));
96
-
97
- it('requires a pongTimeout option', () =>
98
- assert.isRejected(
99
- socket.open('ws://example.com', {
100
- forceCloseDelay: mockoptions.forceCloseDelay,
101
- pingInterval: mockoptions.pingInterval,
102
- }),
103
- /missing required property pongTimeout/
104
- ));
105
-
106
- it('requires a token option', () =>
107
- assert.isRejected(
108
- socket.open('ws://example.com', {
109
- forceCloseDelay: mockoptions.forceCloseDelay,
110
- pingInterval: mockoptions.pingInterval,
111
- pongTimeout: mockoptions.pongTimeout,
112
- }),
113
- /missing required property token/
114
- ));
115
-
116
- it('requires a trackingId option', () =>
117
- assert.isRejected(
118
- socket.open('ws://example.com', {
119
- forceCloseDelay: mockoptions.forceCloseDelay,
120
- pingInterval: mockoptions.pingInterval,
121
- pongTimeout: mockoptions.pongTimeout,
122
- token: 'mocktoken',
123
- }),
124
- /missing required property trackingId/
125
- ));
126
-
127
- it('requires a logger option', () =>
128
- assert.isRejected(
129
- socket.open('ws://example.com', {
130
- forceCloseDelay: mockoptions.forceCloseDelay,
131
- pingInterval: mockoptions.pingInterval,
132
- pongTimeout: mockoptions.pongTimeout,
133
- token: 'mocktoken',
134
- trackingId: 'mocktrackingid',
135
- }),
136
- /missing required property logger/
137
- ));
138
-
139
- it('accepts a logLevelToken option', () => {
140
- const promise = socket.open('ws://example.com', {
141
- forceCloseDelay: mockoptions.forceCloseDelay,
142
- pingInterval: mockoptions.pingInterval,
143
- pongTimeout: mockoptions.pongTimeout,
144
- logger: console,
145
- token: 'mocktoken',
146
- trackingId: 'mocktrackingid',
147
- logLevelToken: 'mocklogleveltoken',
148
- });
149
-
150
- mockWebSocket.readyState = 1;
151
- mockWebSocket.emit('open');
152
-
153
- mockWebSocket.emit('message', {
154
- data: JSON.stringify({
155
- id: uuid.v4(),
156
- data: {
157
- eventType: 'mercury.buffer_state',
158
- },
159
- }),
160
- });
161
-
162
- return promise.then(() => {
163
- assert.equal(socket.logLevelToken, 'mocklogleveltoken');
164
- });
165
- });
166
- });
167
-
168
- describe('#binaryType', () => {
169
- it('proxies to the underlying socket', () => {
170
- assert.notEqual(socket.binaryType, 'test');
171
- mockWebSocket.binaryType = 'test';
172
- assert.equal(socket.binaryType, 'test');
173
- });
174
- });
175
-
176
- describe('#bufferedAmount', () => {
177
- it('proxies to the underlying socket', () => {
178
- assert.notEqual(socket.bufferedAmount, 'test');
179
- mockWebSocket.bufferedAmount = 'test';
180
- assert.equal(socket.bufferedAmount, 'test');
181
- });
182
- });
183
-
184
- describe('#extensions', () => {
185
- it('proxies to the underlying socket', () => {
186
- assert.notEqual(socket.extensions, 'test');
187
- mockWebSocket.extensions = 'test';
188
- assert.equal(socket.extensions, 'test');
189
- });
190
- });
191
-
192
- describe('#protocol', () => {
193
- it('proxies to the underlying socket', () => {
194
- assert.notEqual(socket.protocol, 'test');
195
- mockWebSocket.protocol = 'test';
196
- assert.equal(socket.protocol, 'test');
197
- });
198
- });
199
-
200
- describe('#readyState', () => {
201
- it('proxies to the underlying socket', () => {
202
- assert.notEqual(socket.readyState, 'test');
203
- mockWebSocket.readyState = 'test';
204
- assert.equal(socket.readyState, 'test');
205
- });
206
- });
207
-
208
- describe('#url', () => {
209
- it('proxies to the underlying socket', () => {
210
- assert.notEqual(socket.url, 'test');
211
- mockWebSocket.url = 'test';
212
- assert.equal(socket.url, 'test');
213
- });
214
- });
215
-
216
- describe.skip('#open()', () => {
217
- it('requires a url parameter', () => {
218
- const s = new Socket();
219
-
220
- return assert.isRejected(s.open(), /`url` is required/);
221
- });
222
-
223
- it('cannot be called more than once', () =>
224
- assert.isRejected(
225
- socket.open('ws://example.com'),
226
- /Socket#open\(\) can only be called once/
227
- ));
228
-
229
- it("sets the underlying socket's binary type", () =>
230
- assert.equal(socket.binaryType, 'arraybuffer'));
231
-
232
- describe('when connection fails because this is a service account', () => {
233
- it('rejects with a BadRequest', () => {
234
- const s = new Socket();
235
- const promise = s.open('ws://example.com', mockoptions);
236
-
237
- mockWebSocket.readyState = 1;
238
- mockWebSocket.emit('open');
239
-
240
- const firstCallArgs = JSON.parse(mockWebSocket.send.firstCall.args[0]);
241
-
242
- assert.equal(firstCallArgs.type, 'authorization');
243
-
244
- mockWebSocket.emit('close', {
245
- code: 4400,
246
- reason: "Service accounts can't use this endpoint",
247
- });
248
-
249
- return assert.isRejected(promise).then((reason) => {
250
- assert.instanceOf(reason, BadRequest);
251
- assert.match(reason.code, 4400);
252
- assert.match(reason.reason, /Service accounts can't use this endpoint/);
253
- assert.match(reason.message, /Service accounts can't use this endpoint/);
254
-
255
- return s.close();
256
- });
257
- });
258
- });
259
-
260
- describe('when connection fails because of an invalid token', () => {
261
- it('rejects with a NotAuthorized', () => {
262
- const s = new Socket();
263
- const promise = s.open('ws://example.com', mockoptions);
264
-
265
- mockWebSocket.readyState = 1;
266
- mockWebSocket.emit('open');
267
-
268
- const firstCallArgs = JSON.parse(mockWebSocket.send.firstCall.args[0]);
269
-
270
- assert.equal(firstCallArgs.type, 'authorization');
271
-
272
- mockWebSocket.emit('close', {
273
- code: 4401,
274
- reason: 'Authorization Failed',
275
- });
276
-
277
- return assert.isRejected(promise).then((reason) => {
278
- assert.instanceOf(reason, NotAuthorized);
279
- assert.match(reason.code, 4401);
280
- assert.match(reason.reason, /Authorization Failed/);
281
- assert.match(reason.message, /Authorization Failed/);
282
-
283
- return s.close();
284
- });
285
- });
286
- });
287
-
288
- describe('when connection fails because of a missing entitlement', () => {
289
- it('rejects with a Forbidden', () => {
290
- const s = new Socket();
291
- const promise = s.open('ws://example.com', mockoptions);
292
-
293
- mockWebSocket.readyState = 1;
294
- mockWebSocket.emit('open');
295
-
296
- const firstCallArgs = JSON.parse(mockWebSocket.send.firstCall.args[0]);
297
-
298
- assert.equal(firstCallArgs.type, 'authorization');
299
-
300
- mockWebSocket.emit('close', {
301
- code: 4403,
302
- reason: 'Not entitled',
303
- });
304
-
305
- return assert.isRejected(promise).then((reason) => {
306
- assert.instanceOf(reason, Forbidden);
307
- assert.match(reason.code, 4403);
308
- assert.match(reason.reason, /Not entitled/);
309
- assert.match(reason.message, /Not entitled/);
310
-
311
- return s.close();
312
- });
313
- });
314
- });
315
-
316
- // describe(`when connection fails because the websocket registation has expired`, () => {
317
- // it(`rejects with a NotFound`, () => {
318
- // const s = new Socket();
319
- // const promise = s.open(`ws://example.com`, mockoptions);
320
- // mockWebSocket.readyState = 1;
321
- // mockWebSocket.emit(`open`);
322
- //
323
- // const firstCallArgs = JSON.parse(mockWebSocket.send.firstCall.args[0]);
324
- // assert.equal(firstCallArgs.type, `authorization`);
325
- //
326
- // mockWebSocket.emit(`close`, {
327
- // code: 4404,
328
- // reason: `Expired registration`
329
- // });
330
- //
331
- // return assert.isRejected(promise)
332
- // .then((reason) => {
333
- // assert.instanceOf(reason, NotFound);
334
- // assert.match(reason.code, 4404);
335
- // assert.match(reason.reason, /Expired registration/);
336
- // assert.match(reason.message, /Expired registration/);
337
- // return s.close();
338
- // });
339
- // });
340
- // });
341
-
342
- describe('when connection fails for non-authorization reasons', () => {
343
- it("rejects with the close event's reason", () => {
344
- const s = new Socket();
345
- const promise = s.open('ws://example.com', mockoptions);
346
-
347
- mockWebSocket.emit('close', {
348
- code: 4001,
349
- reason: 'No',
350
- });
351
-
352
- return assert.isRejected(promise).then((reason) => {
353
- assert.instanceOf(reason, ConnectionError);
354
- assert.match(reason.code, 4001);
355
- assert.match(reason.reason, /No/);
356
- assert.match(reason.message, /No/);
357
-
358
- return s.close();
359
- });
360
- });
361
- });
362
-
363
- describe('when the connection succeeds', () => {
364
- it('sends an auth message up the socket', () => {
365
- const firstCallArgs = JSON.parse(mockWebSocket.send.firstCall.args[0]);
366
-
367
- assert.property(firstCallArgs, 'id');
368
- assert.equal(firstCallArgs.type, 'authorization');
369
- assert.property(firstCallArgs, 'data');
370
- assert.property(firstCallArgs.data, 'token');
371
- assert.equal(firstCallArgs.data.token, 'mocktoken');
372
- assert.equal(firstCallArgs.trackingId, 'mocktrackingid');
373
- assert.notProperty(firstCallArgs, 'logLevelToken');
374
- });
375
-
376
- describe('when logLevelToken is set', () => {
377
- it('includes the logLevelToken in the authorization payload', () => {
378
- const s = new Socket();
379
-
380
- s.open('ws://example.com', {
381
- forceCloseDelay: mockoptions.forceCloseDelay,
382
- pingInterval: mockoptions.pingInterval,
383
- pongTimeout: mockoptions.pongTimeout,
384
- logger: console,
385
- token: 'mocktoken',
386
- trackingId: 'mocktrackingid',
387
- logLevelToken: 'mocklogleveltoken',
388
- }).catch((reason) => console.error(reason));
389
- mockWebSocket.readyState = 1;
390
- mockWebSocket.emit('open');
391
-
392
- const firstCallArgs = JSON.parse(mockWebSocket.send.firstCall.args[0]);
393
-
394
- assert.property(firstCallArgs, 'id');
395
- assert.equal(firstCallArgs.type, 'authorization');
396
- assert.property(firstCallArgs, 'data');
397
- assert.property(firstCallArgs.data, 'token');
398
- assert.equal(firstCallArgs.data.token, 'mocktoken');
399
- assert.equal(firstCallArgs.trackingId, 'mocktrackingid');
400
- assert.equal(firstCallArgs.logLevelToken, 'mocklogleveltoken');
401
-
402
- return s.close();
403
- });
404
- });
405
-
406
- it('kicks off ping/ping', () => assert.calledOnce(socket._ping));
407
-
408
- it('resolves upon successful authorization', () => {
409
- const s = new Socket();
410
- const promise = s.open('ws://example.com', mockoptions);
411
-
412
- mockWebSocket.readyState = 1;
413
- mockWebSocket.emit('open');
414
- mockWebSocket.emit('message', {
415
- data: JSON.stringify({
416
- id: uuid.v4(),
417
- data: {
418
- eventType: 'mercury.buffer_state',
419
- },
420
- }),
421
- });
422
-
423
- return promise.then(() => s.close());
424
- });
425
-
426
- it('resolves upon receiving registration status', () => {
427
- const s = new Socket();
428
- const promise = s.open('ws://example.com', mockoptions);
429
-
430
- mockWebSocket.readyState = 1;
431
- mockWebSocket.emit('open');
432
- mockWebSocket.emit('message', {
433
- data: JSON.stringify({
434
- id: uuid.v4(),
435
- data: {
436
- eventType: 'mercury.registration_status',
437
- },
438
- }),
439
- });
440
-
441
- return promise.then(() => s.close());
442
- });
443
- });
444
- });
445
-
446
- describe('#close()', () => {
447
- it('closes the socket', () => socket.close().then(() => assert.called(mockWebSocket.close)));
448
-
449
- it('only accepts valid close codes', () =>
450
- Promise.all([
451
- assert.isRejected(
452
- socket.close({code: 1001}),
453
- /`options.code` must be 1000 or between 3000 and 4999 \(inclusive\)/
454
- ),
455
- socket.close({code: 1000}),
456
- ]));
457
-
458
- it('accepts a reason', () =>
459
- socket
460
- .close({
461
- code: 3001,
462
- reason: 'Custom Normal',
463
- })
464
- .then(() => assert.calledWith(mockWebSocket.close, 3001, 'Custom Normal')));
465
-
466
- it('can safely be called called multiple times', () => {
467
- const p1 = socket.close();
468
-
469
- mockWebSocket.readyState = 2;
470
- const p2 = socket.close();
471
-
472
- return Promise.all([p1, p2]);
473
- });
474
-
475
- it('signals closure if no close frame is received within the specified window', () => {
476
- const socket = new Socket();
477
- const promise = socket.open('ws://example.com', mockoptions);
478
-
479
- mockWebSocket.readyState = 1;
480
- mockWebSocket.emit('open');
481
- mockWebSocket.emit('message', {
482
- data: JSON.stringify({
483
- id: uuid.v4(),
484
- data: {
485
- eventType: 'mercury.buffer_state',
486
- },
487
- }),
488
- });
489
-
490
- return promise.then(() => {
491
- const spy = sinon.spy();
492
-
493
- socket.on('close', spy);
494
- mockWebSocket.close = () =>
495
- new Promise(() => {
496
- /* eslint no-inline-comments: [0] */
497
- });
498
- mockWebSocket.removeAllListeners('close');
499
-
500
- const promise = socket.close();
501
-
502
- clock.tick(mockoptions.forceCloseDelay);
503
-
504
- return promise.then(() => {
505
- assert.called(spy);
506
- assert.calledWith(spy, {
507
- code: 1000,
508
- reason: 'Done (forced)',
509
- });
510
- });
511
- });
512
- });
513
-
514
- it('cancels any outstanding ping/pong timers', () => {
515
- mockWebSocket.send = sinon.stub();
516
- socket._ping.resetHistory();
517
- const spy = sinon.spy();
518
-
519
- socket.on('close', spy);
520
- socket._ping();
521
- socket.close();
522
- clock.tick(2 * mockoptions.pingInterval);
523
- assert.neverCalledWith(spy, {
524
- code: 1000,
525
- reason: 'Pong not received',
526
- });
527
- assert.calledOnce(socket._ping);
528
- });
529
- });
530
-
531
- describe('#send()', () => {
532
- describe('when the socket is not in the OPEN state', () => {
533
- it('fails', () => {
534
- mockWebSocket.readyState = 0;
535
-
536
- return assert
537
- .isRejected(socket.send('test0'), /INVALID_STATE_ERROR/)
538
- .then(() => {
539
- mockWebSocket.readyState = 2;
540
-
541
- return assert.isRejected(socket.send('test2'), /INVALID_STATE_ERROR/);
542
- })
543
- .then(() => {
544
- mockWebSocket.readyState = 3;
545
-
546
- return assert.isRejected(socket.send('test3'), /INVALID_STATE_ERROR/);
547
- })
548
- .then(() => {
549
- mockWebSocket.readyState = 1;
550
-
551
- return socket.send('test1');
552
- });
553
- });
554
- });
555
-
556
- it('sends strings', () => {
557
- socket.send('this is a string');
558
- assert.calledWith(mockWebSocket.send, 'this is a string');
559
- });
560
-
561
- it('sends JSON.stringifyable object', () => {
562
- socket.send({
563
- json: true,
564
- });
565
- assert.calledWith(mockWebSocket.send, '{"json":true}');
566
- });
567
- });
568
-
569
- describe.skip('#onclose()', () => {
570
- it('stops further ping checks', () => {
571
- socket._ping.resetHistory();
572
- assert.notCalled(socket._ping);
573
- const spy = sinon.spy();
574
-
575
- assert.notCalled(socket._ping);
576
- socket.on('close', spy);
577
- assert.notCalled(socket._ping);
578
- socket._ping();
579
- assert.calledOnce(socket._ping);
580
- mockWebSocket.emit('close', {
581
- code: 1000,
582
- reason: 'Done',
583
- });
584
- assert.calledOnce(socket._ping);
585
- clock.tick(5 * mockoptions.pingInterval);
586
- assert.neverCalledWith(spy, {
587
- code: 1000,
588
- reason: 'Pong not received',
589
- });
590
- assert.calledOnce(socket._ping);
591
- });
592
-
593
- describe('when it receives close code 1005', () => {
594
- forEach(
595
- {
596
- Replaced: 4000,
597
- 'Authentication Failed': 1008,
598
- 'Authentication did not happen within the timeout window of 30000 seconds.': 1008,
599
- },
600
- (code, reason) => {
601
- it(`emits code ${code} for reason ${reason}`, () => {
602
- const spy = sinon.spy();
603
-
604
- socket.on('close', spy);
605
-
606
- mockWebSocket.emit('close', {
607
- code: 1005,
608
- reason,
609
- });
610
- assert.called(spy);
611
- assert.calledWith(spy, {
612
- code,
613
- reason,
614
- });
615
- });
616
- }
617
- );
618
- });
619
- });
620
-
621
- describe('#onmessage()', () => {
622
- let spy;
623
-
624
- beforeEach(() => {
625
- spy = sinon.spy();
626
- socket.on('message', spy);
627
- });
628
-
629
- it('emits messages from the underlying socket', () => {
630
- mockWebSocket.emit('message', {
631
- data: JSON.stringify({
632
- sequenceNumber: 3,
633
- id: 'mockid',
634
- }),
635
- });
636
-
637
- assert.called(spy);
638
- });
639
-
640
- it('parses received messages', () => {
641
- mockWebSocket.emit('message', {
642
- data: JSON.stringify({
643
- sequenceNumber: 3,
644
- id: 'mockid',
645
- }),
646
- });
647
-
648
- assert.calledWith(spy, {
649
- data: {
650
- sequenceNumber: 3,
651
- id: 'mockid',
652
- },
653
- });
654
- });
655
-
656
- it('emits skipped sequence numbers', () => {
657
- const spy2 = sinon.spy();
658
-
659
- socket.on('sequence-mismatch', spy2);
660
-
661
- mockWebSocket.emit('message', {
662
- data: JSON.stringify({
663
- sequenceNumber: 2,
664
- id: 'mockid',
665
- }),
666
- });
667
- assert.notCalled(spy2);
668
-
669
- mockWebSocket.emit('message', {
670
- data: JSON.stringify({
671
- sequenceNumber: 4,
672
- id: 'mockid',
673
- }),
674
- });
675
- assert.calledOnce(spy2);
676
- assert.calledWith(spy2, 4, 3);
677
- });
678
-
679
- it('acknowledges received messages', () => {
680
- sinon.spy(socket, '_acknowledge');
681
- mockWebSocket.emit('message', {
682
- data: JSON.stringify({
683
- sequenceNumber: 5,
684
- id: 'mockid',
685
- }),
686
- });
687
- assert.called(socket._acknowledge);
688
- assert.calledWith(socket._acknowledge, {
689
- data: {
690
- sequenceNumber: 5,
691
- id: 'mockid',
692
- },
693
- });
694
- });
695
-
696
- it('emits pongs separately from other messages', () => {
697
- const pongSpy = sinon.spy();
698
-
699
- socket.on('pong', pongSpy);
700
-
701
- mockWebSocket.emit('message', {
702
- data: JSON.stringify({
703
- sequenceNumber: 5,
704
- id: 'mockid1',
705
- type: 'pong',
706
- }),
707
- });
708
-
709
- assert.calledOnce(pongSpy);
710
- assert.notCalled(spy);
711
-
712
- mockWebSocket.emit('message', {
713
- data: JSON.stringify({
714
- sequenceNumber: 6,
715
- id: 'mockid2',
716
- }),
717
- });
718
-
719
- assert.calledOnce(pongSpy);
720
- assert.calledOnce(spy);
721
- });
722
- });
723
-
724
- describe('#_acknowledge', () => {
725
- it('requires an event', () =>
726
- assert.isRejected(socket._acknowledge(), /`event` is required/));
727
-
728
- it('requires a message id', () =>
729
- assert.isRejected(socket._acknowledge({}), /`event.data.id` is required/));
730
-
731
- it('acknowledges the specified message', () => {
732
- const id = 'mockuuid';
733
-
734
- return socket
735
- ._acknowledge({
736
- data: {
737
- type: 'not an ack',
738
- id,
739
- },
740
- })
741
- .then(() => {
742
- assert.calledWith(
743
- mockWebSocket.send,
744
- JSON.stringify({
745
- messageId: id,
746
- type: 'ack',
747
- })
748
- );
749
- });
750
- });
751
- });
752
-
753
- describe.skip('#_ping()', () => {
754
- let id;
755
-
756
- beforeEach(() => {
757
- id = uuid.v4();
758
- });
759
-
760
- it('sends a ping up the socket', () =>
761
- socket._ping(id).then(() => {
762
- assert.calledWith(
763
- mockWebSocket.send,
764
- JSON.stringify({
765
- id,
766
- type: 'ping',
767
- })
768
- );
769
- }));
770
-
771
- it('considers the socket closed if no pong is received in an acceptable time period', () => {
772
- const spy = sinon.spy();
773
-
774
- socket.on('close', spy);
775
-
776
- mockWebSocket.send = sinon.stub();
777
- socket._ping(id);
778
- clock.tick(2 * mockoptions.pongTimeout);
779
- assert.called(spy);
780
- assert.calledWith(spy, {
781
- code: 1000,
782
- reason: 'Pong not received',
783
- });
784
- });
785
-
786
- it('schedules a future ping', () => {
787
- assert.callCount(socket._ping, 1);
788
- clock.tick(mockoptions.pingInterval);
789
- assert.callCount(socket._ping, 2);
790
- });
791
-
792
- it('closes the socket when an unexpected pong is received', () => {
793
- const spy = sinon.spy();
794
-
795
- socket.on('close', spy);
796
-
797
- socket._ping(2);
798
- mockWebSocket.emit('message', {
799
- data: JSON.stringify({
800
- type: 'pong',
801
- id: 1,
802
- }),
803
- });
804
-
805
- assert.calledWith(spy, {
806
- code: 1000,
807
- reason: 'Pong mismatch',
808
- });
809
- });
810
- });
811
- });
812
- });
1
+ /*!
2
+ * Copyright (c) 2015-2020 Cisco Systems, Inc. See LICENSE file.
3
+ */
4
+
5
+ import {forEach} from 'lodash';
6
+ import {assert} from '@webex/test-helper-chai';
7
+ import MockWebSocket from '@webex/test-helper-mock-web-socket';
8
+ import sinon from 'sinon';
9
+ import {
10
+ BadRequest,
11
+ NotAuthorized,
12
+ Forbidden,
13
+ // NotFound,
14
+ config,
15
+ ConnectionError,
16
+ Socket,
17
+ } from '@webex/internal-plugin-mercury';
18
+ import uuid from 'uuid';
19
+ import FakeTimers from '@sinonjs/fake-timers';
20
+
21
+ describe('plugin-mercury', () => {
22
+ describe('Socket', () => {
23
+ let clock, mockWebSocket, socket;
24
+
25
+ const mockoptions = Object.assign(
26
+ {
27
+ logger: console,
28
+ token: 'mocktoken',
29
+ trackingId: 'mocktrackingid',
30
+ },
31
+ config.mercury
32
+ );
33
+
34
+ beforeEach(() => {
35
+ clock = FakeTimers.install({now: Date.now()});
36
+ });
37
+
38
+ afterEach(() => {
39
+ clock.uninstall();
40
+ });
41
+
42
+ beforeEach(() => {
43
+ sinon.stub(Socket, 'getWebSocketConstructor').callsFake(() => function (...args) {
44
+ mockWebSocket = new MockWebSocket(...args);
45
+
46
+ return mockWebSocket;
47
+ }
48
+ );
49
+
50
+ sinon.spy(Socket.prototype, '_ping');
51
+
52
+ socket = new Socket();
53
+ const promise = socket.open('ws://example.com', mockoptions);
54
+
55
+ mockWebSocket.open();
56
+
57
+ return promise;
58
+ });
59
+
60
+ afterEach(() => {
61
+ Socket.getWebSocketConstructor.restore();
62
+ if (Socket.prototype._ping.restore) {
63
+ Socket.prototype._ping.restore();
64
+ }
65
+
66
+ return Promise.resolve(socket && socket.close()).then(() => {
67
+ mockWebSocket = undefined;
68
+ socket = undefined;
69
+ });
70
+ });
71
+
72
+ describe.skip('#open()', () => {
73
+ let socket;
74
+
75
+ beforeEach(() => {
76
+ socket = new Socket();
77
+ });
78
+
79
+ afterEach(() => socket.close().catch(() => console.log()));
80
+
81
+ it('requires a url', () => assert.isRejected(socket.open(), /`url` is required/));
82
+
83
+ it('requires a forceCloseDelay option', () =>
84
+ assert.isRejected(
85
+ socket.open('ws://example.com'),
86
+ /missing required property forceCloseDelay/
87
+ ));
88
+
89
+ it('requires a pingInterval option', () =>
90
+ assert.isRejected(
91
+ socket.open('ws://example.com', {
92
+ forceCloseDelay: mockoptions.forceCloseDelay,
93
+ }),
94
+ /missing required property pingInterval/
95
+ ));
96
+
97
+ it('requires a pongTimeout option', () =>
98
+ assert.isRejected(
99
+ socket.open('ws://example.com', {
100
+ forceCloseDelay: mockoptions.forceCloseDelay,
101
+ pingInterval: mockoptions.pingInterval,
102
+ }),
103
+ /missing required property pongTimeout/
104
+ ));
105
+
106
+ it('requires a token option', () =>
107
+ assert.isRejected(
108
+ socket.open('ws://example.com', {
109
+ forceCloseDelay: mockoptions.forceCloseDelay,
110
+ pingInterval: mockoptions.pingInterval,
111
+ pongTimeout: mockoptions.pongTimeout,
112
+ }),
113
+ /missing required property token/
114
+ ));
115
+
116
+ it('requires a trackingId option', () =>
117
+ assert.isRejected(
118
+ socket.open('ws://example.com', {
119
+ forceCloseDelay: mockoptions.forceCloseDelay,
120
+ pingInterval: mockoptions.pingInterval,
121
+ pongTimeout: mockoptions.pongTimeout,
122
+ token: 'mocktoken',
123
+ }),
124
+ /missing required property trackingId/
125
+ ));
126
+
127
+ it('requires a logger option', () =>
128
+ assert.isRejected(
129
+ socket.open('ws://example.com', {
130
+ forceCloseDelay: mockoptions.forceCloseDelay,
131
+ pingInterval: mockoptions.pingInterval,
132
+ pongTimeout: mockoptions.pongTimeout,
133
+ token: 'mocktoken',
134
+ trackingId: 'mocktrackingid',
135
+ }),
136
+ /missing required property logger/
137
+ ));
138
+
139
+ it('accepts a logLevelToken option', () => {
140
+ const promise = socket.open('ws://example.com', {
141
+ forceCloseDelay: mockoptions.forceCloseDelay,
142
+ pingInterval: mockoptions.pingInterval,
143
+ pongTimeout: mockoptions.pongTimeout,
144
+ logger: console,
145
+ token: 'mocktoken',
146
+ trackingId: 'mocktrackingid',
147
+ logLevelToken: 'mocklogleveltoken',
148
+ });
149
+
150
+ mockWebSocket.readyState = 1;
151
+ mockWebSocket.emit('open');
152
+
153
+ mockWebSocket.emit('message', {
154
+ data: JSON.stringify({
155
+ id: uuid.v4(),
156
+ data: {
157
+ eventType: 'mercury.buffer_state',
158
+ },
159
+ }),
160
+ });
161
+
162
+ return promise.then(() => {
163
+ assert.equal(socket.logLevelToken, 'mocklogleveltoken');
164
+ });
165
+ });
166
+ });
167
+
168
+ describe('#binaryType', () => {
169
+ it('proxies to the underlying socket', () => {
170
+ assert.notEqual(socket.binaryType, 'test');
171
+ mockWebSocket.binaryType = 'test';
172
+ assert.equal(socket.binaryType, 'test');
173
+ });
174
+ });
175
+
176
+ describe('#bufferedAmount', () => {
177
+ it('proxies to the underlying socket', () => {
178
+ assert.notEqual(socket.bufferedAmount, 'test');
179
+ mockWebSocket.bufferedAmount = 'test';
180
+ assert.equal(socket.bufferedAmount, 'test');
181
+ });
182
+ });
183
+
184
+ describe('#extensions', () => {
185
+ it('proxies to the underlying socket', () => {
186
+ assert.notEqual(socket.extensions, 'test');
187
+ mockWebSocket.extensions = 'test';
188
+ assert.equal(socket.extensions, 'test');
189
+ });
190
+ });
191
+
192
+ describe('#protocol', () => {
193
+ it('proxies to the underlying socket', () => {
194
+ assert.notEqual(socket.protocol, 'test');
195
+ mockWebSocket.protocol = 'test';
196
+ assert.equal(socket.protocol, 'test');
197
+ });
198
+ });
199
+
200
+ describe('#readyState', () => {
201
+ it('proxies to the underlying socket', () => {
202
+ assert.notEqual(socket.readyState, 'test');
203
+ mockWebSocket.readyState = 'test';
204
+ assert.equal(socket.readyState, 'test');
205
+ });
206
+ });
207
+
208
+ describe('#url', () => {
209
+ it('proxies to the underlying socket', () => {
210
+ assert.notEqual(socket.url, 'test');
211
+ mockWebSocket.url = 'test';
212
+ assert.equal(socket.url, 'test');
213
+ });
214
+ });
215
+
216
+ describe.skip('#open()', () => {
217
+ it('requires a url parameter', () => {
218
+ const s = new Socket();
219
+
220
+ return assert.isRejected(s.open(), /`url` is required/);
221
+ });
222
+
223
+ it('cannot be called more than once', () =>
224
+ assert.isRejected(
225
+ socket.open('ws://example.com'),
226
+ /Socket#open\(\) can only be called once/
227
+ ));
228
+
229
+ it("sets the underlying socket's binary type", () =>
230
+ assert.equal(socket.binaryType, 'arraybuffer'));
231
+
232
+ describe('when connection fails because this is a service account', () => {
233
+ it('rejects with a BadRequest', () => {
234
+ const s = new Socket();
235
+ const promise = s.open('ws://example.com', mockoptions);
236
+
237
+ mockWebSocket.readyState = 1;
238
+ mockWebSocket.emit('open');
239
+
240
+ const firstCallArgs = JSON.parse(mockWebSocket.send.firstCall.args[0]);
241
+
242
+ assert.equal(firstCallArgs.type, 'authorization');
243
+
244
+ mockWebSocket.emit('close', {
245
+ code: 4400,
246
+ reason: "Service accounts can't use this endpoint",
247
+ });
248
+
249
+ return assert.isRejected(promise).then((reason) => {
250
+ assert.instanceOf(reason, BadRequest);
251
+ assert.match(reason.code, 4400);
252
+ assert.match(reason.reason, /Service accounts can't use this endpoint/);
253
+ assert.match(reason.message, /Service accounts can't use this endpoint/);
254
+
255
+ return s.close();
256
+ });
257
+ });
258
+ });
259
+
260
+ describe('when connection fails because of an invalid token', () => {
261
+ it('rejects with a NotAuthorized', () => {
262
+ const s = new Socket();
263
+ const promise = s.open('ws://example.com', mockoptions);
264
+
265
+ mockWebSocket.readyState = 1;
266
+ mockWebSocket.emit('open');
267
+
268
+ const firstCallArgs = JSON.parse(mockWebSocket.send.firstCall.args[0]);
269
+
270
+ assert.equal(firstCallArgs.type, 'authorization');
271
+
272
+ mockWebSocket.emit('close', {
273
+ code: 4401,
274
+ reason: 'Authorization Failed',
275
+ });
276
+
277
+ return assert.isRejected(promise).then((reason) => {
278
+ assert.instanceOf(reason, NotAuthorized);
279
+ assert.match(reason.code, 4401);
280
+ assert.match(reason.reason, /Authorization Failed/);
281
+ assert.match(reason.message, /Authorization Failed/);
282
+
283
+ return s.close();
284
+ });
285
+ });
286
+ });
287
+
288
+ describe('when connection fails because of a missing entitlement', () => {
289
+ it('rejects with a Forbidden', () => {
290
+ const s = new Socket();
291
+ const promise = s.open('ws://example.com', mockoptions);
292
+
293
+ mockWebSocket.readyState = 1;
294
+ mockWebSocket.emit('open');
295
+
296
+ const firstCallArgs = JSON.parse(mockWebSocket.send.firstCall.args[0]);
297
+
298
+ assert.equal(firstCallArgs.type, 'authorization');
299
+
300
+ mockWebSocket.emit('close', {
301
+ code: 4403,
302
+ reason: 'Not entitled',
303
+ });
304
+
305
+ return assert.isRejected(promise).then((reason) => {
306
+ assert.instanceOf(reason, Forbidden);
307
+ assert.match(reason.code, 4403);
308
+ assert.match(reason.reason, /Not entitled/);
309
+ assert.match(reason.message, /Not entitled/);
310
+
311
+ return s.close();
312
+ });
313
+ });
314
+ });
315
+
316
+ // describe(`when connection fails because the websocket registation has expired`, () => {
317
+ // it(`rejects with a NotFound`, () => {
318
+ // const s = new Socket();
319
+ // const promise = s.open(`ws://example.com`, mockoptions);
320
+ // mockWebSocket.readyState = 1;
321
+ // mockWebSocket.emit(`open`);
322
+ //
323
+ // const firstCallArgs = JSON.parse(mockWebSocket.send.firstCall.args[0]);
324
+ // assert.equal(firstCallArgs.type, `authorization`);
325
+ //
326
+ // mockWebSocket.emit(`close`, {
327
+ // code: 4404,
328
+ // reason: `Expired registration`
329
+ // });
330
+ //
331
+ // return assert.isRejected(promise)
332
+ // .then((reason) => {
333
+ // assert.instanceOf(reason, NotFound);
334
+ // assert.match(reason.code, 4404);
335
+ // assert.match(reason.reason, /Expired registration/);
336
+ // assert.match(reason.message, /Expired registration/);
337
+ // return s.close();
338
+ // });
339
+ // });
340
+ // });
341
+
342
+ describe('when connection fails for non-authorization reasons', () => {
343
+ it("rejects with the close event's reason", () => {
344
+ const s = new Socket();
345
+ const promise = s.open('ws://example.com', mockoptions);
346
+
347
+ mockWebSocket.emit('close', {
348
+ code: 4001,
349
+ reason: 'No',
350
+ });
351
+
352
+ return assert.isRejected(promise).then((reason) => {
353
+ assert.instanceOf(reason, ConnectionError);
354
+ assert.match(reason.code, 4001);
355
+ assert.match(reason.reason, /No/);
356
+ assert.match(reason.message, /No/);
357
+
358
+ return s.close();
359
+ });
360
+ });
361
+ });
362
+
363
+ describe('when the connection succeeds', () => {
364
+ it('sends an auth message up the socket', () => {
365
+ const firstCallArgs = JSON.parse(mockWebSocket.send.firstCall.args[0]);
366
+
367
+ assert.property(firstCallArgs, 'id');
368
+ assert.equal(firstCallArgs.type, 'authorization');
369
+ assert.property(firstCallArgs, 'data');
370
+ assert.property(firstCallArgs.data, 'token');
371
+ assert.equal(firstCallArgs.data.token, 'mocktoken');
372
+ assert.equal(firstCallArgs.trackingId, 'mocktrackingid');
373
+ assert.notProperty(firstCallArgs, 'logLevelToken');
374
+ });
375
+
376
+ describe('when logLevelToken is set', () => {
377
+ it('includes the logLevelToken in the authorization payload', () => {
378
+ const s = new Socket();
379
+
380
+ s.open('ws://example.com', {
381
+ forceCloseDelay: mockoptions.forceCloseDelay,
382
+ pingInterval: mockoptions.pingInterval,
383
+ pongTimeout: mockoptions.pongTimeout,
384
+ logger: console,
385
+ token: 'mocktoken',
386
+ trackingId: 'mocktrackingid',
387
+ logLevelToken: 'mocklogleveltoken',
388
+ }).catch((reason) => console.error(reason));
389
+ mockWebSocket.readyState = 1;
390
+ mockWebSocket.emit('open');
391
+
392
+ const firstCallArgs = JSON.parse(mockWebSocket.send.firstCall.args[0]);
393
+
394
+ assert.property(firstCallArgs, 'id');
395
+ assert.equal(firstCallArgs.type, 'authorization');
396
+ assert.property(firstCallArgs, 'data');
397
+ assert.property(firstCallArgs.data, 'token');
398
+ assert.equal(firstCallArgs.data.token, 'mocktoken');
399
+ assert.equal(firstCallArgs.trackingId, 'mocktrackingid');
400
+ assert.equal(firstCallArgs.logLevelToken, 'mocklogleveltoken');
401
+
402
+ return s.close();
403
+ });
404
+ });
405
+
406
+ it('kicks off ping/ping', () => assert.calledOnce(socket._ping));
407
+
408
+ it('resolves upon successful authorization', () => {
409
+ const s = new Socket();
410
+ const promise = s.open('ws://example.com', mockoptions);
411
+
412
+ mockWebSocket.readyState = 1;
413
+ mockWebSocket.emit('open');
414
+ mockWebSocket.emit('message', {
415
+ data: JSON.stringify({
416
+ id: uuid.v4(),
417
+ data: {
418
+ eventType: 'mercury.buffer_state',
419
+ },
420
+ }),
421
+ });
422
+
423
+ return promise.then(() => s.close());
424
+ });
425
+
426
+ it('resolves upon receiving registration status', () => {
427
+ const s = new Socket();
428
+ const promise = s.open('ws://example.com', mockoptions);
429
+
430
+ mockWebSocket.readyState = 1;
431
+ mockWebSocket.emit('open');
432
+ mockWebSocket.emit('message', {
433
+ data: JSON.stringify({
434
+ id: uuid.v4(),
435
+ data: {
436
+ eventType: 'mercury.registration_status',
437
+ },
438
+ }),
439
+ });
440
+
441
+ return promise.then(() => s.close());
442
+ });
443
+ });
444
+ });
445
+
446
+ describe('#close()', () => {
447
+ it('closes the socket', () => socket.close().then(() => assert.called(mockWebSocket.close)));
448
+
449
+ it('only accepts valid close codes', () =>
450
+ Promise.all([
451
+ assert.isRejected(
452
+ socket.close({code: 1001}),
453
+ /`options.code` must be 1000 or between 3000 and 4999 \(inclusive\)/
454
+ ),
455
+ socket.close({code: 1000}),
456
+ ]));
457
+
458
+ it('accepts a reason', () =>
459
+ socket
460
+ .close({
461
+ code: 3001,
462
+ reason: 'Custom Normal',
463
+ })
464
+ .then(() => assert.calledWith(mockWebSocket.close, 3001, 'Custom Normal')));
465
+
466
+ it('can safely be called called multiple times', () => {
467
+ const p1 = socket.close();
468
+
469
+ mockWebSocket.readyState = 2;
470
+ const p2 = socket.close();
471
+
472
+ return Promise.all([p1, p2]);
473
+ });
474
+
475
+ it('signals closure if no close frame is received within the specified window', () => {
476
+ const socket = new Socket();
477
+ const promise = socket.open('ws://example.com', mockoptions);
478
+
479
+ mockWebSocket.readyState = 1;
480
+ mockWebSocket.emit('open');
481
+ mockWebSocket.emit('message', {
482
+ data: JSON.stringify({
483
+ id: uuid.v4(),
484
+ data: {
485
+ eventType: 'mercury.buffer_state',
486
+ },
487
+ }),
488
+ });
489
+
490
+ return promise.then(() => {
491
+ const spy = sinon.spy();
492
+
493
+ socket.on('close', spy);
494
+ mockWebSocket.close = () =>
495
+ new Promise(() => {
496
+ /* eslint no-inline-comments: [0] */
497
+ });
498
+ mockWebSocket.removeAllListeners('close');
499
+
500
+ const promise = socket.close();
501
+
502
+ clock.tick(mockoptions.forceCloseDelay);
503
+
504
+ return promise.then(() => {
505
+ assert.called(spy);
506
+ assert.calledWith(spy, {
507
+ code: 1000,
508
+ reason: 'Done (forced)',
509
+ });
510
+ });
511
+ });
512
+ });
513
+
514
+ it('cancels any outstanding ping/pong timers', () => {
515
+ mockWebSocket.send = sinon.stub();
516
+ socket._ping.resetHistory();
517
+ const spy = sinon.spy();
518
+
519
+ socket.on('close', spy);
520
+ socket._ping();
521
+ socket.close();
522
+ clock.tick(2 * mockoptions.pingInterval);
523
+ assert.neverCalledWith(spy, {
524
+ code: 1000,
525
+ reason: 'Pong not received',
526
+ });
527
+ assert.calledOnce(socket._ping);
528
+ });
529
+ });
530
+
531
+ describe('#send()', () => {
532
+ describe('when the socket is not in the OPEN state', () => {
533
+ it('fails', () => {
534
+ mockWebSocket.readyState = 0;
535
+
536
+ return assert
537
+ .isRejected(socket.send('test0'), /INVALID_STATE_ERROR/)
538
+ .then(() => {
539
+ mockWebSocket.readyState = 2;
540
+
541
+ return assert.isRejected(socket.send('test2'), /INVALID_STATE_ERROR/);
542
+ })
543
+ .then(() => {
544
+ mockWebSocket.readyState = 3;
545
+
546
+ return assert.isRejected(socket.send('test3'), /INVALID_STATE_ERROR/);
547
+ })
548
+ .then(() => {
549
+ mockWebSocket.readyState = 1;
550
+
551
+ return socket.send('test1');
552
+ });
553
+ });
554
+ });
555
+
556
+ it('sends strings', () => {
557
+ socket.send('this is a string');
558
+ assert.calledWith(mockWebSocket.send, 'this is a string');
559
+ });
560
+
561
+ it('sends JSON.stringifyable object', () => {
562
+ socket.send({
563
+ json: true,
564
+ });
565
+ assert.calledWith(mockWebSocket.send, '{"json":true}');
566
+ });
567
+ });
568
+
569
+ describe.skip('#onclose()', () => {
570
+ it('stops further ping checks', () => {
571
+ socket._ping.resetHistory();
572
+ assert.notCalled(socket._ping);
573
+ const spy = sinon.spy();
574
+
575
+ assert.notCalled(socket._ping);
576
+ socket.on('close', spy);
577
+ assert.notCalled(socket._ping);
578
+ socket._ping();
579
+ assert.calledOnce(socket._ping);
580
+ mockWebSocket.emit('close', {
581
+ code: 1000,
582
+ reason: 'Done',
583
+ });
584
+ assert.calledOnce(socket._ping);
585
+ clock.tick(5 * mockoptions.pingInterval);
586
+ assert.neverCalledWith(spy, {
587
+ code: 1000,
588
+ reason: 'Pong not received',
589
+ });
590
+ assert.calledOnce(socket._ping);
591
+ });
592
+
593
+ describe('when it receives close code 1005', () => {
594
+ forEach(
595
+ {
596
+ Replaced: 4000,
597
+ 'Authentication Failed': 1008,
598
+ 'Authentication did not happen within the timeout window of 30000 seconds.': 1008,
599
+ },
600
+ (code, reason) => {
601
+ it(`emits code ${code} for reason ${reason}`, () => {
602
+ const spy = sinon.spy();
603
+
604
+ socket.on('close', spy);
605
+
606
+ mockWebSocket.emit('close', {
607
+ code: 1005,
608
+ reason,
609
+ });
610
+ assert.called(spy);
611
+ assert.calledWith(spy, {
612
+ code,
613
+ reason,
614
+ });
615
+ });
616
+ }
617
+ );
618
+ });
619
+ });
620
+
621
+ describe('#onmessage()', () => {
622
+ let spy;
623
+
624
+ beforeEach(() => {
625
+ spy = sinon.spy();
626
+ socket.on('message', spy);
627
+ });
628
+
629
+ it('emits messages from the underlying socket', () => {
630
+ mockWebSocket.emit('message', {
631
+ data: JSON.stringify({
632
+ sequenceNumber: 3,
633
+ id: 'mockid',
634
+ }),
635
+ });
636
+
637
+ assert.called(spy);
638
+ });
639
+
640
+ it('parses received messages', () => {
641
+ mockWebSocket.emit('message', {
642
+ data: JSON.stringify({
643
+ sequenceNumber: 3,
644
+ id: 'mockid',
645
+ }),
646
+ });
647
+
648
+ assert.calledWith(spy, {
649
+ data: {
650
+ sequenceNumber: 3,
651
+ id: 'mockid',
652
+ },
653
+ });
654
+ });
655
+
656
+ it('emits skipped sequence numbers', () => {
657
+ const spy2 = sinon.spy();
658
+
659
+ socket.on('sequence-mismatch', spy2);
660
+
661
+ mockWebSocket.emit('message', {
662
+ data: JSON.stringify({
663
+ sequenceNumber: 2,
664
+ id: 'mockid',
665
+ }),
666
+ });
667
+ assert.notCalled(spy2);
668
+
669
+ mockWebSocket.emit('message', {
670
+ data: JSON.stringify({
671
+ sequenceNumber: 4,
672
+ id: 'mockid',
673
+ }),
674
+ });
675
+ assert.calledOnce(spy2);
676
+ assert.calledWith(spy2, 4, 3);
677
+ });
678
+
679
+ it('acknowledges received messages', () => {
680
+ sinon.spy(socket, '_acknowledge');
681
+ mockWebSocket.emit('message', {
682
+ data: JSON.stringify({
683
+ sequenceNumber: 5,
684
+ id: 'mockid',
685
+ }),
686
+ });
687
+ assert.called(socket._acknowledge);
688
+ assert.calledWith(socket._acknowledge, {
689
+ data: {
690
+ sequenceNumber: 5,
691
+ id: 'mockid',
692
+ },
693
+ });
694
+ });
695
+
696
+ it('emits pongs separately from other messages', () => {
697
+ const pongSpy = sinon.spy();
698
+
699
+ socket.on('pong', pongSpy);
700
+
701
+ mockWebSocket.emit('message', {
702
+ data: JSON.stringify({
703
+ sequenceNumber: 5,
704
+ id: 'mockid1',
705
+ type: 'pong',
706
+ }),
707
+ });
708
+
709
+ assert.calledOnce(pongSpy);
710
+ assert.notCalled(spy);
711
+
712
+ mockWebSocket.emit('message', {
713
+ data: JSON.stringify({
714
+ sequenceNumber: 6,
715
+ id: 'mockid2',
716
+ }),
717
+ });
718
+
719
+ assert.calledOnce(pongSpy);
720
+ assert.calledOnce(spy);
721
+ });
722
+ });
723
+
724
+ describe('#_acknowledge', () => {
725
+ it('requires an event', () =>
726
+ assert.isRejected(socket._acknowledge(), /`event` is required/));
727
+
728
+ it('requires a message id', () =>
729
+ assert.isRejected(socket._acknowledge({}), /`event.data.id` is required/));
730
+
731
+ it('acknowledges the specified message', () => {
732
+ const id = 'mockuuid';
733
+
734
+ return socket
735
+ ._acknowledge({
736
+ data: {
737
+ type: 'not an ack',
738
+ id,
739
+ },
740
+ })
741
+ .then(() => {
742
+ assert.calledWith(
743
+ mockWebSocket.send,
744
+ JSON.stringify({
745
+ messageId: id,
746
+ type: 'ack',
747
+ })
748
+ );
749
+ });
750
+ });
751
+ });
752
+
753
+ describe.skip('#_ping()', () => {
754
+ let id;
755
+
756
+ beforeEach(() => {
757
+ id = uuid.v4();
758
+ });
759
+
760
+ it('sends a ping up the socket', () =>
761
+ socket._ping(id).then(() => {
762
+ assert.calledWith(
763
+ mockWebSocket.send,
764
+ JSON.stringify({
765
+ id,
766
+ type: 'ping',
767
+ })
768
+ );
769
+ }));
770
+
771
+ it('considers the socket closed if no pong is received in an acceptable time period', () => {
772
+ const spy = sinon.spy();
773
+
774
+ socket.on('close', spy);
775
+
776
+ mockWebSocket.send = sinon.stub();
777
+ socket._ping(id);
778
+ clock.tick(2 * mockoptions.pongTimeout);
779
+ assert.called(spy);
780
+ assert.calledWith(spy, {
781
+ code: 1000,
782
+ reason: 'Pong not received',
783
+ });
784
+ });
785
+
786
+ it('schedules a future ping', () => {
787
+ assert.callCount(socket._ping, 1);
788
+ clock.tick(mockoptions.pingInterval);
789
+ assert.callCount(socket._ping, 2);
790
+ });
791
+
792
+ it('closes the socket when an unexpected pong is received', () => {
793
+ const spy = sinon.spy();
794
+
795
+ socket.on('close', spy);
796
+
797
+ socket._ping(2);
798
+ mockWebSocket.emit('message', {
799
+ data: JSON.stringify({
800
+ type: 'pong',
801
+ id: 1,
802
+ }),
803
+ });
804
+
805
+ assert.calledWith(spy, {
806
+ code: 1000,
807
+ reason: 'Pong mismatch',
808
+ });
809
+ });
810
+ });
811
+ });
812
+ });