@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.
- package/.eslintrc.js +6 -0
- package/README.md +131 -0
- package/babel.config.js +3 -0
- package/dist/config.js +47 -0
- package/dist/config.js.map +1 -0
- package/dist/errors.js +106 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.js +80 -0
- package/dist/index.js.map +1 -0
- package/dist/mercury.js +916 -0
- package/dist/mercury.js.map +1 -0
- package/dist/socket/constants.js +16 -0
- package/dist/socket/constants.js.map +1 -0
- package/dist/socket/index.js +15 -0
- package/dist/socket/index.js.map +1 -0
- package/dist/socket/socket-base.js +537 -0
- package/dist/socket/socket-base.js.map +1 -0
- package/dist/socket/socket.js +19 -0
- package/dist/socket/socket.js.map +1 -0
- package/dist/socket/socket.shim.js +36 -0
- package/dist/socket/socket.shim.js.map +1 -0
- package/jest.config.js +3 -0
- package/package.json +68 -0
- package/process +1 -0
- package/src/config.js +40 -0
- package/src/errors.js +66 -0
- package/src/index.js +32 -0
- package/src/mercury.js +1059 -0
- package/src/socket/constants.js +6 -0
- package/src/socket/index.js +5 -0
- package/src/socket/socket-base.js +558 -0
- package/src/socket/socket.js +13 -0
- package/src/socket/socket.shim.js +31 -0
- package/test/integration/spec/mercury.js +117 -0
- package/test/integration/spec/sharable-mercury.js +59 -0
- package/test/integration/spec/webex.js +44 -0
- package/test/unit/lib/promise-tick.js +19 -0
- package/test/unit/spec/mercury-events.js +492 -0
- package/test/unit/spec/mercury.js +1787 -0
- package/test/unit/spec/socket.js +1037 -0
|
@@ -0,0 +1,1787 @@
|
|
|
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
|
+
switchActiveClusterIds: sinon.stub(),
|
|
79
|
+
invalidateCache: sinon.stub(),
|
|
80
|
+
isValidHost: sinon.stub().returns(Promise.resolve(true)),
|
|
81
|
+
};
|
|
82
|
+
webex.internal.metrics.submitClientMetrics = sinon.stub();
|
|
83
|
+
webex.internal.newMetrics.callDiagnosticMetrics.setMercuryConnectedStatus = sinon.stub();
|
|
84
|
+
webex.trackingId = 'fakeTrackingId';
|
|
85
|
+
webex.config.mercury = mercuryConfig.mercury;
|
|
86
|
+
|
|
87
|
+
webex.logger = console;
|
|
88
|
+
|
|
89
|
+
mockWebSocket = new MockWebSocket();
|
|
90
|
+
sinon.stub(Socket, 'getWebSocketConstructor').returns(() => mockWebSocket);
|
|
91
|
+
|
|
92
|
+
const origOpen = Socket.prototype.open;
|
|
93
|
+
|
|
94
|
+
socketOpenStub = sinon.stub(Socket.prototype, 'open').callsFake(function (...args) {
|
|
95
|
+
const promise = Reflect.apply(origOpen, this, args);
|
|
96
|
+
|
|
97
|
+
process.nextTick(() => mockWebSocket.open());
|
|
98
|
+
|
|
99
|
+
return promise;
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
mercury = webex.internal.mercury;
|
|
103
|
+
mercury.defaultSessionId = 'mercury-default-session';
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
afterEach(async () => {
|
|
107
|
+
// Clean up Mercury connections and internal state
|
|
108
|
+
if (mercury) {
|
|
109
|
+
try {
|
|
110
|
+
await mercury.disconnectAll();
|
|
111
|
+
} catch (e) {
|
|
112
|
+
// Ignore cleanup errors
|
|
113
|
+
}
|
|
114
|
+
// Clear any remaining connection promises
|
|
115
|
+
if (mercury._connectPromises) {
|
|
116
|
+
mercury._connectPromises.clear();
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Ensure mock socket is properly closed
|
|
121
|
+
if (mockWebSocket && typeof mockWebSocket.close === 'function') {
|
|
122
|
+
try {
|
|
123
|
+
mockWebSocket.close();
|
|
124
|
+
} catch (e) {
|
|
125
|
+
// Ignore cleanup errors
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (socketOpenStub) {
|
|
130
|
+
socketOpenStub.restore();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (Socket.getWebSocketConstructor.restore) {
|
|
134
|
+
Socket.getWebSocketConstructor.restore();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Small delay to ensure all async operations complete
|
|
138
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
describe('#listen()', () => {
|
|
142
|
+
it('proxies to #connect()', () => {
|
|
143
|
+
const connectStub = sinon.stub(mercury, 'connect').callThrough();
|
|
144
|
+
return mercury.listen().then(() => {
|
|
145
|
+
assert.called(connectStub);
|
|
146
|
+
assert.calledWith(
|
|
147
|
+
webex.internal.newMetrics.callDiagnosticMetrics.setMercuryConnectedStatus,
|
|
148
|
+
true
|
|
149
|
+
);
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
describe('#stopListening()', () => {
|
|
155
|
+
it('proxies to #disconnect()', () => {
|
|
156
|
+
return mercury.connect().then(() => {
|
|
157
|
+
webex.internal.newMetrics.callDiagnosticMetrics.setMercuryConnectedStatus.resetHistory();
|
|
158
|
+
const disconnectStub = sinon.stub(mercury, 'disconnect').callThrough();
|
|
159
|
+
|
|
160
|
+
mercury.stopListening();
|
|
161
|
+
assert.called(disconnectStub);
|
|
162
|
+
mockWebSocket.emit('close', {code: 1000, reason: 'test'});
|
|
163
|
+
assert.calledWith(
|
|
164
|
+
webex.internal.newMetrics.callDiagnosticMetrics.setMercuryConnectedStatus,
|
|
165
|
+
false
|
|
166
|
+
);
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
describe('#connect()', () => {
|
|
172
|
+
it('lazily registers the device', () => {
|
|
173
|
+
webex.internal.device.registered = false;
|
|
174
|
+
assert.notCalled(webex.internal.device.register);
|
|
175
|
+
const promise = mercury.connect();
|
|
176
|
+
|
|
177
|
+
mockWebSocket.open();
|
|
178
|
+
|
|
179
|
+
return promise.then(() => {
|
|
180
|
+
assert.calledOnce(webex.internal.device.register);
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('connects to Mercury using default url', () => {
|
|
185
|
+
webex.internal.feature.updateFeature = sinon.stub();
|
|
186
|
+
const promise = mercury.connect();
|
|
187
|
+
const envelope = {
|
|
188
|
+
data: {
|
|
189
|
+
featureToggle: {
|
|
190
|
+
'feature-name': true,
|
|
191
|
+
},
|
|
192
|
+
},
|
|
193
|
+
};
|
|
194
|
+
assert.isFalse(mercury.connected, 'Mercury is not connected');
|
|
195
|
+
assert.isTrue(mercury.connecting, 'Mercury is connecting');
|
|
196
|
+
mockWebSocket.open();
|
|
197
|
+
|
|
198
|
+
return promise.then(() => {
|
|
199
|
+
assert.isTrue(mercury.connected, 'Mercury is connected');
|
|
200
|
+
assert.isFalse(mercury.connecting, 'Mercury is not connecting');
|
|
201
|
+
assert.calledWith(socketOpenStub, sinon.match(/ws:\/\/example.com/), sinon.match.any);
|
|
202
|
+
mercury._emit('event:featureToggle_update', envelope);
|
|
203
|
+
assert.calledOnceWithExactly(
|
|
204
|
+
webex.internal.feature.updateFeature,
|
|
205
|
+
envelope.data.featureToggle
|
|
206
|
+
);
|
|
207
|
+
sinon.restore();
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('connects to Mercury but does not call updateFeature', () => {
|
|
212
|
+
webex.internal.feature.updateFeature = sinon.stub();
|
|
213
|
+
const promise = mercury.connect();
|
|
214
|
+
const envelope = {};
|
|
215
|
+
|
|
216
|
+
return promise.then(() => {
|
|
217
|
+
mercury._emit('event:featureToggle_update', envelope);
|
|
218
|
+
assert.notCalled(webex.internal.feature.updateFeature);
|
|
219
|
+
sinon.restore();
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
it('Mercury emit event:ActiveClusterStatusEvent, call services switchActiveClusterIds', () => {
|
|
223
|
+
const promise = mercury.connect();
|
|
224
|
+
const activeClusterEventEnvelope = {
|
|
225
|
+
data: {
|
|
226
|
+
activeClusters: {
|
|
227
|
+
wdm: 'wdm-cluster-id.com',
|
|
228
|
+
},
|
|
229
|
+
},
|
|
230
|
+
};
|
|
231
|
+
mockWebSocket.open();
|
|
232
|
+
|
|
233
|
+
return promise.then(() => {
|
|
234
|
+
mercury._emit('event:ActiveClusterStatusEvent', activeClusterEventEnvelope);
|
|
235
|
+
assert.calledOnceWithExactly(
|
|
236
|
+
webex.internal.services.switchActiveClusterIds,
|
|
237
|
+
activeClusterEventEnvelope.data.activeClusters
|
|
238
|
+
);
|
|
239
|
+
sinon.restore();
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
it('Mercury emit event:ActiveClusterStatusEvent with no data, not call services switchActiveClusterIds', () => {
|
|
243
|
+
webex.internal.feature.updateFeature = sinon.stub();
|
|
244
|
+
const promise = mercury.connect();
|
|
245
|
+
const envelope = {};
|
|
246
|
+
|
|
247
|
+
return promise.then(() => {
|
|
248
|
+
mercury._emit('event:ActiveClusterStatusEvent', envelope);
|
|
249
|
+
assert.notCalled(webex.internal.services.switchActiveClusterIds);
|
|
250
|
+
sinon.restore();
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
it('Mercury emit event:u2c.cache-invalidation, call services invalidateCache', () => {
|
|
254
|
+
const promise = mercury.connect();
|
|
255
|
+
const u2cInvalidateEventEnvelope = {
|
|
256
|
+
data: {
|
|
257
|
+
timestamp: '1759289614',
|
|
258
|
+
},
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
mockWebSocket.open();
|
|
262
|
+
|
|
263
|
+
return promise.then(() => {
|
|
264
|
+
mercury._emit('event:u2c.cache-invalidation', u2cInvalidateEventEnvelope);
|
|
265
|
+
assert.calledOnceWithExactly(
|
|
266
|
+
webex.internal.services.invalidateCache,
|
|
267
|
+
u2cInvalidateEventEnvelope.data.timestamp
|
|
268
|
+
);
|
|
269
|
+
sinon.restore();
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
it('Mercury emit event:u2c.cache-invalidation with no data, not call services switchActiveClusterIds', () => {
|
|
273
|
+
webex.internal.feature.updateFeature = sinon.stub();
|
|
274
|
+
const promise = mercury.connect();
|
|
275
|
+
const envelope = {};
|
|
276
|
+
|
|
277
|
+
return promise.then(() => {
|
|
278
|
+
mercury._emit('event:u2c.cache-invalidation', envelope);
|
|
279
|
+
assert.notCalled(webex.internal.services.invalidateCache);
|
|
280
|
+
sinon.restore();
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
describe('when `maxRetries` is set', () => {
|
|
285
|
+
const check = () => {
|
|
286
|
+
socketOpenStub.restore();
|
|
287
|
+
socketOpenStub = sinon.stub(Socket.prototype, 'open');
|
|
288
|
+
socketOpenStub.returns(Promise.reject(new ConnectionError()));
|
|
289
|
+
assert.notCalled(Socket.prototype.open);
|
|
290
|
+
|
|
291
|
+
const promise = mercury.connect();
|
|
292
|
+
|
|
293
|
+
return promiseTick(5)
|
|
294
|
+
.then(() => {
|
|
295
|
+
assert.calledOnce(Socket.prototype.open);
|
|
296
|
+
|
|
297
|
+
return promiseTick(5);
|
|
298
|
+
})
|
|
299
|
+
.then(() => {
|
|
300
|
+
clock.tick(mercury.config.backoffTimeReset);
|
|
301
|
+
|
|
302
|
+
return promiseTick(5);
|
|
303
|
+
})
|
|
304
|
+
.then(() => {
|
|
305
|
+
assert.calledTwice(Socket.prototype.open);
|
|
306
|
+
clock.tick(2 * mercury.config.backoffTimeReset);
|
|
307
|
+
|
|
308
|
+
return promiseTick(5);
|
|
309
|
+
})
|
|
310
|
+
.then(() => {
|
|
311
|
+
assert.calledThrice(Socket.prototype.open);
|
|
312
|
+
clock.tick(5 * mercury.config.backoffTimeReset);
|
|
313
|
+
return assert.isRejected(promise);
|
|
314
|
+
})
|
|
315
|
+
.then(() => {
|
|
316
|
+
assert.calledThrice(Socket.prototype.open);
|
|
317
|
+
});
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
// skipping due to apparent bug with lolex in all browsers but Chrome.
|
|
321
|
+
// if initial retries is zero and mercury has never connected max retries is used
|
|
322
|
+
skipInBrowser(it)('fails after `maxRetries` attempts', () => {
|
|
323
|
+
mercury.config.maxRetries = 2;
|
|
324
|
+
mercury.config.initialConnectionMaxRetries = 0;
|
|
325
|
+
|
|
326
|
+
return check();
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
// initial retries is non-zero so takes precedence over maxRetries when mercury has never connected
|
|
330
|
+
skipInBrowser(it)('fails after `initialConnectionMaxRetries` attempts', () => {
|
|
331
|
+
mercury.config.maxRetries = 0;
|
|
332
|
+
mercury.config.initialConnectionMaxRetries = 2;
|
|
333
|
+
return check();
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
// initial retries is non-zero so takes precedence over maxRetries when mercury has never connected
|
|
337
|
+
skipInBrowser(it)('fails after `initialConnectionMaxRetries` attempts', () => {
|
|
338
|
+
mercury.config.initialConnectionMaxRetries = 2;
|
|
339
|
+
mercury.config.maxRetries = 5;
|
|
340
|
+
return check();
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
// when mercury has connected maxRetries is used and the initialConnectionMaxRetries is ignored
|
|
344
|
+
skipInBrowser(it)('fails after `initialConnectionMaxRetries` attempts', () => {
|
|
345
|
+
mercury.config.initialConnectionMaxRetries = 5;
|
|
346
|
+
mercury.config.maxRetries = 2;
|
|
347
|
+
mercury.hasEverConnected = true;
|
|
348
|
+
return check();
|
|
349
|
+
});
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
it('can safely be called multiple times', () => {
|
|
353
|
+
const promise = Promise.all([
|
|
354
|
+
mercury.connect(),
|
|
355
|
+
mercury.connect(),
|
|
356
|
+
mercury.connect(),
|
|
357
|
+
mercury.connect(),
|
|
358
|
+
]);
|
|
359
|
+
|
|
360
|
+
mockWebSocket.open();
|
|
361
|
+
|
|
362
|
+
return promise.then(() => {
|
|
363
|
+
assert.calledOnce(Socket.prototype.open);
|
|
364
|
+
});
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
// skipping due to apparent bug with lolex in all browsers but Chrome.
|
|
368
|
+
skipInBrowser(describe)('when the connection fails', () => {
|
|
369
|
+
it('backs off exponentially', () => {
|
|
370
|
+
socketOpenStub.restore();
|
|
371
|
+
socketOpenStub = sinon.stub(Socket.prototype, 'open');
|
|
372
|
+
socketOpenStub.returns(Promise.reject(new ConnectionError({code: 4001})));
|
|
373
|
+
// Note: onCall is zero-based
|
|
374
|
+
socketOpenStub.onCall(2).returns(Promise.resolve(new MockWebSocket()));
|
|
375
|
+
assert.notCalled(Socket.prototype.open);
|
|
376
|
+
|
|
377
|
+
const promise = mercury.connect();
|
|
378
|
+
|
|
379
|
+
return promiseTick(5)
|
|
380
|
+
.then(() => {
|
|
381
|
+
assert.calledOnce(Socket.prototype.open);
|
|
382
|
+
|
|
383
|
+
// I'm not sure why, but it's important the clock doesn't advance
|
|
384
|
+
// until a tick happens
|
|
385
|
+
return promiseTick(5);
|
|
386
|
+
})
|
|
387
|
+
.then(() => {
|
|
388
|
+
clock.tick(mercury.config.backoffTimeReset);
|
|
389
|
+
|
|
390
|
+
return promiseTick(5);
|
|
391
|
+
})
|
|
392
|
+
.then(() => {
|
|
393
|
+
assert.calledTwice(Socket.prototype.open);
|
|
394
|
+
clock.tick(2 * mercury.config.backoffTimeReset);
|
|
395
|
+
|
|
396
|
+
return promiseTick(5);
|
|
397
|
+
})
|
|
398
|
+
.then(() => {
|
|
399
|
+
assert.calledThrice(Socket.prototype.open);
|
|
400
|
+
clock.tick(5 * mercury.config.backoffTimeReset);
|
|
401
|
+
|
|
402
|
+
return promise;
|
|
403
|
+
})
|
|
404
|
+
.then(() => {
|
|
405
|
+
assert.calledThrice(Socket.prototype.open);
|
|
406
|
+
clock.tick(8 * mercury.config.backoffTimeReset);
|
|
407
|
+
|
|
408
|
+
return promiseTick(5);
|
|
409
|
+
})
|
|
410
|
+
.then(() => {
|
|
411
|
+
assert.calledThrice(Socket.prototype.open);
|
|
412
|
+
});
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
describe('with `BadRequest`', () => {
|
|
416
|
+
it('fails permanently', () => {
|
|
417
|
+
clock.uninstall();
|
|
418
|
+
socketOpenStub.restore();
|
|
419
|
+
socketOpenStub = sinon
|
|
420
|
+
.stub(Socket.prototype, 'open')
|
|
421
|
+
.returns(Promise.reject(new BadRequest({code: 4400})));
|
|
422
|
+
|
|
423
|
+
return assert.isRejected(mercury.connect());
|
|
424
|
+
});
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
describe('with `UnknownResponse`', () => {
|
|
428
|
+
it('triggers a device refresh', () => {
|
|
429
|
+
socketOpenStub.restore();
|
|
430
|
+
socketOpenStub = sinon.stub(Socket.prototype, 'open').returns(Promise.resolve());
|
|
431
|
+
socketOpenStub.onCall(0).returns(Promise.reject(new UnknownResponse({code: 4444})));
|
|
432
|
+
assert.notCalled(webex.credentials.refresh);
|
|
433
|
+
assert.notCalled(webex.internal.device.refresh);
|
|
434
|
+
const promise = mercury.connect();
|
|
435
|
+
|
|
436
|
+
return promiseTick(7).then(() => {
|
|
437
|
+
assert.notCalled(webex.credentials.refresh);
|
|
438
|
+
assert.called(webex.internal.device.refresh);
|
|
439
|
+
clock.tick(1000);
|
|
440
|
+
|
|
441
|
+
return promise;
|
|
442
|
+
});
|
|
443
|
+
});
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
describe('with `NotAuthorized`', () => {
|
|
447
|
+
it('triggers a token refresh', () => {
|
|
448
|
+
socketOpenStub.restore();
|
|
449
|
+
socketOpenStub = sinon.stub(Socket.prototype, 'open').returns(Promise.resolve());
|
|
450
|
+
socketOpenStub.onCall(0).returns(Promise.reject(new NotAuthorized({code: 4401})));
|
|
451
|
+
assert.notCalled(webex.credentials.refresh);
|
|
452
|
+
assert.notCalled(webex.internal.device.refresh);
|
|
453
|
+
const promise = mercury.connect();
|
|
454
|
+
|
|
455
|
+
return promiseTick(7).then(() => {
|
|
456
|
+
assert.called(webex.credentials.refresh);
|
|
457
|
+
assert.notCalled(webex.internal.device.refresh);
|
|
458
|
+
clock.tick(1000);
|
|
459
|
+
|
|
460
|
+
return promise;
|
|
461
|
+
});
|
|
462
|
+
});
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
describe('with `Forbidden`', () => {
|
|
466
|
+
it('fails permanently', () => {
|
|
467
|
+
clock.uninstall();
|
|
468
|
+
socketOpenStub.restore();
|
|
469
|
+
socketOpenStub = sinon
|
|
470
|
+
.stub(Socket.prototype, 'open')
|
|
471
|
+
.returns(Promise.reject(new Forbidden({code: 4403})));
|
|
472
|
+
|
|
473
|
+
return assert.isRejected(mercury.connect());
|
|
474
|
+
});
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
// describe(`with \`NotFound\``, () => {
|
|
478
|
+
// it(`triggers a device refresh`, () => {
|
|
479
|
+
// socketOpenStub.restore();
|
|
480
|
+
// socketOpenStub = sinon.stub(Socket.prototype, `open`).returns(Promise.resolve());
|
|
481
|
+
// socketOpenStub.onCall(0).returns(Promise.reject(new NotFound({code: 4404})));
|
|
482
|
+
// assert.notCalled(webex.credentials.refresh);
|
|
483
|
+
// assert.notCalled(webex.internal.device.refresh);
|
|
484
|
+
// const promise = mercury.connect();
|
|
485
|
+
// return promiseTick(6)
|
|
486
|
+
// .then(() => {
|
|
487
|
+
// assert.notCalled(webex.credentials.refresh);
|
|
488
|
+
// assert.called(webex.internal.device.refresh);
|
|
489
|
+
// clock.tick(1000);
|
|
490
|
+
// return assert.isFulfilled(promise);
|
|
491
|
+
// });
|
|
492
|
+
// });
|
|
493
|
+
// });
|
|
494
|
+
|
|
495
|
+
describe('when web-high-availability feature is enabled', () => {
|
|
496
|
+
it('marks current socket url as failed and get new one on Connection Error', () => {
|
|
497
|
+
webex.internal.feature.getFeature.returns(Promise.resolve(true));
|
|
498
|
+
socketOpenStub.restore();
|
|
499
|
+
socketOpenStub = sinon.stub(Socket.prototype, 'open').returns(Promise.resolve());
|
|
500
|
+
socketOpenStub.onCall(0).returns(Promise.reject(new ConnectionError({code: 4001})));
|
|
501
|
+
const promise = mercury.connect();
|
|
502
|
+
|
|
503
|
+
return promiseTick(7).then(() => {
|
|
504
|
+
assert.calledOnce(webex.internal.services.markFailedUrl);
|
|
505
|
+
clock.tick(1000);
|
|
506
|
+
|
|
507
|
+
return promise;
|
|
508
|
+
});
|
|
509
|
+
});
|
|
510
|
+
});
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
describe('when connected', () => {
|
|
514
|
+
it('resolves immediately', () =>
|
|
515
|
+
mercury.connect().then(() => {
|
|
516
|
+
assert.isTrue(mercury.connected, 'Mercury is connected');
|
|
517
|
+
assert.isFalse(mercury.connecting, 'Mercury is not connecting');
|
|
518
|
+
const promise = mercury.connect();
|
|
519
|
+
|
|
520
|
+
assert.isTrue(mercury.connected, 'Mercury is connected');
|
|
521
|
+
assert.isFalse(mercury.connecting, 'Mercury is not connecting');
|
|
522
|
+
|
|
523
|
+
return promise;
|
|
524
|
+
}));
|
|
525
|
+
|
|
526
|
+
// skipping due to apparent bug with lolex in all browsers but Chrome.
|
|
527
|
+
skipInBrowser(it)('does not continue attempting to connect', () => {
|
|
528
|
+
const promise = mercury.connect();
|
|
529
|
+
|
|
530
|
+
// Wait for the connection to be established before proceeding
|
|
531
|
+
mockWebSocket.open();
|
|
532
|
+
|
|
533
|
+
return promise.then(() =>
|
|
534
|
+
promiseTick(2)
|
|
535
|
+
.then(() => {
|
|
536
|
+
clock.tick(6 * webex.internal.mercury.config.backoffTimeReset);
|
|
537
|
+
|
|
538
|
+
return promiseTick(2);
|
|
539
|
+
})
|
|
540
|
+
.then(() => {
|
|
541
|
+
assert.calledOnce(Socket.prototype.open);
|
|
542
|
+
})
|
|
543
|
+
);
|
|
544
|
+
});
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
describe('when webSocketUrl is provided', () => {
|
|
548
|
+
it('connects to Mercury with provided url', () => {
|
|
549
|
+
const webSocketUrl = 'ws://providedurl.com';
|
|
550
|
+
const promise = mercury.connect(webSocketUrl);
|
|
551
|
+
|
|
552
|
+
assert.isFalse(mercury.connected, 'Mercury is not connected');
|
|
553
|
+
assert.isTrue(mercury.connecting, 'Mercury is connecting');
|
|
554
|
+
mockWebSocket.open();
|
|
555
|
+
|
|
556
|
+
return promise.then(() => {
|
|
557
|
+
assert.isTrue(mercury.connected, 'Mercury is connected');
|
|
558
|
+
assert.isFalse(mercury.connecting, 'Mercury is not connecting');
|
|
559
|
+
assert.calledWith(
|
|
560
|
+
Socket.prototype.open,
|
|
561
|
+
sinon.match(/ws:\/\/providedurl.com.*clientTimestamp[=]\d+/),
|
|
562
|
+
sinon.match.any
|
|
563
|
+
);
|
|
564
|
+
});
|
|
565
|
+
});
|
|
566
|
+
});
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
describe('Websocket proxy agent', () => {
|
|
570
|
+
afterEach(() => {
|
|
571
|
+
delete webex.config.defaultMercuryOptions;
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
it('connects to Mercury using proxy agent', () => {
|
|
575
|
+
const testProxyUrl = 'http://proxyurl.com:80';
|
|
576
|
+
|
|
577
|
+
webex.config.defaultMercuryOptions = {agent: {proxy: {href: testProxyUrl}}};
|
|
578
|
+
const promise = mercury.connect();
|
|
579
|
+
|
|
580
|
+
assert.isFalse(mercury.connected, 'Mercury is not connected');
|
|
581
|
+
assert.isTrue(mercury.connecting, 'Mercury is connecting');
|
|
582
|
+
mockWebSocket.open();
|
|
583
|
+
|
|
584
|
+
return promise.then(() => {
|
|
585
|
+
assert.isTrue(mercury.connected, 'Mercury is connected');
|
|
586
|
+
assert.isFalse(mercury.connecting, 'Mercury is not connecting');
|
|
587
|
+
assert.calledWith(
|
|
588
|
+
socketOpenStub,
|
|
589
|
+
sinon.match(/ws:\/\/example.com/),
|
|
590
|
+
sinon.match.has(
|
|
591
|
+
'agent',
|
|
592
|
+
sinon.match.has('proxy', sinon.match.has('href', testProxyUrl))
|
|
593
|
+
)
|
|
594
|
+
);
|
|
595
|
+
});
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
it('connects to Mercury without proxy agent', () => {
|
|
599
|
+
const promise = mercury.connect();
|
|
600
|
+
|
|
601
|
+
assert.isFalse(mercury.connected, 'Mercury is not connected');
|
|
602
|
+
assert.isTrue(mercury.connecting, 'Mercury is connecting');
|
|
603
|
+
mockWebSocket.open();
|
|
604
|
+
|
|
605
|
+
return promise.then(() => {
|
|
606
|
+
assert.isTrue(mercury.connected, 'Mercury is connected');
|
|
607
|
+
assert.isFalse(mercury.connecting, 'Mercury is not connecting');
|
|
608
|
+
assert.calledWith(
|
|
609
|
+
socketOpenStub,
|
|
610
|
+
sinon.match(/ws:\/\/example.com/),
|
|
611
|
+
sinon.match({agent: undefined})
|
|
612
|
+
);
|
|
613
|
+
});
|
|
614
|
+
});
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
describe('#logout()', () => {
|
|
618
|
+
it('calls disconnectAll and logs', () => {
|
|
619
|
+
sinon.stub(mercury.logger, 'info');
|
|
620
|
+
sinon.stub(mercury, 'disconnectAll');
|
|
621
|
+
mercury.logout();
|
|
622
|
+
assert.called(mercury.disconnectAll);
|
|
623
|
+
assert.calledTwice(mercury.logger.info);
|
|
624
|
+
|
|
625
|
+
assert.calledWith(mercury.logger.info.getCall(0), 'Mercury: logout() called');
|
|
626
|
+
assert.isTrue(
|
|
627
|
+
mercury.logger.info
|
|
628
|
+
.getCall(1)
|
|
629
|
+
.args[0].startsWith('Mercury: debug_mercury_logging stack: ')
|
|
630
|
+
);
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
it('uses the config.beforeLogoutOptionsCloseReason to disconnect and will send code 3050 for logout', () => {
|
|
634
|
+
sinon.stub(mercury, 'disconnectAll');
|
|
635
|
+
mercury.config.beforeLogoutOptionsCloseReason = 'done (permanent)';
|
|
636
|
+
mercury.logout();
|
|
637
|
+
assert.calledWith(mercury.disconnectAll, {code: 3050, reason: 'done (permanent)'});
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
it('uses the config.beforeLogoutOptionsCloseReason to disconnect and will send code 3050 for logout if the reason is different than standard', () => {
|
|
641
|
+
sinon.stub(mercury, 'disconnectAll');
|
|
642
|
+
mercury.config.beforeLogoutOptionsCloseReason = 'test';
|
|
643
|
+
mercury.logout();
|
|
644
|
+
assert.calledWith(mercury.disconnectAll, {code: 3050, reason: 'test'});
|
|
645
|
+
});
|
|
646
|
+
|
|
647
|
+
it('uses the config.beforeLogoutOptionsCloseReason to disconnect and will send undefined for logout if the reason is same as standard', () => {
|
|
648
|
+
sinon.stub(mercury, 'disconnectAll');
|
|
649
|
+
mercury.config.beforeLogoutOptionsCloseReason = 'done (forced)';
|
|
650
|
+
mercury.logout();
|
|
651
|
+
assert.calledWith(mercury.disconnectAll, undefined);
|
|
652
|
+
});
|
|
653
|
+
});
|
|
654
|
+
|
|
655
|
+
describe('#disconnect()', () => {
|
|
656
|
+
it('disconnects the WebSocket', () =>
|
|
657
|
+
mercury
|
|
658
|
+
.connect()
|
|
659
|
+
.then(() => {
|
|
660
|
+
assert.isTrue(mercury.connected, 'Mercury is connected');
|
|
661
|
+
assert.isFalse(mercury.connecting, 'Mercury is not connecting');
|
|
662
|
+
const promise = mercury.disconnect();
|
|
663
|
+
|
|
664
|
+
mockWebSocket.emit('close', {
|
|
665
|
+
code: 1000,
|
|
666
|
+
reason: 'Done',
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
return promise;
|
|
670
|
+
})
|
|
671
|
+
.then(() => {
|
|
672
|
+
assert.isFalse(mercury.connected, 'Mercury is not connected');
|
|
673
|
+
assert.isFalse(mercury.connecting, 'Mercury is not connecting');
|
|
674
|
+
assert.isUndefined(mercury.mockWebSocket, 'Mercury does not have a mockWebSocket');
|
|
675
|
+
}));
|
|
676
|
+
|
|
677
|
+
it('disconnects the WebSocket with code 3050', () =>
|
|
678
|
+
mercury
|
|
679
|
+
.connect()
|
|
680
|
+
.then(() => {
|
|
681
|
+
assert.isTrue(mercury.connected, 'Mercury is connected');
|
|
682
|
+
assert.isFalse(mercury.connecting, 'Mercury is not connecting');
|
|
683
|
+
const promise = mercury.disconnect();
|
|
684
|
+
|
|
685
|
+
mockWebSocket.emit('close', {
|
|
686
|
+
code: 3050,
|
|
687
|
+
reason: 'done (permanent)',
|
|
688
|
+
});
|
|
689
|
+
|
|
690
|
+
return promise;
|
|
691
|
+
})
|
|
692
|
+
.then(() => {
|
|
693
|
+
assert.isFalse(mercury.connected, 'Mercury is not connected');
|
|
694
|
+
assert.isFalse(mercury.connecting, 'Mercury is not connecting');
|
|
695
|
+
assert.isUndefined(mercury.mockWebSocket, 'Mercury does not have a mockWebSocket');
|
|
696
|
+
}));
|
|
697
|
+
|
|
698
|
+
it('stops emitting message events', () => {
|
|
699
|
+
const spy = sinon.spy();
|
|
700
|
+
|
|
701
|
+
mercury.on('event:status.start_typing', spy);
|
|
702
|
+
|
|
703
|
+
return mercury
|
|
704
|
+
.connect()
|
|
705
|
+
.then(() => {
|
|
706
|
+
assert.isTrue(mercury.connected, 'Mercury is connected');
|
|
707
|
+
assert.isFalse(mercury.connecting, 'Mercury is not connecting');
|
|
708
|
+
|
|
709
|
+
assert.notCalled(spy);
|
|
710
|
+
mockWebSocket.readyState = 1;
|
|
711
|
+
mockWebSocket.emit('open');
|
|
712
|
+
mockWebSocket.emit('message', {data: statusStartTypingMessage});
|
|
713
|
+
})
|
|
714
|
+
.then(() => {
|
|
715
|
+
assert.calledOnce(spy);
|
|
716
|
+
|
|
717
|
+
const promise = mercury.disconnect();
|
|
718
|
+
|
|
719
|
+
mockWebSocket.readyState = 1;
|
|
720
|
+
mockWebSocket.emit('open');
|
|
721
|
+
mockWebSocket.emit('message', {data: statusStartTypingMessage});
|
|
722
|
+
mockWebSocket.emit('close', {
|
|
723
|
+
code: 1000,
|
|
724
|
+
reason: 'Done',
|
|
725
|
+
});
|
|
726
|
+
mockWebSocket.emit('message', {data: statusStartTypingMessage});
|
|
727
|
+
|
|
728
|
+
return promise;
|
|
729
|
+
})
|
|
730
|
+
|
|
731
|
+
.then(() => {
|
|
732
|
+
mockWebSocket.readyState = 1;
|
|
733
|
+
mockWebSocket.emit('open');
|
|
734
|
+
mockWebSocket.emit('message', {data: statusStartTypingMessage});
|
|
735
|
+
assert.calledOnce(spy);
|
|
736
|
+
});
|
|
737
|
+
});
|
|
738
|
+
|
|
739
|
+
describe('when there is a connection attempt inflight', () => {
|
|
740
|
+
it('stops the attempt when disconnect called', () => {
|
|
741
|
+
socketOpenStub.restore();
|
|
742
|
+
socketOpenStub = sinon.stub(Socket.prototype, 'open');
|
|
743
|
+
socketOpenStub.onCall(0).returns(
|
|
744
|
+
// Delay the opening of the socket so that disconnect is called while open
|
|
745
|
+
// is in progress
|
|
746
|
+
promiseTick(2 * webex.internal.mercury.config.backoffTimeReset)
|
|
747
|
+
// Pretend the socket opened successfully. Failing should be fine too but
|
|
748
|
+
// it generates more console output.
|
|
749
|
+
.then(() => Promise.resolve())
|
|
750
|
+
);
|
|
751
|
+
const promise = mercury.connect();
|
|
752
|
+
|
|
753
|
+
// Wait for the connect call to setup
|
|
754
|
+
return promiseTick(webex.internal.mercury.config.backoffTimeReset).then(() => {
|
|
755
|
+
// By this time backoffCall and mercury socket should be defined by the
|
|
756
|
+
// 'connect' call
|
|
757
|
+
assert.isDefined(mercury.backoffCalls.get('mercury-default-session'), 'Mercury backoffCall is not defined');
|
|
758
|
+
assert.isDefined(mercury.socket, 'Mercury socket is not defined');
|
|
759
|
+
// Calling disconnect will abort the backoffCall, close the socket, and
|
|
760
|
+
// reject the connect
|
|
761
|
+
mercury.disconnect();
|
|
762
|
+
assert.isUndefined(mercury.backoffCalls.get('mercury-default-session'), 'Mercury backoffCall is still defined');
|
|
763
|
+
// The socket will never be unset (which seems bad)
|
|
764
|
+
assert.isDefined(mercury.socket, 'Mercury socket is not defined');
|
|
765
|
+
|
|
766
|
+
return assert.isRejected(promise).then((error) => {
|
|
767
|
+
// connection did not fail, so no last error
|
|
768
|
+
assert.isUndefined(mercury.getLastError());
|
|
769
|
+
});
|
|
770
|
+
});
|
|
771
|
+
});
|
|
772
|
+
|
|
773
|
+
it('stops the attempt when backoffCall is undefined', () => {
|
|
774
|
+
socketOpenStub.restore();
|
|
775
|
+
socketOpenStub = sinon.stub(Socket.prototype, 'open');
|
|
776
|
+
socketOpenStub.returns(Promise.resolve());
|
|
777
|
+
|
|
778
|
+
let reason;
|
|
779
|
+
|
|
780
|
+
mercury.backoffCalls.clear();
|
|
781
|
+
|
|
782
|
+
const promise = mercury._attemptConnection(
|
|
783
|
+
'ws://example.com',
|
|
784
|
+
'mercury-default-session',
|
|
785
|
+
(_reason) => {
|
|
786
|
+
reason = _reason;
|
|
787
|
+
}
|
|
788
|
+
);
|
|
789
|
+
|
|
790
|
+
return promiseTick(webex.internal.mercury.config.backoffTimeReset).then(() => {
|
|
791
|
+
assert.equal(
|
|
792
|
+
reason.message,
|
|
793
|
+
`Mercury: prevent socket open when backoffCall no longer defined for ${mercury.defaultSessionId}`
|
|
794
|
+
);
|
|
795
|
+
|
|
796
|
+
// Ensure the promise was actually rejected (short-circuited)
|
|
797
|
+
return assert.isRejected(promise);
|
|
798
|
+
});
|
|
799
|
+
});
|
|
800
|
+
|
|
801
|
+
it('sets lastError when retrying', () => {
|
|
802
|
+
const realError = new Error('FORCED');
|
|
803
|
+
|
|
804
|
+
socketOpenStub.restore();
|
|
805
|
+
socketOpenStub = sinon.stub(Socket.prototype, 'open');
|
|
806
|
+
socketOpenStub.onCall(0).returns(Promise.reject(realError));
|
|
807
|
+
const promise = mercury.connect();
|
|
808
|
+
|
|
809
|
+
// Wait for the connect call to setup
|
|
810
|
+
return promiseTick(webex.internal.mercury.config.backoffTimeReset).then(() => {
|
|
811
|
+
// Calling disconnect will abort the backoffCall, close the socket, and
|
|
812
|
+
// reject the connect
|
|
813
|
+
mercury.disconnect();
|
|
814
|
+
|
|
815
|
+
return assert.isRejected(promise).then((error) => {
|
|
816
|
+
const lastError = mercury.getLastError();
|
|
817
|
+
|
|
818
|
+
assert.equal(error.message, `Mercury Connection Aborted for ${mercury.defaultSessionId}`);
|
|
819
|
+
assert.isDefined(lastError);
|
|
820
|
+
assert.equal(lastError, realError);
|
|
821
|
+
});
|
|
822
|
+
});
|
|
823
|
+
});
|
|
824
|
+
});
|
|
825
|
+
});
|
|
826
|
+
|
|
827
|
+
describe('#_emit()', () => {
|
|
828
|
+
it('emits Error-safe events and log the error with the call parameters', () => {
|
|
829
|
+
const error = 'error';
|
|
830
|
+
const event = {data: 'some data'};
|
|
831
|
+
mercury.on('break', () => {
|
|
832
|
+
throw error;
|
|
833
|
+
});
|
|
834
|
+
sinon.stub(mercury.logger, 'error');
|
|
835
|
+
|
|
836
|
+
return Promise.resolve(mercury._emit('break', event)).then((res) => {
|
|
837
|
+
assert.calledWith(
|
|
838
|
+
mercury.logger.error,
|
|
839
|
+
'Mercury: error occurred in event handler:',
|
|
840
|
+
error,
|
|
841
|
+
' with args: ',
|
|
842
|
+
['break', event]
|
|
843
|
+
);
|
|
844
|
+
return res;
|
|
845
|
+
});
|
|
846
|
+
});
|
|
847
|
+
});
|
|
848
|
+
|
|
849
|
+
describe('#_applyOverrides()', () => {
|
|
850
|
+
const lastSeenActivityDate = 'Some date';
|
|
851
|
+
const lastReadableActivityDate = 'Some other date';
|
|
852
|
+
|
|
853
|
+
it('merges a single header field with data', () => {
|
|
854
|
+
const envelope = {
|
|
855
|
+
headers: {
|
|
856
|
+
'data.activity.target.lastSeenActivityDate': lastSeenActivityDate,
|
|
857
|
+
},
|
|
858
|
+
data: {
|
|
859
|
+
activity: {},
|
|
860
|
+
},
|
|
861
|
+
};
|
|
862
|
+
|
|
863
|
+
mercury._applyOverrides(envelope);
|
|
864
|
+
|
|
865
|
+
assert.equal(envelope.data.activity.target.lastSeenActivityDate, lastSeenActivityDate);
|
|
866
|
+
});
|
|
867
|
+
|
|
868
|
+
it('merges a multiple header fields with data', () => {
|
|
869
|
+
const envelope = {
|
|
870
|
+
headers: {
|
|
871
|
+
'data.activity.target.lastSeenActivityDate': lastSeenActivityDate,
|
|
872
|
+
'data.activity.target.lastReadableActivityDate': lastReadableActivityDate,
|
|
873
|
+
},
|
|
874
|
+
data: {
|
|
875
|
+
activity: {},
|
|
876
|
+
},
|
|
877
|
+
};
|
|
878
|
+
|
|
879
|
+
mercury._applyOverrides(envelope);
|
|
880
|
+
|
|
881
|
+
assert.equal(envelope.data.activity.target.lastSeenActivityDate, lastSeenActivityDate);
|
|
882
|
+
assert.equal(
|
|
883
|
+
envelope.data.activity.target.lastReadableActivityDate,
|
|
884
|
+
lastReadableActivityDate
|
|
885
|
+
);
|
|
886
|
+
});
|
|
887
|
+
|
|
888
|
+
it('merges headers when Mercury messages arrive', () => {
|
|
889
|
+
const envelope = {
|
|
890
|
+
headers: {
|
|
891
|
+
'data.activity.target.lastSeenActivityDate': lastSeenActivityDate,
|
|
892
|
+
},
|
|
893
|
+
data: {
|
|
894
|
+
activity: {},
|
|
895
|
+
},
|
|
896
|
+
};
|
|
897
|
+
|
|
898
|
+
mercury._applyOverrides(envelope);
|
|
899
|
+
|
|
900
|
+
assert.equal(envelope.data.activity.target.lastSeenActivityDate, lastSeenActivityDate);
|
|
901
|
+
});
|
|
902
|
+
});
|
|
903
|
+
|
|
904
|
+
describe('#_setTimeOffset', () => {
|
|
905
|
+
it('sets mercuryTimeOffset based on the difference between wsWriteTimestamp and now', () => {
|
|
906
|
+
const event = {
|
|
907
|
+
data: {
|
|
908
|
+
wsWriteTimestamp: Date.now() - 60000,
|
|
909
|
+
},
|
|
910
|
+
};
|
|
911
|
+
assert.isUndefined(mercury.mercuryTimeOffset);
|
|
912
|
+
mercury._setTimeOffset('mercury-default-session', event);
|
|
913
|
+
assert.isDefined(mercury.mercuryTimeOffset);
|
|
914
|
+
assert.isTrue(mercury.mercuryTimeOffset > 0);
|
|
915
|
+
});
|
|
916
|
+
it('handles negative offsets', () => {
|
|
917
|
+
const event = {
|
|
918
|
+
data: {
|
|
919
|
+
wsWriteTimestamp: Date.now() + 60000,
|
|
920
|
+
},
|
|
921
|
+
};
|
|
922
|
+
mercury._setTimeOffset('mercury-default-session', event);
|
|
923
|
+
assert.isTrue(mercury.mercuryTimeOffset < 0);
|
|
924
|
+
});
|
|
925
|
+
it('handles invalid wsWriteTimestamp', () => {
|
|
926
|
+
const invalidTimestamps = [null, -1, 'invalid', undefined];
|
|
927
|
+
invalidTimestamps.forEach((invalidTimestamp) => {
|
|
928
|
+
const event = {
|
|
929
|
+
data: {
|
|
930
|
+
wsWriteTimestamp: invalidTimestamp,
|
|
931
|
+
},
|
|
932
|
+
};
|
|
933
|
+
mercury._setTimeOffset('mercury-default-session', event);
|
|
934
|
+
assert.isUndefined(mercury.mercuryTimeOffset);
|
|
935
|
+
});
|
|
936
|
+
});
|
|
937
|
+
});
|
|
938
|
+
|
|
939
|
+
describe('#_prepareUrl()', () => {
|
|
940
|
+
beforeEach(() => {
|
|
941
|
+
webex.internal.device.webSocketUrl = 'ws://example.com';
|
|
942
|
+
});
|
|
943
|
+
|
|
944
|
+
it('uses device default webSocketUrl', () =>
|
|
945
|
+
webex.internal.mercury._prepareUrl().then((wsUrl) => assert.match(wsUrl, /example.com/)));
|
|
946
|
+
it('uses provided webSocketUrl', () =>
|
|
947
|
+
webex.internal.mercury
|
|
948
|
+
._prepareUrl('ws://provided.com')
|
|
949
|
+
.then((wsUrl) => assert.match(wsUrl, /.*provided.com.*/)));
|
|
950
|
+
it('requests text-mode WebSockets', () =>
|
|
951
|
+
webex.internal.mercury
|
|
952
|
+
._prepareUrl()
|
|
953
|
+
.then((wsUrl) => assert.match(wsUrl, /.*outboundWireFormat=text.*/)));
|
|
954
|
+
|
|
955
|
+
it('requests the buffer state message', () =>
|
|
956
|
+
webex.internal.mercury
|
|
957
|
+
._prepareUrl()
|
|
958
|
+
.then((wsUrl) => assert.match(wsUrl, /.*bufferStates=true.*/)));
|
|
959
|
+
|
|
960
|
+
it('does not add conditional properties', () =>
|
|
961
|
+
webex.internal.mercury._prepareUrl().then((wsUrl) => {
|
|
962
|
+
assert.notMatch(wsUrl, /mercuryRegistrationStatus/);
|
|
963
|
+
assert.notMatch(wsUrl, /mercuryRegistrationStatus/);
|
|
964
|
+
assert.notMatch(wsUrl, /isRegistrationRefreshEnabled/);
|
|
965
|
+
assert.notMatch(wsUrl, /multipleConnections/);
|
|
966
|
+
}));
|
|
967
|
+
|
|
968
|
+
describe('when web-high-availability is enabled', () => {
|
|
969
|
+
it('uses webSocketUrl provided by device', () => {
|
|
970
|
+
webex.internal.device.useServiceCatalogUrl = sinon
|
|
971
|
+
.stub()
|
|
972
|
+
.returns(Promise.resolve('ws://example-2.com'));
|
|
973
|
+
webex.internal.feature.getFeature.onCall(0).returns(Promise.resolve(true));
|
|
974
|
+
|
|
975
|
+
return webex.internal.mercury
|
|
976
|
+
._prepareUrl()
|
|
977
|
+
.then((wsUrl) => assert.match(wsUrl, /example-2.com/));
|
|
978
|
+
});
|
|
979
|
+
it('uses high priority url instead of provided webSocketUrl', () => {
|
|
980
|
+
webex.internal.feature.getFeature.onCall(0).returns(Promise.resolve(true));
|
|
981
|
+
webex.internal.services.convertUrlToPriorityHostUrl = sinon
|
|
982
|
+
.stub()
|
|
983
|
+
.returns(Promise.resolve('ws://example-2.com'));
|
|
984
|
+
return webex.internal.mercury
|
|
985
|
+
._prepareUrl('ws://provided.com')
|
|
986
|
+
.then((wsUrl) => assert.match(wsUrl, /example-2.com/));
|
|
987
|
+
});
|
|
988
|
+
});
|
|
989
|
+
|
|
990
|
+
describe("when 'web-shared-socket' is enabled", () => {
|
|
991
|
+
beforeEach(() => {
|
|
992
|
+
webex.internal.feature.getFeature.returns(Promise.resolve(true));
|
|
993
|
+
});
|
|
994
|
+
|
|
995
|
+
it('requests shared socket support', () =>
|
|
996
|
+
webex.internal.mercury
|
|
997
|
+
._prepareUrl()
|
|
998
|
+
.then((wsUrl) => assert.match(wsUrl, /isRegistrationRefreshEnabled=true/)));
|
|
999
|
+
|
|
1000
|
+
it('requests the registration banner', () =>
|
|
1001
|
+
webex.internal.mercury
|
|
1002
|
+
._prepareUrl()
|
|
1003
|
+
.then((wsUrl) => assert.match(wsUrl, /mercuryRegistrationStatus=true/)));
|
|
1004
|
+
|
|
1005
|
+
it('does not request the buffer state message', () =>
|
|
1006
|
+
webex.internal.mercury._prepareUrl().then((wsUrl) => {
|
|
1007
|
+
assert.match(wsUrl, /mercuryRegistrationStatus=true/);
|
|
1008
|
+
assert.notMatch(wsUrl, /bufferStates/);
|
|
1009
|
+
}));
|
|
1010
|
+
});
|
|
1011
|
+
|
|
1012
|
+
describe('when using an ephemeral device', () => {
|
|
1013
|
+
beforeEach(() => {
|
|
1014
|
+
webex.config.device.ephemeral = true;
|
|
1015
|
+
});
|
|
1016
|
+
|
|
1017
|
+
it('indicates multiple connections may be coming from this user', () =>
|
|
1018
|
+
webex.internal.mercury
|
|
1019
|
+
._prepareUrl()
|
|
1020
|
+
.then((wsUrl) => assert.match(wsUrl, /multipleConnections/)));
|
|
1021
|
+
});
|
|
1022
|
+
});
|
|
1023
|
+
|
|
1024
|
+
describe('ping pong latency event is forwarded', () => {
|
|
1025
|
+
it('should forward ping pong latency event', () => {
|
|
1026
|
+
const spy = sinon.spy();
|
|
1027
|
+
|
|
1028
|
+
mercury.on('ping-pong-latency', spy);
|
|
1029
|
+
|
|
1030
|
+
return mercury.connect().then(() => {
|
|
1031
|
+
assert.calledWith(spy, 0);
|
|
1032
|
+
assert.calledOnce(spy);
|
|
1033
|
+
});
|
|
1034
|
+
});
|
|
1035
|
+
});
|
|
1036
|
+
|
|
1037
|
+
describe('shutdown protocol', () => {
|
|
1038
|
+
describe('#_handleImminentShutdown()', () => {
|
|
1039
|
+
let connectWithBackoffStub;
|
|
1040
|
+
const sessionId = 'mercury-default-session';
|
|
1041
|
+
|
|
1042
|
+
beforeEach(() => {
|
|
1043
|
+
mercury.connected = true;
|
|
1044
|
+
mercury.sockets.set(sessionId, {
|
|
1045
|
+
url: 'ws://old-socket.com',
|
|
1046
|
+
removeAllListeners: sinon.stub(),
|
|
1047
|
+
});
|
|
1048
|
+
mercury.socket = mercury.sockets.get(sessionId);
|
|
1049
|
+
connectWithBackoffStub = sinon.stub(mercury, '_connectWithBackoff');
|
|
1050
|
+
connectWithBackoffStub.returns(Promise.resolve());
|
|
1051
|
+
sinon.stub(mercury, '_emit');
|
|
1052
|
+
});
|
|
1053
|
+
|
|
1054
|
+
afterEach(() => {
|
|
1055
|
+
connectWithBackoffStub.restore();
|
|
1056
|
+
mercury._emit.restore();
|
|
1057
|
+
mercury.sockets.clear();
|
|
1058
|
+
});
|
|
1059
|
+
|
|
1060
|
+
it('should be idempotent - no-op if already in progress', () => {
|
|
1061
|
+
// Simulate an existing switchover in progress by seeding the backoff map
|
|
1062
|
+
mercury._shutdownSwitchoverBackoffCalls.set(sessionId, {placeholder: true});
|
|
1063
|
+
|
|
1064
|
+
mercury._handleImminentShutdown(sessionId);
|
|
1065
|
+
|
|
1066
|
+
assert.notCalled(connectWithBackoffStub);
|
|
1067
|
+
});
|
|
1068
|
+
|
|
1069
|
+
it('should set switchover flags when called', () => {
|
|
1070
|
+
mercury._handleImminentShutdown(sessionId);
|
|
1071
|
+
|
|
1072
|
+
// With _connectWithBackoff stubbed, the backoff map entry may not be created here.
|
|
1073
|
+
// Assert that switchover initiation state was set and a shutdown switchover connect was requested.
|
|
1074
|
+
assert.isDefined(mercury._shutdownSwitchoverId);
|
|
1075
|
+
|
|
1076
|
+
assert.calledOnce(connectWithBackoffStub);
|
|
1077
|
+
const callArgs = connectWithBackoffStub.firstCall.args;
|
|
1078
|
+
assert.isUndefined(callArgs[0]); // webSocketUrl
|
|
1079
|
+
assert.equal(callArgs[1], sessionId); // sessionId
|
|
1080
|
+
assert.isObject(callArgs[2]); // context
|
|
1081
|
+
assert.isTrue(callArgs[2].isShutdownSwitchover);
|
|
1082
|
+
assert.isObject(callArgs[2].attemptOptions);
|
|
1083
|
+
assert.isTrue(callArgs[2].attemptOptions.isShutdownSwitchover);
|
|
1084
|
+
});
|
|
1085
|
+
|
|
1086
|
+
it('should call _connectWithBackoff with correct parameters', (done) => {
|
|
1087
|
+
mercury._handleImminentShutdown(sessionId);
|
|
1088
|
+
|
|
1089
|
+
process.nextTick(() => {
|
|
1090
|
+
assert.calledOnce(connectWithBackoffStub);
|
|
1091
|
+
const callArgs = connectWithBackoffStub.firstCall.args;
|
|
1092
|
+
assert.isUndefined(callArgs[0]); // webSocketUrl
|
|
1093
|
+
assert.equal(callArgs[1], sessionId); // sessionId
|
|
1094
|
+
assert.isObject(callArgs[2]); // context
|
|
1095
|
+
assert.isTrue(callArgs[2].isShutdownSwitchover);
|
|
1096
|
+
assert.isObject(callArgs[2].attemptOptions);
|
|
1097
|
+
assert.isTrue(callArgs[2].attemptOptions.isShutdownSwitchover);
|
|
1098
|
+
done();
|
|
1099
|
+
});
|
|
1100
|
+
});
|
|
1101
|
+
|
|
1102
|
+
it('should handle exceptions during switchover', () => {
|
|
1103
|
+
connectWithBackoffStub.restore();
|
|
1104
|
+
sinon.stub(mercury, '_connectWithBackoff').throws(new Error('Connection failed'));
|
|
1105
|
+
|
|
1106
|
+
mercury._handleImminentShutdown(sessionId);
|
|
1107
|
+
|
|
1108
|
+
// When an exception happens synchronously, the placeholder entry
|
|
1109
|
+
// should be removed from the map.
|
|
1110
|
+
const switchoverCall = mercury._shutdownSwitchoverBackoffCalls.get(sessionId);
|
|
1111
|
+
assert.isUndefined(switchoverCall);
|
|
1112
|
+
mercury._connectWithBackoff.restore();
|
|
1113
|
+
});
|
|
1114
|
+
});
|
|
1115
|
+
|
|
1116
|
+
|
|
1117
|
+
describe('#_onmessage() with shutdown message', () => {
|
|
1118
|
+
beforeEach(() => {
|
|
1119
|
+
sinon.stub(mercury, '_handleImminentShutdown');
|
|
1120
|
+
sinon.stub(mercury, '_emit');
|
|
1121
|
+
sinon.stub(mercury, '_setTimeOffset');
|
|
1122
|
+
});
|
|
1123
|
+
|
|
1124
|
+
afterEach(() => {
|
|
1125
|
+
mercury._handleImminentShutdown.restore();
|
|
1126
|
+
mercury._emit.restore();
|
|
1127
|
+
mercury._setTimeOffset.restore();
|
|
1128
|
+
});
|
|
1129
|
+
|
|
1130
|
+
it('should trigger _handleImminentShutdown on shutdown message', () => {
|
|
1131
|
+
const shutdownEvent = {
|
|
1132
|
+
data: {
|
|
1133
|
+
type: 'shutdown',
|
|
1134
|
+
},
|
|
1135
|
+
};
|
|
1136
|
+
|
|
1137
|
+
const result = mercury._onmessage(mercury.defaultSessionId, shutdownEvent);
|
|
1138
|
+
|
|
1139
|
+
assert.calledOnce(mercury._handleImminentShutdown);
|
|
1140
|
+
assert.calledWith(
|
|
1141
|
+
mercury._emit,
|
|
1142
|
+
mercury.defaultSessionId,
|
|
1143
|
+
'event:mercury_shutdown_imminent',
|
|
1144
|
+
shutdownEvent.data
|
|
1145
|
+
);
|
|
1146
|
+
assert.instanceOf(result, Promise);
|
|
1147
|
+
});
|
|
1148
|
+
|
|
1149
|
+
it('should handle shutdown message without additional data gracefully', () => {
|
|
1150
|
+
const shutdownEvent = {
|
|
1151
|
+
data: {
|
|
1152
|
+
type: 'shutdown',
|
|
1153
|
+
},
|
|
1154
|
+
};
|
|
1155
|
+
|
|
1156
|
+
mercury._onmessage(mercury.defaultSessionId, shutdownEvent);
|
|
1157
|
+
|
|
1158
|
+
assert.calledOnce(mercury._handleImminentShutdown);
|
|
1159
|
+
});
|
|
1160
|
+
|
|
1161
|
+
it('should not trigger shutdown handling for non-shutdown messages', () => {
|
|
1162
|
+
const regularEvent = {
|
|
1163
|
+
data: {
|
|
1164
|
+
type: 'regular',
|
|
1165
|
+
data: {
|
|
1166
|
+
eventType: 'conversation.activity',
|
|
1167
|
+
},
|
|
1168
|
+
},
|
|
1169
|
+
};
|
|
1170
|
+
|
|
1171
|
+
mercury._onmessage(mercury.defaultSessionId, regularEvent);
|
|
1172
|
+
|
|
1173
|
+
assert.notCalled(mercury._handleImminentShutdown);
|
|
1174
|
+
});
|
|
1175
|
+
});
|
|
1176
|
+
|
|
1177
|
+
describe('#_onmessage() with missing data or eventType', () => {
|
|
1178
|
+
beforeEach(() => {
|
|
1179
|
+
sinon.stub(mercury, '_emit');
|
|
1180
|
+
sinon.stub(mercury, '_setTimeOffset');
|
|
1181
|
+
sinon.stub(mercury, '_applyOverrides');
|
|
1182
|
+
});
|
|
1183
|
+
|
|
1184
|
+
afterEach(() => {
|
|
1185
|
+
mercury._emit.restore();
|
|
1186
|
+
mercury._setTimeOffset.restore();
|
|
1187
|
+
mercury._applyOverrides.restore();
|
|
1188
|
+
});
|
|
1189
|
+
|
|
1190
|
+
it('should not throw when envelope.data is undefined', () => {
|
|
1191
|
+
const event = {
|
|
1192
|
+
data: {
|
|
1193
|
+
type: 'someType',
|
|
1194
|
+
// no nested data property
|
|
1195
|
+
},
|
|
1196
|
+
};
|
|
1197
|
+
|
|
1198
|
+
const result = mercury._onmessage(mercury.defaultSessionId, event);
|
|
1199
|
+
|
|
1200
|
+
assert.instanceOf(result, Promise);
|
|
1201
|
+
assert.calledWith(mercury._emit, mercury.defaultSessionId, 'event', event.data);
|
|
1202
|
+
});
|
|
1203
|
+
|
|
1204
|
+
it('should not throw when data.eventType is undefined', () => {
|
|
1205
|
+
const event = {
|
|
1206
|
+
data: {
|
|
1207
|
+
type: 'someType',
|
|
1208
|
+
data: {
|
|
1209
|
+
// no eventType property
|
|
1210
|
+
someField: 'value',
|
|
1211
|
+
},
|
|
1212
|
+
},
|
|
1213
|
+
};
|
|
1214
|
+
|
|
1215
|
+
const result = mercury._onmessage(mercury.defaultSessionId, event);
|
|
1216
|
+
|
|
1217
|
+
assert.instanceOf(result, Promise);
|
|
1218
|
+
assert.calledWith(mercury._emit, mercury.defaultSessionId, 'event', event.data);
|
|
1219
|
+
});
|
|
1220
|
+
|
|
1221
|
+
it('should emit generic event for messages without eventType (e.g. subscription responses)', () => {
|
|
1222
|
+
const event = {
|
|
1223
|
+
data: {
|
|
1224
|
+
id: 'msg-123',
|
|
1225
|
+
sequenceNumber: 5,
|
|
1226
|
+
data: {
|
|
1227
|
+
statusCode: 200,
|
|
1228
|
+
},
|
|
1229
|
+
},
|
|
1230
|
+
};
|
|
1231
|
+
|
|
1232
|
+
const result = mercury._onmessage(mercury.defaultSessionId, event);
|
|
1233
|
+
|
|
1234
|
+
assert.instanceOf(result, Promise);
|
|
1235
|
+
assert.calledOnce(mercury._emit);
|
|
1236
|
+
assert.calledWith(mercury._emit, mercury.defaultSessionId, 'event', event.data);
|
|
1237
|
+
});
|
|
1238
|
+
|
|
1239
|
+
it('should still process messages with a valid eventType', async () => {
|
|
1240
|
+
const event = {
|
|
1241
|
+
data: {
|
|
1242
|
+
data: {
|
|
1243
|
+
eventType: 'conversation.activity',
|
|
1244
|
+
},
|
|
1245
|
+
},
|
|
1246
|
+
};
|
|
1247
|
+
|
|
1248
|
+
await mercury._onmessage(mercury.defaultSessionId, event);
|
|
1249
|
+
|
|
1250
|
+
// Normal flow emits namespace-specific events after processing handlers.
|
|
1251
|
+
// The early-return guard only emits 'event', so asserting these proves the normal path was taken.
|
|
1252
|
+
assert.calledWith(mercury._emit, mercury.defaultSessionId, 'event:conversation', event.data);
|
|
1253
|
+
assert.calledWith(mercury._emit, mercury.defaultSessionId, 'event:conversation.activity', event.data);
|
|
1254
|
+
});
|
|
1255
|
+
});
|
|
1256
|
+
|
|
1257
|
+
describe('#_getEventHandlers()', () => {
|
|
1258
|
+
it('should return an empty array when eventType is undefined', () => {
|
|
1259
|
+
const result = mercury._getEventHandlers(undefined);
|
|
1260
|
+
|
|
1261
|
+
assert.deepEqual(result, []);
|
|
1262
|
+
});
|
|
1263
|
+
|
|
1264
|
+
it('should return an empty array when eventType is null', () => {
|
|
1265
|
+
const result = mercury._getEventHandlers(null);
|
|
1266
|
+
|
|
1267
|
+
assert.deepEqual(result, []);
|
|
1268
|
+
});
|
|
1269
|
+
|
|
1270
|
+
it('should return an empty array when eventType is an empty string', () => {
|
|
1271
|
+
const result = mercury._getEventHandlers('');
|
|
1272
|
+
|
|
1273
|
+
assert.deepEqual(result, []);
|
|
1274
|
+
});
|
|
1275
|
+
|
|
1276
|
+
it('should return an empty array when namespace is not registered', () => {
|
|
1277
|
+
const result = mercury._getEventHandlers('unknownNamespace.someEvent');
|
|
1278
|
+
|
|
1279
|
+
assert.deepEqual(result, []);
|
|
1280
|
+
});
|
|
1281
|
+
});
|
|
1282
|
+
|
|
1283
|
+
describe('#_onclose() with code 4001 (shutdown replacement)', () => {
|
|
1284
|
+
let mockSocket, anotherSocket;
|
|
1285
|
+
|
|
1286
|
+
beforeEach(() => {
|
|
1287
|
+
mockSocket = {
|
|
1288
|
+
url: 'ws://active-socket.com',
|
|
1289
|
+
removeAllListeners: sinon.stub(),
|
|
1290
|
+
};
|
|
1291
|
+
anotherSocket = {
|
|
1292
|
+
url: 'ws://old-socket.com',
|
|
1293
|
+
removeAllListeners: sinon.stub(),
|
|
1294
|
+
};
|
|
1295
|
+
mercury.socket = mockSocket;
|
|
1296
|
+
mercury.sockets.set(mercury.defaultSessionId, mockSocket);
|
|
1297
|
+
mercury.connected = true;
|
|
1298
|
+
sinon.stub(mercury, '_emit');
|
|
1299
|
+
sinon.stub(mercury, '_reconnect');
|
|
1300
|
+
sinon.stub(mercury, 'unset');
|
|
1301
|
+
});
|
|
1302
|
+
|
|
1303
|
+
afterEach(() => {
|
|
1304
|
+
mercury._emit.restore();
|
|
1305
|
+
mercury._reconnect.restore();
|
|
1306
|
+
mercury.unset.restore();
|
|
1307
|
+
});
|
|
1308
|
+
|
|
1309
|
+
it('should handle active socket close with 4001 - permanent failure', () => {
|
|
1310
|
+
const closeEvent = {
|
|
1311
|
+
code: 4001,
|
|
1312
|
+
reason: 'replaced during shutdown',
|
|
1313
|
+
};
|
|
1314
|
+
|
|
1315
|
+
mercury._onclose(mercury.defaultSessionId, closeEvent, mockSocket);
|
|
1316
|
+
|
|
1317
|
+
assert.calledWith(mercury._emit, mercury.defaultSessionId, 'offline.permanent', closeEvent);
|
|
1318
|
+
assert.notCalled(mercury._reconnect); // No reconnect for 4001 on active socket
|
|
1319
|
+
assert.isFalse(mercury.connected);
|
|
1320
|
+
});
|
|
1321
|
+
|
|
1322
|
+
it('should handle non-active socket close with 4001 - no reconnect needed', () => {
|
|
1323
|
+
const closeEvent = {
|
|
1324
|
+
code: 4001,
|
|
1325
|
+
reason: 'replaced during shutdown',
|
|
1326
|
+
};
|
|
1327
|
+
|
|
1328
|
+
mercury._onclose(mercury.defaultSessionId, closeEvent, anotherSocket);
|
|
1329
|
+
|
|
1330
|
+
assert.calledWith(mercury._emit, mercury.defaultSessionId, 'offline.replaced', closeEvent);
|
|
1331
|
+
assert.notCalled(mercury._reconnect);
|
|
1332
|
+
assert.isTrue(mercury.connected); // Should remain connected
|
|
1333
|
+
assert.notCalled(mercury.unset);
|
|
1334
|
+
});
|
|
1335
|
+
|
|
1336
|
+
it('should distinguish between active and non-active socket closes', () => {
|
|
1337
|
+
const closeEvent = {
|
|
1338
|
+
code: 4001,
|
|
1339
|
+
reason: 'replaced during shutdown',
|
|
1340
|
+
};
|
|
1341
|
+
|
|
1342
|
+
// Test non-active socket
|
|
1343
|
+
mercury._onclose(mercury.defaultSessionId, closeEvent, anotherSocket);
|
|
1344
|
+
assert.calledWith(mercury._emit, mercury.defaultSessionId, 'offline.replaced', closeEvent);
|
|
1345
|
+
|
|
1346
|
+
// Reset the spy call history
|
|
1347
|
+
mercury._emit.resetHistory();
|
|
1348
|
+
|
|
1349
|
+
// Test active socket
|
|
1350
|
+
mercury.sockets.set(mercury.defaultSessionId, mockSocket);
|
|
1351
|
+
mercury._onclose(mercury.defaultSessionId, closeEvent, mockSocket);
|
|
1352
|
+
assert.calledWith(mercury._emit, mercury.defaultSessionId, 'offline.permanent', closeEvent);
|
|
1353
|
+
});
|
|
1354
|
+
|
|
1355
|
+
it('should handle missing sourceSocket parameter (treats as non-active)', () => {
|
|
1356
|
+
const closeEvent = {
|
|
1357
|
+
code: 4001,
|
|
1358
|
+
reason: 'replaced during shutdown',
|
|
1359
|
+
};
|
|
1360
|
+
|
|
1361
|
+
mercury._onclose(mercury.defaultSessionId, closeEvent); // No sourceSocket parameter
|
|
1362
|
+
|
|
1363
|
+
// With simplified logic, undefined !== this.socket, so isActiveSocket = false
|
|
1364
|
+
assert.calledWith(mercury._emit, mercury.defaultSessionId, 'offline.replaced', closeEvent);
|
|
1365
|
+
assert.notCalled(mercury._reconnect);
|
|
1366
|
+
});
|
|
1367
|
+
|
|
1368
|
+
it('should clean up event listeners from non-active socket when it closes', () => {
|
|
1369
|
+
const closeEvent = {
|
|
1370
|
+
code: 4001,
|
|
1371
|
+
reason: 'replaced during shutdown',
|
|
1372
|
+
};
|
|
1373
|
+
|
|
1374
|
+
// Close non-active socket (not the active one)
|
|
1375
|
+
mercury._onclose(mercury.defaultSessionId, closeEvent, anotherSocket);
|
|
1376
|
+
|
|
1377
|
+
// Verify listeners were removed from the old socket
|
|
1378
|
+
// The _onclose method checks if sourceSocket !== this.socket (non-active)
|
|
1379
|
+
// and then calls removeAllListeners in the else branch
|
|
1380
|
+
assert.calledOnce(anotherSocket.removeAllListeners);
|
|
1381
|
+
});
|
|
1382
|
+
|
|
1383
|
+
it('should not clean up listeners from active socket listeners until close handler runs', () => {
|
|
1384
|
+
const closeEvent = {
|
|
1385
|
+
code: 4001,
|
|
1386
|
+
reason: 'replaced during shutdown',
|
|
1387
|
+
};
|
|
1388
|
+
|
|
1389
|
+
// Close active socket
|
|
1390
|
+
mercury._onclose(mercury.defaultSessionId, closeEvent, mockSocket);
|
|
1391
|
+
|
|
1392
|
+
// Verify listeners were removed from active socket
|
|
1393
|
+
assert.calledOnce(mockSocket.removeAllListeners);
|
|
1394
|
+
});
|
|
1395
|
+
});
|
|
1396
|
+
|
|
1397
|
+
describe('shutdown switchover with retry logic', () => {
|
|
1398
|
+
let connectWithBackoffStub;
|
|
1399
|
+
const sessionId = 'mercury-default-session';
|
|
1400
|
+
|
|
1401
|
+
beforeEach(() => {
|
|
1402
|
+
mercury.connected = true;
|
|
1403
|
+
mercury.sockets.set(sessionId, {
|
|
1404
|
+
url: 'ws://old-socket.com',
|
|
1405
|
+
removeAllListeners: sinon.stub(),
|
|
1406
|
+
});
|
|
1407
|
+
mercury.socket = mercury.sockets.get(sessionId);
|
|
1408
|
+
connectWithBackoffStub = sinon.stub(mercury, '_connectWithBackoff');
|
|
1409
|
+
sinon.stub(mercury, '_emit');
|
|
1410
|
+
});
|
|
1411
|
+
|
|
1412
|
+
afterEach(() => {
|
|
1413
|
+
connectWithBackoffStub.restore();
|
|
1414
|
+
mercury._emit.restore();
|
|
1415
|
+
mercury.sockets.clear();
|
|
1416
|
+
});
|
|
1417
|
+
|
|
1418
|
+
it('should call _connectWithBackoff with shutdown switchover context', (done) => {
|
|
1419
|
+
connectWithBackoffStub.returns(Promise.resolve());
|
|
1420
|
+
|
|
1421
|
+
mercury._handleImminentShutdown(sessionId);
|
|
1422
|
+
|
|
1423
|
+
process.nextTick(() => {
|
|
1424
|
+
assert.calledOnce(connectWithBackoffStub);
|
|
1425
|
+
const callArgs = connectWithBackoffStub.firstCall.args;
|
|
1426
|
+
|
|
1427
|
+
assert.isUndefined(callArgs[0]); // webSocketUrl
|
|
1428
|
+
assert.equal(callArgs[1], sessionId);
|
|
1429
|
+
assert.isObject(callArgs[2]);
|
|
1430
|
+
assert.isTrue(callArgs[2].isShutdownSwitchover);
|
|
1431
|
+
assert.isObject(callArgs[2].attemptOptions);
|
|
1432
|
+
assert.isTrue(callArgs[2].attemptOptions.isShutdownSwitchover);
|
|
1433
|
+
done();
|
|
1434
|
+
});
|
|
1435
|
+
});
|
|
1436
|
+
|
|
1437
|
+
it('should set _shutdownSwitchoverInProgress flag during switchover', () => {
|
|
1438
|
+
// With the new behavior, "in progress" is represented by the presence
|
|
1439
|
+
// of an entry in _shutdownSwitchoverBackoffCalls.
|
|
1440
|
+
// Since _connectWithBackoff is stubbed in this suite, simulate its side-effect
|
|
1441
|
+
// of seeding the backoff-call map entry.
|
|
1442
|
+
connectWithBackoffStub.callsFake(() => {
|
|
1443
|
+
mercury._shutdownSwitchoverBackoffCalls.set(sessionId, {placeholder: true});
|
|
1444
|
+
return new Promise(() => {}); // Never resolves
|
|
1445
|
+
});
|
|
1446
|
+
|
|
1447
|
+
mercury._handleImminentShutdown(sessionId);
|
|
1448
|
+
|
|
1449
|
+
const switchoverBackoffCall = mercury._shutdownSwitchoverBackoffCalls.get(sessionId);
|
|
1450
|
+
assert.isOk(switchoverBackoffCall);
|
|
1451
|
+
});
|
|
1452
|
+
|
|
1453
|
+
it('should emit success event when switchover completes', async () => {
|
|
1454
|
+
connectWithBackoffStub.callsFake((url, sid, context) => {
|
|
1455
|
+
if (context && context.attemptOptions && context.attemptOptions.onSuccess) {
|
|
1456
|
+
const mockSocket = {url: 'ws://new-socket.com'};
|
|
1457
|
+
context.attemptOptions.onSuccess(mockSocket, 'ws://new-socket.com');
|
|
1458
|
+
}
|
|
1459
|
+
return Promise.resolve();
|
|
1460
|
+
});
|
|
1461
|
+
|
|
1462
|
+
mercury._handleImminentShutdown(sessionId);
|
|
1463
|
+
|
|
1464
|
+
await promiseTick(50);
|
|
1465
|
+
|
|
1466
|
+
const emitCalls = mercury._emit.getCalls();
|
|
1467
|
+
const hasCompleteEvent = emitCalls.some(
|
|
1468
|
+
(call) =>
|
|
1469
|
+
call.args[0] === sessionId &&
|
|
1470
|
+
call.args[1] === 'event:mercury_shutdown_switchover_complete'
|
|
1471
|
+
);
|
|
1472
|
+
|
|
1473
|
+
assert.isTrue(hasCompleteEvent, 'Should emit switchover complete event');
|
|
1474
|
+
});
|
|
1475
|
+
|
|
1476
|
+
it('should emit failure event when switchover exhausts retries', async () => {
|
|
1477
|
+
const testError = new Error('Connection failed');
|
|
1478
|
+
|
|
1479
|
+
connectWithBackoffStub.returns(Promise.reject(testError));
|
|
1480
|
+
|
|
1481
|
+
mercury._handleImminentShutdown(sessionId);
|
|
1482
|
+
await promiseTick(50);
|
|
1483
|
+
|
|
1484
|
+
const emitCalls = mercury._emit.getCalls();
|
|
1485
|
+
const hasFailureEvent = emitCalls.some(
|
|
1486
|
+
(call) =>
|
|
1487
|
+
call.args[0] === sessionId &&
|
|
1488
|
+
call.args[1] === 'event:mercury_shutdown_switchover_failed' &&
|
|
1489
|
+
call.args[2] &&
|
|
1490
|
+
call.args[2].reason === testError
|
|
1491
|
+
);
|
|
1492
|
+
|
|
1493
|
+
assert.isTrue(hasFailureEvent, 'Should emit switchover failed event');
|
|
1494
|
+
});
|
|
1495
|
+
|
|
1496
|
+
it('should allow old socket to be closed by server after switchover failure', async () => {
|
|
1497
|
+
connectWithBackoffStub.returns(Promise.reject(new Error('Failed')));
|
|
1498
|
+
|
|
1499
|
+
mercury._handleImminentShutdown(sessionId);
|
|
1500
|
+
await promiseTick(50);
|
|
1501
|
+
|
|
1502
|
+
assert.equal(mercury.socket.removeAllListeners.callCount, 0);
|
|
1503
|
+
});
|
|
1504
|
+
});
|
|
1505
|
+
|
|
1506
|
+
describe('#_prepareAndOpenSocket()', () => {
|
|
1507
|
+
let mockSocket, prepareUrlStub, getUserTokenStub;
|
|
1508
|
+
|
|
1509
|
+
beforeEach(() => {
|
|
1510
|
+
mockSocket = {
|
|
1511
|
+
open: sinon.stub().returns(Promise.resolve()),
|
|
1512
|
+
};
|
|
1513
|
+
prepareUrlStub = sinon
|
|
1514
|
+
.stub(mercury, '_prepareUrl')
|
|
1515
|
+
.returns(Promise.resolve('ws://example.com'));
|
|
1516
|
+
getUserTokenStub = webex.credentials.getUserToken;
|
|
1517
|
+
getUserTokenStub.returns(
|
|
1518
|
+
Promise.resolve({
|
|
1519
|
+
toString: () => 'mock-token',
|
|
1520
|
+
})
|
|
1521
|
+
);
|
|
1522
|
+
});
|
|
1523
|
+
|
|
1524
|
+
afterEach(() => {
|
|
1525
|
+
prepareUrlStub.restore();
|
|
1526
|
+
});
|
|
1527
|
+
|
|
1528
|
+
it('should prepare URL and get user token', async () => {
|
|
1529
|
+
await mercury._prepareAndOpenSocket(mockSocket, 'ws://test.com', false);
|
|
1530
|
+
|
|
1531
|
+
assert.calledOnce(prepareUrlStub);
|
|
1532
|
+
assert.calledWith(prepareUrlStub, 'ws://test.com');
|
|
1533
|
+
assert.calledOnce(getUserTokenStub);
|
|
1534
|
+
});
|
|
1535
|
+
|
|
1536
|
+
it('should open socket with correct options for normal connection', async () => {
|
|
1537
|
+
await mercury._prepareAndOpenSocket(mockSocket, undefined, false);
|
|
1538
|
+
|
|
1539
|
+
assert.calledOnce(mockSocket.open);
|
|
1540
|
+
const callArgs = mockSocket.open.firstCall.args;
|
|
1541
|
+
|
|
1542
|
+
assert.equal(callArgs[0], 'ws://example.com');
|
|
1543
|
+
assert.isObject(callArgs[1]);
|
|
1544
|
+
assert.equal(callArgs[1].token, 'mock-token');
|
|
1545
|
+
assert.isDefined(callArgs[1].forceCloseDelay);
|
|
1546
|
+
assert.isDefined(callArgs[1].pingInterval);
|
|
1547
|
+
assert.isDefined(callArgs[1].pongTimeout);
|
|
1548
|
+
});
|
|
1549
|
+
|
|
1550
|
+
it('should log with correct prefix for normal connection', async () => {
|
|
1551
|
+
await mercury._prepareAndOpenSocket(mockSocket, undefined, false);
|
|
1552
|
+
|
|
1553
|
+
// The method should complete successfully - we're testing it runs without error
|
|
1554
|
+
// Actual log message verification is complex due to existing stubs in parent scope
|
|
1555
|
+
assert.calledOnce(mockSocket.open);
|
|
1556
|
+
});
|
|
1557
|
+
|
|
1558
|
+
it('should log with shutdown prefix for shutdown connection', async () => {
|
|
1559
|
+
await mercury._prepareAndOpenSocket(mockSocket, undefined, true);
|
|
1560
|
+
|
|
1561
|
+
// The method should complete successfully with shutdown flag
|
|
1562
|
+
assert.calledOnce(mockSocket.open);
|
|
1563
|
+
});
|
|
1564
|
+
|
|
1565
|
+
it('should merge custom mercury options when provided', async () => {
|
|
1566
|
+
webex.config.defaultMercuryOptions = {
|
|
1567
|
+
customOption: 'test-value',
|
|
1568
|
+
pingInterval: 99999,
|
|
1569
|
+
};
|
|
1570
|
+
|
|
1571
|
+
await mercury._prepareAndOpenSocket(mockSocket, undefined, false);
|
|
1572
|
+
|
|
1573
|
+
const callArgs = mockSocket.open.firstCall.args;
|
|
1574
|
+
|
|
1575
|
+
assert.equal(callArgs[1].customOption, 'test-value');
|
|
1576
|
+
assert.equal(callArgs[1].pingInterval, 99999); // Custom value overrides default
|
|
1577
|
+
});
|
|
1578
|
+
|
|
1579
|
+
it('should return the webSocketUrl after opening', async () => {
|
|
1580
|
+
const result = await mercury._prepareAndOpenSocket(mockSocket, undefined, false);
|
|
1581
|
+
|
|
1582
|
+
assert.equal(result, 'ws://example.com');
|
|
1583
|
+
});
|
|
1584
|
+
|
|
1585
|
+
it('should handle errors during socket open', async () => {
|
|
1586
|
+
mockSocket.open.returns(Promise.reject(new Error('Open failed')));
|
|
1587
|
+
|
|
1588
|
+
try {
|
|
1589
|
+
await mercury._prepareAndOpenSocket(mockSocket, undefined, false);
|
|
1590
|
+
assert.fail('Should have thrown an error');
|
|
1591
|
+
} catch (err) {
|
|
1592
|
+
assert.equal(err.message, 'Open failed');
|
|
1593
|
+
}
|
|
1594
|
+
});
|
|
1595
|
+
});
|
|
1596
|
+
|
|
1597
|
+
describe('#_attemptConnection() with shutdown switchover', () => {
|
|
1598
|
+
let prepareAndOpenSocketStub, callback;
|
|
1599
|
+
const sessionId = 'mercury-default-session';
|
|
1600
|
+
|
|
1601
|
+
beforeEach(() => {
|
|
1602
|
+
prepareAndOpenSocketStub = sinon
|
|
1603
|
+
.stub(mercury, '_prepareAndOpenSocket')
|
|
1604
|
+
.returns(Promise.resolve('ws://new-socket.com'));
|
|
1605
|
+
callback = sinon.stub();
|
|
1606
|
+
mercury._shutdownSwitchoverBackoffCalls.set(sessionId, {abort: sinon.stub()});
|
|
1607
|
+
mercury.socket = {url: 'ws://test.com'};
|
|
1608
|
+
mercury.connected = true;
|
|
1609
|
+
sinon.stub(mercury, '_emit');
|
|
1610
|
+
sinon.stub(mercury, '_attachSocketEventListeners');
|
|
1611
|
+
});
|
|
1612
|
+
|
|
1613
|
+
afterEach(() => {
|
|
1614
|
+
prepareAndOpenSocketStub.restore();
|
|
1615
|
+
mercury._emit.restore();
|
|
1616
|
+
mercury._attachSocketEventListeners.restore();
|
|
1617
|
+
mercury._shutdownSwitchoverBackoffCalls.clear();
|
|
1618
|
+
});
|
|
1619
|
+
|
|
1620
|
+
it('should not set socket reference before opening for shutdown switchover', async () => {
|
|
1621
|
+
const originalSocket = mercury.socket;
|
|
1622
|
+
|
|
1623
|
+
await mercury._attemptConnection('ws://test.com', sessionId, callback, {
|
|
1624
|
+
isShutdownSwitchover: true,
|
|
1625
|
+
onSuccess: (newSocket, url) => {
|
|
1626
|
+
assert.equal(mercury.socket, originalSocket);
|
|
1627
|
+
},
|
|
1628
|
+
});
|
|
1629
|
+
|
|
1630
|
+
assert.equal(mercury.socket, originalSocket);
|
|
1631
|
+
});
|
|
1632
|
+
|
|
1633
|
+
it('should call onSuccess callback with new socket and URL for shutdown', async () => {
|
|
1634
|
+
const onSuccessStub = sinon.stub();
|
|
1635
|
+
|
|
1636
|
+
await mercury._attemptConnection('ws://test.com', sessionId, callback, {
|
|
1637
|
+
isShutdownSwitchover: true,
|
|
1638
|
+
onSuccess: onSuccessStub,
|
|
1639
|
+
});
|
|
1640
|
+
|
|
1641
|
+
assert.calledOnce(onSuccessStub);
|
|
1642
|
+
assert.equal(onSuccessStub.firstCall.args[1], 'ws://new-socket.com');
|
|
1643
|
+
});
|
|
1644
|
+
|
|
1645
|
+
it('should emit shutdown switchover complete event', async () => {
|
|
1646
|
+
await mercury._attemptConnection('ws://test.com', sessionId, callback, {
|
|
1647
|
+
isShutdownSwitchover: true,
|
|
1648
|
+
onSuccess: (newSocket, url) => {
|
|
1649
|
+
mercury.socket = newSocket;
|
|
1650
|
+
mercury.connected = true;
|
|
1651
|
+
mercury._emit(
|
|
1652
|
+
sessionId,
|
|
1653
|
+
'event:mercury_shutdown_switchover_complete',
|
|
1654
|
+
{url}
|
|
1655
|
+
);
|
|
1656
|
+
},
|
|
1657
|
+
});
|
|
1658
|
+
|
|
1659
|
+
assert.calledWith(
|
|
1660
|
+
mercury._emit,
|
|
1661
|
+
sessionId,
|
|
1662
|
+
'event:mercury_shutdown_switchover_complete',
|
|
1663
|
+
sinon.match.has('url', 'ws://new-socket.com')
|
|
1664
|
+
);
|
|
1665
|
+
});
|
|
1666
|
+
|
|
1667
|
+
it('should use simpler error handling for shutdown switchover failures', async () => {
|
|
1668
|
+
prepareAndOpenSocketStub.returns(Promise.reject(new Error('Connection failed')));
|
|
1669
|
+
|
|
1670
|
+
await mercury
|
|
1671
|
+
._attemptConnection('ws://test.com', sessionId, callback, {
|
|
1672
|
+
isShutdownSwitchover: true,
|
|
1673
|
+
})
|
|
1674
|
+
.catch(() => {});
|
|
1675
|
+
|
|
1676
|
+
assert.calledOnce(callback);
|
|
1677
|
+
assert.instanceOf(callback.firstCall.args[0], Error);
|
|
1678
|
+
});
|
|
1679
|
+
|
|
1680
|
+
it('should check _shutdownSwitchoverBackoffCall for shutdown connections', () => {
|
|
1681
|
+
mercury._shutdownSwitchoverBackoffCalls.clear();
|
|
1682
|
+
|
|
1683
|
+
const result = mercury._attemptConnection(
|
|
1684
|
+
'ws://test.com',
|
|
1685
|
+
sessionId,
|
|
1686
|
+
callback,
|
|
1687
|
+
{isShutdownSwitchover: true}
|
|
1688
|
+
);
|
|
1689
|
+
|
|
1690
|
+
return result.catch((err) => {
|
|
1691
|
+
assert.instanceOf(err, Error);
|
|
1692
|
+
assert.match(err.message, /switchover backoff call/);
|
|
1693
|
+
});
|
|
1694
|
+
});
|
|
1695
|
+
});
|
|
1696
|
+
|
|
1697
|
+
describe('#_connectWithBackoff() with shutdown switchover', () => {
|
|
1698
|
+
const sessionId = 'mercury-default-session';
|
|
1699
|
+
|
|
1700
|
+
it('should use shutdown-specific parameters when called', () => {
|
|
1701
|
+
const connectWithBackoffStub = sinon
|
|
1702
|
+
.stub(mercury, '_connectWithBackoff')
|
|
1703
|
+
.returns(Promise.resolve());
|
|
1704
|
+
|
|
1705
|
+
mercury._handleImminentShutdown(sessionId);
|
|
1706
|
+
|
|
1707
|
+
assert.calledOnce(connectWithBackoffStub);
|
|
1708
|
+
const callArgs = connectWithBackoffStub.firstCall.args;
|
|
1709
|
+
assert.equal(callArgs[1], sessionId);
|
|
1710
|
+
assert.isObject(callArgs[2]);
|
|
1711
|
+
assert.isTrue(callArgs[2].isShutdownSwitchover);
|
|
1712
|
+
|
|
1713
|
+
connectWithBackoffStub.restore();
|
|
1714
|
+
});
|
|
1715
|
+
|
|
1716
|
+
it('should pass shutdown switchover options to _attemptConnection', () => {
|
|
1717
|
+
const attemptStub = sinon.stub(mercury, '_attemptConnection');
|
|
1718
|
+
attemptStub.callsFake((url, sid, cb) => cb());
|
|
1719
|
+
|
|
1720
|
+
const context = {
|
|
1721
|
+
isShutdownSwitchover: true,
|
|
1722
|
+
attemptOptions: {
|
|
1723
|
+
isShutdownSwitchover: true,
|
|
1724
|
+
onSuccess: () => {},
|
|
1725
|
+
},
|
|
1726
|
+
};
|
|
1727
|
+
|
|
1728
|
+
const promise = mercury._connectWithBackoff(undefined, sessionId, context);
|
|
1729
|
+
|
|
1730
|
+
return promise.then(() => {
|
|
1731
|
+
assert.calledOnce(attemptStub);
|
|
1732
|
+
const callArgs = attemptStub.firstCall.args;
|
|
1733
|
+
assert.equal(callArgs[1], sessionId);
|
|
1734
|
+
assert.isObject(callArgs[3]);
|
|
1735
|
+
assert.isTrue(callArgs[3].isShutdownSwitchover);
|
|
1736
|
+
attemptStub.restore();
|
|
1737
|
+
});
|
|
1738
|
+
});
|
|
1739
|
+
|
|
1740
|
+
it('should set and clear state flags appropriately', () => {
|
|
1741
|
+
sinon.stub(mercury, '_attemptConnection').callsFake((url, sid, cb) => cb());
|
|
1742
|
+
|
|
1743
|
+
mercury._shutdownSwitchoverBackoffCalls.set(sessionId, {placeholder: true});
|
|
1744
|
+
|
|
1745
|
+
const promise = mercury._connectWithBackoff(undefined, sessionId, {
|
|
1746
|
+
isShutdownSwitchover: true,
|
|
1747
|
+
attemptOptions: {isShutdownSwitchover: true, onSuccess: () => {}},
|
|
1748
|
+
});
|
|
1749
|
+
|
|
1750
|
+
return promise.then(() => {
|
|
1751
|
+
assert.isUndefined(mercury._shutdownSwitchoverBackoffCalls.get(sessionId));
|
|
1752
|
+
mercury._attemptConnection.restore();
|
|
1753
|
+
});
|
|
1754
|
+
});
|
|
1755
|
+
});
|
|
1756
|
+
|
|
1757
|
+
describe('#disconnect() with shutdown switchover in progress', () => {
|
|
1758
|
+
let abortStub;
|
|
1759
|
+
const sessionId = 'mercury-default-session';
|
|
1760
|
+
|
|
1761
|
+
beforeEach(() => {
|
|
1762
|
+
mercury.sockets.clear();
|
|
1763
|
+
mercury.sockets.set(sessionId, {
|
|
1764
|
+
close: sinon.stub().returns(Promise.resolve()),
|
|
1765
|
+
removeAllListeners: sinon.stub(),
|
|
1766
|
+
});
|
|
1767
|
+
abortStub = sinon.stub();
|
|
1768
|
+
mercury._shutdownSwitchoverBackoffCalls.set(sessionId, {abort: abortStub});
|
|
1769
|
+
});
|
|
1770
|
+
|
|
1771
|
+
it('should abort shutdown switchover backoff call on disconnect', async () => {
|
|
1772
|
+
await mercury.disconnect(undefined, sessionId);
|
|
1773
|
+
|
|
1774
|
+
assert.calledOnce(abortStub);
|
|
1775
|
+
});
|
|
1776
|
+
|
|
1777
|
+
it('should handle disconnect when no switchover is in progress', async () => {
|
|
1778
|
+
mercury._shutdownSwitchoverBackoffCalls.clear();
|
|
1779
|
+
|
|
1780
|
+
await mercury.disconnect(undefined, sessionId);
|
|
1781
|
+
|
|
1782
|
+
assert.calledOnce(mercury.sockets.get(sessionId).close);
|
|
1783
|
+
});
|
|
1784
|
+
});
|
|
1785
|
+
});
|
|
1786
|
+
});
|
|
1787
|
+
});
|