@webex/internal-plugin-mercury 3.11.0 → 3.12.0-llmrefactor.2
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/README.md +54 -0
- package/dist/index.js +13 -2
- package/dist/index.js.map +1 -1
- package/dist/mercury-plugin.js +125 -0
- package/dist/mercury-plugin.js.map +1 -0
- package/dist/mercury.js +76 -33
- package/dist/mercury.js.map +1 -1
- package/dist/socket/constants.js +16 -0
- package/dist/socket/constants.js.map +1 -0
- package/dist/socket/socket-base.js +36 -3
- package/dist/socket/socket-base.js.map +1 -1
- package/package.json +17 -17
- package/src/index.js +3 -2
- package/src/mercury-plugin.ts +79 -0
- package/src/mercury.js +105 -49
- package/src/socket/constants.js +6 -0
- package/src/socket/socket-base.js +40 -3
- package/test/unit/spec/mercury-events.js +26 -2
- package/test/unit/spec/mercury-plugin.js +280 -0
- package/test/unit/spec/mercury.js +374 -136
- package/test/unit/spec/socket.js +61 -0
|
@@ -102,7 +102,48 @@ describe('plugin-mercury', () => {
|
|
|
102
102
|
mercury = webex.internal.mercury;
|
|
103
103
|
});
|
|
104
104
|
|
|
105
|
-
afterEach(() => {
|
|
105
|
+
afterEach(async () => {
|
|
106
|
+
// Clean up Mercury connections and internal state synchronously
|
|
107
|
+
// to avoid issues with fake timers
|
|
108
|
+
if (mercury) {
|
|
109
|
+
// Abort any pending backoff calls first
|
|
110
|
+
if (mercury.backoffCall) {
|
|
111
|
+
mercury.backoffCall.abort();
|
|
112
|
+
mercury.backoffCall = undefined;
|
|
113
|
+
}
|
|
114
|
+
if (mercury._shutdownSwitchoverBackoffCall) {
|
|
115
|
+
mercury._shutdownSwitchoverBackoffCall.abort();
|
|
116
|
+
mercury._shutdownSwitchoverBackoffCall = undefined;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Close socket directly without waiting for offline event
|
|
120
|
+
if (mercury.socket) {
|
|
121
|
+
if (typeof mercury.socket.removeAllListeners === 'function') {
|
|
122
|
+
mercury.socket.removeAllListeners();
|
|
123
|
+
}
|
|
124
|
+
try {
|
|
125
|
+
mercury.socket.close();
|
|
126
|
+
} catch (e) {
|
|
127
|
+
// Ignore cleanup errors
|
|
128
|
+
}
|
|
129
|
+
mercury.socket = undefined;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Clear any remaining connection promise
|
|
133
|
+
mercury._connectPromise = null;
|
|
134
|
+
mercury.connected = false;
|
|
135
|
+
mercury.connecting = false;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Ensure mock socket is properly closed
|
|
139
|
+
if (mockWebSocket && typeof mockWebSocket.close === 'function') {
|
|
140
|
+
try {
|
|
141
|
+
mockWebSocket.close();
|
|
142
|
+
} catch (e) {
|
|
143
|
+
// Ignore cleanup errors
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
106
147
|
if (socketOpenStub) {
|
|
107
148
|
socketOpenStub.restore();
|
|
108
149
|
}
|
|
@@ -110,6 +151,9 @@ describe('plugin-mercury', () => {
|
|
|
110
151
|
if (Socket.getWebSocketConstructor.restore) {
|
|
111
152
|
Socket.getWebSocketConstructor.restore();
|
|
112
153
|
}
|
|
154
|
+
|
|
155
|
+
// Small delay to ensure all async operations complete
|
|
156
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
113
157
|
});
|
|
114
158
|
|
|
115
159
|
describe('#listen()', () => {
|
|
@@ -268,18 +312,20 @@ describe('plugin-mercury', () => {
|
|
|
268
312
|
.then(() => {
|
|
269
313
|
assert.calledOnce(Socket.prototype.open);
|
|
270
314
|
|
|
271
|
-
|
|
315
|
+
// Wait for error handling from 1st failure to schedule 2nd timer
|
|
316
|
+
return promiseTick(10);
|
|
272
317
|
})
|
|
273
318
|
.then(() => {
|
|
274
319
|
clock.tick(mercury.config.backoffTimeReset);
|
|
275
320
|
|
|
276
|
-
|
|
321
|
+
// Wait for 2nd socket.open call AND error handling to schedule 3rd timer
|
|
322
|
+
return promiseTick(10);
|
|
277
323
|
})
|
|
278
324
|
.then(() => {
|
|
279
325
|
assert.calledTwice(Socket.prototype.open);
|
|
280
326
|
clock.tick(2 * mercury.config.backoffTimeReset);
|
|
281
327
|
|
|
282
|
-
return promiseTick(
|
|
328
|
+
return promiseTick(10);
|
|
283
329
|
})
|
|
284
330
|
.then(() => {
|
|
285
331
|
assert.calledThrice(Socket.prototype.open);
|
|
@@ -354,20 +400,20 @@ describe('plugin-mercury', () => {
|
|
|
354
400
|
.then(() => {
|
|
355
401
|
assert.calledOnce(Socket.prototype.open);
|
|
356
402
|
|
|
357
|
-
//
|
|
358
|
-
|
|
359
|
-
return promiseTick(5);
|
|
403
|
+
// Wait for error handling from 1st failure to schedule 2nd timer
|
|
404
|
+
return promiseTick(10);
|
|
360
405
|
})
|
|
361
406
|
.then(() => {
|
|
362
407
|
clock.tick(mercury.config.backoffTimeReset);
|
|
363
408
|
|
|
364
|
-
|
|
409
|
+
// Wait for 2nd socket.open call AND error handling to schedule 3rd timer
|
|
410
|
+
return promiseTick(10);
|
|
365
411
|
})
|
|
366
412
|
.then(() => {
|
|
367
413
|
assert.calledTwice(Socket.prototype.open);
|
|
368
414
|
clock.tick(2 * mercury.config.backoffTimeReset);
|
|
369
415
|
|
|
370
|
-
return promiseTick(
|
|
416
|
+
return promiseTick(10);
|
|
371
417
|
})
|
|
372
418
|
.then(() => {
|
|
373
419
|
assert.calledThrice(Socket.prototype.open);
|
|
@@ -467,15 +513,49 @@ describe('plugin-mercury', () => {
|
|
|
467
513
|
// });
|
|
468
514
|
|
|
469
515
|
describe('when web-high-availability feature is enabled', () => {
|
|
470
|
-
it('marks
|
|
516
|
+
it('marks the URL as failed when socket.open() fails with a ConnectionError', () => {
|
|
517
|
+
// When socket.open() fails, _prepareAndOpenSocket attaches webSocketUrl to the error.
|
|
518
|
+
// The error handler then uses wsUrl = newWSUrl || reason.webSocketUrl, so markFailedUrl
|
|
519
|
+
// is called with the URL obtained before the socket open attempt.
|
|
471
520
|
webex.internal.feature.getFeature.returns(Promise.resolve(true));
|
|
472
521
|
socketOpenStub.restore();
|
|
473
522
|
socketOpenStub = sinon.stub(Socket.prototype, 'open').returns(Promise.resolve());
|
|
474
523
|
socketOpenStub.onCall(0).returns(Promise.reject(new ConnectionError({code: 4001})));
|
|
475
524
|
const promise = mercury.connect();
|
|
476
525
|
|
|
477
|
-
return promiseTick(
|
|
526
|
+
return promiseTick(15).then(() => {
|
|
478
527
|
assert.calledOnce(webex.internal.services.markFailedUrl);
|
|
528
|
+
assert.calledWith(
|
|
529
|
+
webex.internal.services.markFailedUrl,
|
|
530
|
+
sinon.match(/example-2\.com/)
|
|
531
|
+
);
|
|
532
|
+
clock.tick(1000);
|
|
533
|
+
|
|
534
|
+
return promise;
|
|
535
|
+
});
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
it('does not mark a URL as failed when no wsUrl is available in ConnectionError', () => {
|
|
539
|
+
// If the error has no webSocketUrl and newWSUrl is also undefined,
|
|
540
|
+
// markFailedUrl should not be called.
|
|
541
|
+
webex.internal.feature.getFeature.returns(Promise.resolve(true));
|
|
542
|
+
|
|
543
|
+
const errWithNoUrl = new ConnectionError({code: 4001});
|
|
544
|
+
// Explicitly ensure no webSocketUrl on the error
|
|
545
|
+
delete errWithNoUrl.webSocketUrl;
|
|
546
|
+
|
|
547
|
+
// Stub _prepareAndOpenSocket so it rejects without attaching webSocketUrl
|
|
548
|
+
// (simulating a failure before socket.open is reached, e.g. _prepareUrl failure)
|
|
549
|
+
const prepareAndOpenStub = sinon.stub(mercury, '_prepareAndOpenSocket');
|
|
550
|
+
|
|
551
|
+
prepareAndOpenStub.onCall(0).returns(Promise.reject(errWithNoUrl));
|
|
552
|
+
prepareAndOpenStub.returns(Promise.resolve('ws://example-2.com'));
|
|
553
|
+
|
|
554
|
+
const promise = mercury.connect();
|
|
555
|
+
|
|
556
|
+
return promiseTick(15).then(() => {
|
|
557
|
+
assert.notCalled(webex.internal.services.markFailedUrl);
|
|
558
|
+
prepareAndOpenStub.restore();
|
|
479
559
|
clock.tick(1000);
|
|
480
560
|
|
|
481
561
|
return promise;
|
|
@@ -499,17 +579,22 @@ describe('plugin-mercury', () => {
|
|
|
499
579
|
|
|
500
580
|
// skipping due to apparent bug with lolex in all browsers but Chrome.
|
|
501
581
|
skipInBrowser(it)('does not continue attempting to connect', () => {
|
|
502
|
-
mercury.connect();
|
|
582
|
+
const promise = mercury.connect();
|
|
503
583
|
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
clock.tick(6 * webex.internal.mercury.config.backoffTimeReset);
|
|
584
|
+
// Wait for the connection to be established before proceeding
|
|
585
|
+
mockWebSocket.open();
|
|
507
586
|
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
587
|
+
return promise.then(() =>
|
|
588
|
+
promiseTick(2)
|
|
589
|
+
.then(() => {
|
|
590
|
+
clock.tick(6 * webex.internal.mercury.config.backoffTimeReset);
|
|
591
|
+
|
|
592
|
+
return promiseTick(2);
|
|
593
|
+
})
|
|
594
|
+
.then(() => {
|
|
595
|
+
assert.calledOnce(Socket.prototype.open);
|
|
596
|
+
})
|
|
597
|
+
);
|
|
513
598
|
});
|
|
514
599
|
});
|
|
515
600
|
|
|
@@ -583,44 +668,6 @@ describe('plugin-mercury', () => {
|
|
|
583
668
|
});
|
|
584
669
|
});
|
|
585
670
|
|
|
586
|
-
describe('#logout()', () => {
|
|
587
|
-
it('calls disconnect and logs', () => {
|
|
588
|
-
sinon.stub(mercury.logger, 'info');
|
|
589
|
-
sinon.stub(mercury, 'disconnect');
|
|
590
|
-
mercury.logout();
|
|
591
|
-
assert.called(mercury.disconnect);
|
|
592
|
-
assert.calledTwice(mercury.logger.info);
|
|
593
|
-
|
|
594
|
-
assert.calledWith(mercury.logger.info.getCall(0), 'Mercury: logout() called');
|
|
595
|
-
assert.isTrue(
|
|
596
|
-
mercury.logger.info
|
|
597
|
-
.getCall(1)
|
|
598
|
-
.args[0].startsWith('Mercury: debug_mercury_logging stack: ')
|
|
599
|
-
);
|
|
600
|
-
});
|
|
601
|
-
|
|
602
|
-
it('uses the config.beforeLogoutOptionsCloseReason to disconnect and will send code 3050 for logout', () => {
|
|
603
|
-
sinon.stub(mercury, 'disconnect');
|
|
604
|
-
mercury.config.beforeLogoutOptionsCloseReason = 'done (permanent)';
|
|
605
|
-
mercury.logout();
|
|
606
|
-
assert.calledWith(mercury.disconnect, {code: 3050, reason: 'done (permanent)'});
|
|
607
|
-
});
|
|
608
|
-
|
|
609
|
-
it('uses the config.beforeLogoutOptionsCloseReason to disconnect and will send code 3050 for logout if the reason is different than standard', () => {
|
|
610
|
-
sinon.stub(mercury, 'disconnect');
|
|
611
|
-
mercury.config.beforeLogoutOptionsCloseReason = 'test';
|
|
612
|
-
mercury.logout();
|
|
613
|
-
assert.calledWith(mercury.disconnect, {code: 3050, reason: 'test'});
|
|
614
|
-
});
|
|
615
|
-
|
|
616
|
-
it('uses the config.beforeLogoutOptionsCloseReason to disconnect and will send undefined for logout if the reason is same as standard', () => {
|
|
617
|
-
sinon.stub(mercury, 'disconnect');
|
|
618
|
-
mercury.config.beforeLogoutOptionsCloseReason = 'done (forced)';
|
|
619
|
-
mercury.logout();
|
|
620
|
-
assert.calledWith(mercury.disconnect, undefined);
|
|
621
|
-
});
|
|
622
|
-
});
|
|
623
|
-
|
|
624
671
|
describe('#disconnect()', () => {
|
|
625
672
|
it('disconnects the WebSocket', () =>
|
|
626
673
|
mercury
|
|
@@ -723,12 +770,15 @@ describe('plugin-mercury', () => {
|
|
|
723
770
|
return promiseTick(webex.internal.mercury.config.backoffTimeReset).then(() => {
|
|
724
771
|
// By this time backoffCall and mercury socket should be defined by the
|
|
725
772
|
// 'connect' call
|
|
726
|
-
assert.isDefined(
|
|
773
|
+
assert.isDefined(
|
|
774
|
+
mercury.backoffCall,
|
|
775
|
+
'Mercury backoffCall is not defined'
|
|
776
|
+
);
|
|
727
777
|
assert.isDefined(mercury.socket, 'Mercury socket is not defined');
|
|
728
778
|
// Calling disconnect will abort the backoffCall, close the socket, and
|
|
729
779
|
// reject the connect
|
|
730
780
|
mercury.disconnect();
|
|
731
|
-
assert.
|
|
781
|
+
assert.notExists(mercury.backoffCall, 'Mercury backoffCall is still defined');
|
|
732
782
|
// The socket will never be unset (which seems bad)
|
|
733
783
|
assert.isDefined(mercury.socket, 'Mercury socket is not defined');
|
|
734
784
|
|
|
@@ -747,7 +797,8 @@ describe('plugin-mercury', () => {
|
|
|
747
797
|
let reason;
|
|
748
798
|
|
|
749
799
|
mercury.backoffCall = undefined;
|
|
750
|
-
|
|
800
|
+
|
|
801
|
+
const promise = mercury._attemptConnection('ws://example.com', (_reason) => {
|
|
751
802
|
reason = _reason;
|
|
752
803
|
});
|
|
753
804
|
|
|
@@ -756,6 +807,9 @@ describe('plugin-mercury', () => {
|
|
|
756
807
|
reason.message,
|
|
757
808
|
'Mercury: prevent socket open when backoffCall no longer defined'
|
|
758
809
|
);
|
|
810
|
+
|
|
811
|
+
// Ensure the promise was actually rejected (short-circuited)
|
|
812
|
+
return assert.isRejected(promise);
|
|
759
813
|
});
|
|
760
814
|
});
|
|
761
815
|
|
|
@@ -1016,7 +1070,8 @@ describe('plugin-mercury', () => {
|
|
|
1016
1070
|
});
|
|
1017
1071
|
|
|
1018
1072
|
it('should be idempotent - no-op if already in progress', () => {
|
|
1019
|
-
|
|
1073
|
+
// Simulate an existing switchover in progress
|
|
1074
|
+
mercury._shutdownSwitchoverBackoffCall = {abort: sinon.stub()};
|
|
1020
1075
|
|
|
1021
1076
|
mercury._handleImminentShutdown();
|
|
1022
1077
|
|
|
@@ -1026,8 +1081,15 @@ describe('plugin-mercury', () => {
|
|
|
1026
1081
|
it('should set switchover flags when called', () => {
|
|
1027
1082
|
mercury._handleImminentShutdown();
|
|
1028
1083
|
|
|
1029
|
-
assert.isTrue(mercury._shutdownSwitchoverInProgress);
|
|
1030
1084
|
assert.isDefined(mercury._shutdownSwitchoverId);
|
|
1085
|
+
|
|
1086
|
+
assert.calledOnce(connectWithBackoffStub);
|
|
1087
|
+
const callArgs = connectWithBackoffStub.firstCall.args;
|
|
1088
|
+
assert.isUndefined(callArgs[0]); // webSocketUrl
|
|
1089
|
+
assert.isObject(callArgs[1]); // context
|
|
1090
|
+
assert.isTrue(callArgs[1].isShutdownSwitchover);
|
|
1091
|
+
assert.isObject(callArgs[1].attemptOptions);
|
|
1092
|
+
assert.isTrue(callArgs[1].attemptOptions.isShutdownSwitchover);
|
|
1031
1093
|
});
|
|
1032
1094
|
|
|
1033
1095
|
it('should call _connectWithBackoff with correct parameters', (done) => {
|
|
@@ -1039,6 +1101,8 @@ describe('plugin-mercury', () => {
|
|
|
1039
1101
|
assert.isUndefined(callArgs[0]); // webSocketUrl
|
|
1040
1102
|
assert.isObject(callArgs[1]); // context
|
|
1041
1103
|
assert.isTrue(callArgs[1].isShutdownSwitchover);
|
|
1104
|
+
assert.isObject(callArgs[1].attemptOptions);
|
|
1105
|
+
assert.isTrue(callArgs[1].attemptOptions.isShutdownSwitchover);
|
|
1042
1106
|
done();
|
|
1043
1107
|
});
|
|
1044
1108
|
});
|
|
@@ -1049,7 +1113,9 @@ describe('plugin-mercury', () => {
|
|
|
1049
1113
|
|
|
1050
1114
|
mercury._handleImminentShutdown();
|
|
1051
1115
|
|
|
1052
|
-
|
|
1116
|
+
// When an exception happens synchronously, the backoff call should be cleared
|
|
1117
|
+
assert.notExists(mercury._shutdownSwitchoverBackoffCall);
|
|
1118
|
+
mercury._connectWithBackoff.restore();
|
|
1053
1119
|
});
|
|
1054
1120
|
});
|
|
1055
1121
|
|
|
@@ -1108,6 +1174,117 @@ describe('plugin-mercury', () => {
|
|
|
1108
1174
|
});
|
|
1109
1175
|
});
|
|
1110
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 emit generic event and return early when envelope.data (nested) is undefined', async () => {
|
|
1191
|
+
const event = {
|
|
1192
|
+
data: {
|
|
1193
|
+
type: 'someType',
|
|
1194
|
+
// no nested data property
|
|
1195
|
+
},
|
|
1196
|
+
};
|
|
1197
|
+
|
|
1198
|
+
const result = mercury._onmessage(event);
|
|
1199
|
+
|
|
1200
|
+
assert.instanceOf(result, Promise);
|
|
1201
|
+
await result;
|
|
1202
|
+
assert.calledOnceWithExactly(mercury._emit, 'event', event.data);
|
|
1203
|
+
assert.notCalled(mercury._applyOverrides);
|
|
1204
|
+
});
|
|
1205
|
+
|
|
1206
|
+
it('should emit generic event and return early when data.eventType is undefined', async () => {
|
|
1207
|
+
const event = {
|
|
1208
|
+
data: {
|
|
1209
|
+
type: 'someType',
|
|
1210
|
+
data: {
|
|
1211
|
+
// no eventType property
|
|
1212
|
+
someField: 'value',
|
|
1213
|
+
},
|
|
1214
|
+
},
|
|
1215
|
+
};
|
|
1216
|
+
|
|
1217
|
+
const result = mercury._onmessage(event);
|
|
1218
|
+
|
|
1219
|
+
assert.instanceOf(result, Promise);
|
|
1220
|
+
await result;
|
|
1221
|
+
assert.calledOnceWithExactly(mercury._emit, 'event', event.data);
|
|
1222
|
+
assert.notCalled(mercury._applyOverrides);
|
|
1223
|
+
});
|
|
1224
|
+
|
|
1225
|
+
it('should emit generic event for messages without eventType (e.g. subscription responses)', async () => {
|
|
1226
|
+
const event = {
|
|
1227
|
+
data: {
|
|
1228
|
+
id: 'msg-123',
|
|
1229
|
+
sequenceNumber: 5,
|
|
1230
|
+
data: {
|
|
1231
|
+
statusCode: 200,
|
|
1232
|
+
},
|
|
1233
|
+
},
|
|
1234
|
+
};
|
|
1235
|
+
|
|
1236
|
+
const result = mercury._onmessage(event);
|
|
1237
|
+
|
|
1238
|
+
assert.instanceOf(result, Promise);
|
|
1239
|
+
await result;
|
|
1240
|
+
assert.calledOnceWithExactly(mercury._emit, 'event', event.data);
|
|
1241
|
+
assert.notCalled(mercury._applyOverrides);
|
|
1242
|
+
});
|
|
1243
|
+
|
|
1244
|
+
it('should still process messages with a valid eventType', async () => {
|
|
1245
|
+
const event = {
|
|
1246
|
+
data: {
|
|
1247
|
+
data: {
|
|
1248
|
+
eventType: 'conversation.activity',
|
|
1249
|
+
},
|
|
1250
|
+
},
|
|
1251
|
+
};
|
|
1252
|
+
|
|
1253
|
+
await mercury._onmessage(event);
|
|
1254
|
+
|
|
1255
|
+
// Normal flow emits namespace-specific events after processing handlers.
|
|
1256
|
+
// The early-return guard only emits 'event', so asserting these proves the normal path was taken.
|
|
1257
|
+
assert.calledWith(mercury._emit, 'event:conversation', event.data);
|
|
1258
|
+
assert.calledWith(mercury._emit, 'event:conversation.activity', event.data);
|
|
1259
|
+
});
|
|
1260
|
+
});
|
|
1261
|
+
|
|
1262
|
+
describe('#_getEventHandlers()', () => {
|
|
1263
|
+
it('should return an empty array when eventType is undefined', () => {
|
|
1264
|
+
const result = mercury._getEventHandlers(undefined);
|
|
1265
|
+
|
|
1266
|
+
assert.deepEqual(result, []);
|
|
1267
|
+
});
|
|
1268
|
+
|
|
1269
|
+
it('should return an empty array when eventType is null', () => {
|
|
1270
|
+
const result = mercury._getEventHandlers(null);
|
|
1271
|
+
|
|
1272
|
+
assert.deepEqual(result, []);
|
|
1273
|
+
});
|
|
1274
|
+
|
|
1275
|
+
it('should return an empty array when eventType is an empty string', () => {
|
|
1276
|
+
const result = mercury._getEventHandlers('');
|
|
1277
|
+
|
|
1278
|
+
assert.deepEqual(result, []);
|
|
1279
|
+
});
|
|
1280
|
+
|
|
1281
|
+
it('should return an empty array when namespace is not registered', () => {
|
|
1282
|
+
const result = mercury._getEventHandlers('unknownNamespace.someEvent');
|
|
1283
|
+
|
|
1284
|
+
assert.deepEqual(result, []);
|
|
1285
|
+
});
|
|
1286
|
+
});
|
|
1287
|
+
|
|
1111
1288
|
describe('#_onclose() with code 4001 (shutdown replacement)', () => {
|
|
1112
1289
|
let mockSocket, anotherSocket;
|
|
1113
1290
|
|
|
@@ -1201,8 +1378,6 @@ describe('plugin-mercury', () => {
|
|
|
1201
1378
|
mercury._onclose(closeEvent, anotherSocket);
|
|
1202
1379
|
|
|
1203
1380
|
// Verify listeners were removed from the old socket
|
|
1204
|
-
// The _onclose method checks if sourceSocket !== this.socket (non-active)
|
|
1205
|
-
// and then calls removeAllListeners in the else branch
|
|
1206
1381
|
assert.calledOnce(anotherSocket.removeAllListeners);
|
|
1207
1382
|
});
|
|
1208
1383
|
|
|
@@ -1220,6 +1395,69 @@ describe('plugin-mercury', () => {
|
|
|
1220
1395
|
});
|
|
1221
1396
|
});
|
|
1222
1397
|
|
|
1398
|
+
describe('#_onclose() reconnect URL derivation', () => {
|
|
1399
|
+
// The URL resolved and stored in _prepareAndOpenSocket (catalog-valid host).
|
|
1400
|
+
const resolvedUrl = 'wss://mercury-connection.wbx2.com/v1/apps/wx2/registrations';
|
|
1401
|
+
// The URL the native socket was actually opened with, after a lower
|
|
1402
|
+
// layer (e.g. the same-site websocket proxy) rewrote it into a
|
|
1403
|
+
// non-catalog host. Reusing this would fail host-catalog validation.
|
|
1404
|
+
const proxiedUrl = 'wss://web.webex.com/webproxy/mercury-connection.wbx2.com/v1/apps/wx2';
|
|
1405
|
+
let sourceSocket;
|
|
1406
|
+
|
|
1407
|
+
beforeEach(() => {
|
|
1408
|
+
sourceSocket = {
|
|
1409
|
+
url: proxiedUrl,
|
|
1410
|
+
removeAllListeners: sinon.stub(),
|
|
1411
|
+
};
|
|
1412
|
+
mercury.socket = sourceSocket;
|
|
1413
|
+
mercury.resolvedWebSocketUrl = resolvedUrl;
|
|
1414
|
+
mercury.connected = true;
|
|
1415
|
+
sinon.stub(mercury, '_emit');
|
|
1416
|
+
sinon.stub(mercury, '_reconnect');
|
|
1417
|
+
sinon.stub(mercury, 'unset');
|
|
1418
|
+
});
|
|
1419
|
+
|
|
1420
|
+
afterEach(() => {
|
|
1421
|
+
mercury._emit.restore();
|
|
1422
|
+
mercury._reconnect.restore();
|
|
1423
|
+
mercury.unset.restore();
|
|
1424
|
+
});
|
|
1425
|
+
|
|
1426
|
+
[1001, 1005, 1006, 1011].forEach((code) => {
|
|
1427
|
+
it(`reconnects an active socket using the stored resolved URL (not the proxied socket.url) for code ${code}`, () => {
|
|
1428
|
+
mercury._onclose({code, reason: 'transient'}, sourceSocket);
|
|
1429
|
+
|
|
1430
|
+
assert.calledOnceWithExactly(mercury._reconnect, resolvedUrl);
|
|
1431
|
+
assert.neverCalledWith(mercury._reconnect, proxiedUrl);
|
|
1432
|
+
});
|
|
1433
|
+
});
|
|
1434
|
+
|
|
1435
|
+
it('reconnects using the stored resolved URL for a normal close (1000) with a reconnect reason', () => {
|
|
1436
|
+
mercury._onclose({code: 1000, reason: 'done (forced)'}, sourceSocket);
|
|
1437
|
+
|
|
1438
|
+
assert.calledOnceWithExactly(mercury._reconnect, resolvedUrl);
|
|
1439
|
+
});
|
|
1440
|
+
|
|
1441
|
+
it('does not reconnect when a non-active (old) socket closes', () => {
|
|
1442
|
+
const oldSocket = {
|
|
1443
|
+
url: proxiedUrl,
|
|
1444
|
+
removeAllListeners: sinon.stub(),
|
|
1445
|
+
};
|
|
1446
|
+
|
|
1447
|
+
mercury._onclose({code: 1006, reason: 'transient'}, oldSocket);
|
|
1448
|
+
|
|
1449
|
+
assert.notCalled(mercury._reconnect);
|
|
1450
|
+
});
|
|
1451
|
+
|
|
1452
|
+
it('falls back to socket.url when resolvedWebSocketUrl is not set', () => {
|
|
1453
|
+
mercury.resolvedWebSocketUrl = undefined;
|
|
1454
|
+
|
|
1455
|
+
mercury._onclose({code: 1006, reason: 'transient'}, sourceSocket);
|
|
1456
|
+
|
|
1457
|
+
assert.calledOnceWithExactly(mercury._reconnect, proxiedUrl);
|
|
1458
|
+
});
|
|
1459
|
+
});
|
|
1460
|
+
|
|
1223
1461
|
describe('shutdown switchover with retry logic', () => {
|
|
1224
1462
|
let connectWithBackoffStub;
|
|
1225
1463
|
|
|
@@ -1243,13 +1481,12 @@ describe('plugin-mercury', () => {
|
|
|
1243
1481
|
|
|
1244
1482
|
mercury._handleImminentShutdown();
|
|
1245
1483
|
|
|
1246
|
-
// Give it a tick for the async call to happen
|
|
1247
1484
|
process.nextTick(() => {
|
|
1248
1485
|
assert.calledOnce(connectWithBackoffStub);
|
|
1249
1486
|
const callArgs = connectWithBackoffStub.firstCall.args;
|
|
1250
1487
|
|
|
1251
|
-
assert.isUndefined(callArgs[0]); // webSocketUrl
|
|
1252
|
-
assert.isObject(callArgs[1]);
|
|
1488
|
+
assert.isUndefined(callArgs[0]); // webSocketUrl
|
|
1489
|
+
assert.isObject(callArgs[1]);
|
|
1253
1490
|
assert.isTrue(callArgs[1].isShutdownSwitchover);
|
|
1254
1491
|
assert.isObject(callArgs[1].attemptOptions);
|
|
1255
1492
|
assert.isTrue(callArgs[1].attemptOptions.isShutdownSwitchover);
|
|
@@ -1257,18 +1494,21 @@ describe('plugin-mercury', () => {
|
|
|
1257
1494
|
});
|
|
1258
1495
|
});
|
|
1259
1496
|
|
|
1260
|
-
it('should set
|
|
1261
|
-
|
|
1497
|
+
it('should set _shutdownSwitchoverBackoffCall during switchover', () => {
|
|
1498
|
+
// Since _connectWithBackoff is stubbed, we can just check that
|
|
1499
|
+
// _handleImminentShutdown sets the flag
|
|
1500
|
+
connectWithBackoffStub.callsFake(() => {
|
|
1501
|
+
return new Promise(() => {}); // Never resolves
|
|
1502
|
+
});
|
|
1262
1503
|
|
|
1263
1504
|
mercury._handleImminentShutdown();
|
|
1264
1505
|
|
|
1265
|
-
|
|
1506
|
+
// The switchover id is set when switchover starts
|
|
1507
|
+
assert.isDefined(mercury._shutdownSwitchoverId);
|
|
1266
1508
|
});
|
|
1267
1509
|
|
|
1268
1510
|
it('should emit success event when switchover completes', async () => {
|
|
1269
|
-
// We need to actually call the onSuccess callback to trigger the event
|
|
1270
1511
|
connectWithBackoffStub.callsFake((url, context) => {
|
|
1271
|
-
// Simulate successful connection by calling onSuccess
|
|
1272
1512
|
if (context && context.attemptOptions && context.attemptOptions.onSuccess) {
|
|
1273
1513
|
const mockSocket = {url: 'ws://new-socket.com'};
|
|
1274
1514
|
context.attemptOptions.onSuccess(mockSocket, 'ws://new-socket.com');
|
|
@@ -1278,7 +1518,6 @@ describe('plugin-mercury', () => {
|
|
|
1278
1518
|
|
|
1279
1519
|
mercury._handleImminentShutdown();
|
|
1280
1520
|
|
|
1281
|
-
// Wait for async operations
|
|
1282
1521
|
await promiseTick(50);
|
|
1283
1522
|
|
|
1284
1523
|
const emitCalls = mercury._emit.getCalls();
|
|
@@ -1292,12 +1531,11 @@ describe('plugin-mercury', () => {
|
|
|
1292
1531
|
it('should emit failure event when switchover exhausts retries', async () => {
|
|
1293
1532
|
const testError = new Error('Connection failed');
|
|
1294
1533
|
|
|
1295
|
-
connectWithBackoffStub.
|
|
1534
|
+
connectWithBackoffStub.callsFake(() => Promise.reject(testError));
|
|
1296
1535
|
|
|
1297
1536
|
mercury._handleImminentShutdown();
|
|
1298
1537
|
await promiseTick(50);
|
|
1299
1538
|
|
|
1300
|
-
// Check if failure event was emitted
|
|
1301
1539
|
const emitCalls = mercury._emit.getCalls();
|
|
1302
1540
|
const hasFailureEvent = emitCalls.some(
|
|
1303
1541
|
(call) =>
|
|
@@ -1310,12 +1548,11 @@ describe('plugin-mercury', () => {
|
|
|
1310
1548
|
});
|
|
1311
1549
|
|
|
1312
1550
|
it('should allow old socket to be closed by server after switchover failure', async () => {
|
|
1313
|
-
connectWithBackoffStub.
|
|
1551
|
+
connectWithBackoffStub.callsFake(() => Promise.reject(new Error('Failed')));
|
|
1314
1552
|
|
|
1315
1553
|
mercury._handleImminentShutdown();
|
|
1316
1554
|
await promiseTick(50);
|
|
1317
1555
|
|
|
1318
|
-
// Old socket should not be closed immediately - server will close it
|
|
1319
1556
|
assert.equal(mercury.socket.removeAllListeners.callCount, 0);
|
|
1320
1557
|
});
|
|
1321
1558
|
});
|
|
@@ -1399,6 +1636,23 @@ describe('plugin-mercury', () => {
|
|
|
1399
1636
|
assert.equal(result, 'ws://example.com');
|
|
1400
1637
|
});
|
|
1401
1638
|
|
|
1639
|
+
it('stores the resolved (pre-proxy) URL in resolvedWebSocketUrl', async () => {
|
|
1640
|
+
await mercury._prepareAndOpenSocket(mockSocket, 'ws://original.com', false);
|
|
1641
|
+
|
|
1642
|
+
// The stored value is the URL resolved by _prepareUrl, not the raw
|
|
1643
|
+
// input, so _onclose can later reconnect from a catalog-valid host.
|
|
1644
|
+
assert.equal(mercury.resolvedWebSocketUrl, 'ws://example.com');
|
|
1645
|
+
});
|
|
1646
|
+
|
|
1647
|
+
it('does not store resolvedWebSocketUrl for shutdown switchover connections', async () => {
|
|
1648
|
+
mercury.resolvedWebSocketUrl = 'ws://existing.com';
|
|
1649
|
+
|
|
1650
|
+
await mercury._prepareAndOpenSocket(mockSocket, 'ws://original.com', true);
|
|
1651
|
+
|
|
1652
|
+
// Shutdown switchover should not overwrite the stored URL
|
|
1653
|
+
assert.equal(mercury.resolvedWebSocketUrl, 'ws://existing.com');
|
|
1654
|
+
});
|
|
1655
|
+
|
|
1402
1656
|
it('should handle errors during socket open', async () => {
|
|
1403
1657
|
mockSocket.open.returns(Promise.reject(new Error('Open failed')));
|
|
1404
1658
|
|
|
@@ -1407,23 +1661,21 @@ describe('plugin-mercury', () => {
|
|
|
1407
1661
|
assert.fail('Should have thrown an error');
|
|
1408
1662
|
} catch (err) {
|
|
1409
1663
|
assert.equal(err.message, 'Open failed');
|
|
1664
|
+
assert.equal(err.webSocketUrl, 'ws://example.com');
|
|
1410
1665
|
}
|
|
1411
1666
|
});
|
|
1412
1667
|
});
|
|
1413
1668
|
|
|
1414
1669
|
describe('#_attemptConnection() with shutdown switchover', () => {
|
|
1415
|
-
let
|
|
1670
|
+
let prepareAndOpenSocketStub, callback;
|
|
1416
1671
|
|
|
1417
1672
|
beforeEach(() => {
|
|
1418
|
-
mockSocket = {
|
|
1419
|
-
url: 'ws://test.com',
|
|
1420
|
-
};
|
|
1421
1673
|
prepareAndOpenSocketStub = sinon
|
|
1422
1674
|
.stub(mercury, '_prepareAndOpenSocket')
|
|
1423
1675
|
.returns(Promise.resolve('ws://new-socket.com'));
|
|
1424
1676
|
callback = sinon.stub();
|
|
1425
|
-
mercury._shutdownSwitchoverBackoffCall = {};
|
|
1426
|
-
mercury.socket =
|
|
1677
|
+
mercury._shutdownSwitchoverBackoffCall = {abort: sinon.stub()};
|
|
1678
|
+
mercury.socket = {url: 'ws://test.com'};
|
|
1427
1679
|
mercury.connected = true;
|
|
1428
1680
|
sinon.stub(mercury, '_emit');
|
|
1429
1681
|
sinon.stub(mercury, '_attachSocketEventListeners');
|
|
@@ -1433,6 +1685,7 @@ describe('plugin-mercury', () => {
|
|
|
1433
1685
|
prepareAndOpenSocketStub.restore();
|
|
1434
1686
|
mercury._emit.restore();
|
|
1435
1687
|
mercury._attachSocketEventListeners.restore();
|
|
1688
|
+
mercury._shutdownSwitchoverBackoffCall = undefined;
|
|
1436
1689
|
});
|
|
1437
1690
|
|
|
1438
1691
|
it('should not set socket reference before opening for shutdown switchover', async () => {
|
|
@@ -1441,13 +1694,10 @@ describe('plugin-mercury', () => {
|
|
|
1441
1694
|
await mercury._attemptConnection('ws://test.com', callback, {
|
|
1442
1695
|
isShutdownSwitchover: true,
|
|
1443
1696
|
onSuccess: (newSocket, url) => {
|
|
1444
|
-
// During onSuccess, verify original socket is still set
|
|
1445
|
-
// (socket swap happens inside onSuccess callback in _handleImminentShutdown)
|
|
1446
1697
|
assert.equal(mercury.socket, originalSocket);
|
|
1447
1698
|
},
|
|
1448
1699
|
});
|
|
1449
1700
|
|
|
1450
|
-
// After onSuccess, socket should still be original since we only swap in _handleImminentShutdown
|
|
1451
1701
|
assert.equal(mercury.socket, originalSocket);
|
|
1452
1702
|
});
|
|
1453
1703
|
|
|
@@ -1464,12 +1714,9 @@ describe('plugin-mercury', () => {
|
|
|
1464
1714
|
});
|
|
1465
1715
|
|
|
1466
1716
|
it('should emit shutdown switchover complete event', async () => {
|
|
1467
|
-
const oldSocket = mercury.socket;
|
|
1468
|
-
|
|
1469
1717
|
await mercury._attemptConnection('ws://test.com', callback, {
|
|
1470
1718
|
isShutdownSwitchover: true,
|
|
1471
1719
|
onSuccess: (newSocket, url) => {
|
|
1472
|
-
// Simulate the onSuccess callback behavior
|
|
1473
1720
|
mercury.socket = newSocket;
|
|
1474
1721
|
mercury.connected = true;
|
|
1475
1722
|
mercury._emit('event:mercury_shutdown_switchover_complete', {url});
|
|
@@ -1486,15 +1733,12 @@ describe('plugin-mercury', () => {
|
|
|
1486
1733
|
it('should use simpler error handling for shutdown switchover failures', async () => {
|
|
1487
1734
|
prepareAndOpenSocketStub.returns(Promise.reject(new Error('Connection failed')));
|
|
1488
1735
|
|
|
1489
|
-
|
|
1490
|
-
|
|
1736
|
+
await mercury
|
|
1737
|
+
._attemptConnection('ws://test.com', callback, {
|
|
1491
1738
|
isShutdownSwitchover: true,
|
|
1492
|
-
})
|
|
1493
|
-
|
|
1494
|
-
// Error should be caught and passed to callback
|
|
1495
|
-
}
|
|
1739
|
+
})
|
|
1740
|
+
.catch(() => {});
|
|
1496
1741
|
|
|
1497
|
-
// Should call callback with error for retry
|
|
1498
1742
|
assert.calledOnce(callback);
|
|
1499
1743
|
assert.instanceOf(callback.firstCall.args[0], Error);
|
|
1500
1744
|
});
|
|
@@ -1514,35 +1758,26 @@ describe('plugin-mercury', () => {
|
|
|
1514
1758
|
});
|
|
1515
1759
|
|
|
1516
1760
|
describe('#_connectWithBackoff() with shutdown switchover', () => {
|
|
1517
|
-
// Note: These tests verify the parameterization logic without running real backoff timers
|
|
1518
|
-
// to avoid test hangs. The backoff mechanism itself is tested in other test suites.
|
|
1519
|
-
|
|
1520
1761
|
it('should use shutdown-specific parameters when called', () => {
|
|
1521
|
-
// Stub _connectWithBackoff to prevent real execution
|
|
1522
1762
|
const connectWithBackoffStub = sinon
|
|
1523
1763
|
.stub(mercury, '_connectWithBackoff')
|
|
1524
1764
|
.returns(Promise.resolve());
|
|
1525
1765
|
|
|
1526
1766
|
mercury._handleImminentShutdown();
|
|
1527
1767
|
|
|
1528
|
-
// Verify it was called with shutdown context
|
|
1529
1768
|
assert.calledOnce(connectWithBackoffStub);
|
|
1530
1769
|
const callArgs = connectWithBackoffStub.firstCall.args;
|
|
1531
|
-
|
|
1770
|
+
// First arg is the URL, second is the context object
|
|
1771
|
+
assert.isObject(callArgs[1]);
|
|
1532
1772
|
assert.isTrue(callArgs[1].isShutdownSwitchover);
|
|
1533
1773
|
|
|
1534
1774
|
connectWithBackoffStub.restore();
|
|
1535
1775
|
});
|
|
1536
1776
|
|
|
1537
1777
|
it('should pass shutdown switchover options to _attemptConnection', () => {
|
|
1538
|
-
// Stub _attemptConnection to verify it receives correct options
|
|
1539
1778
|
const attemptStub = sinon.stub(mercury, '_attemptConnection');
|
|
1540
|
-
attemptStub.callsFake((url,
|
|
1541
|
-
// Immediately succeed
|
|
1542
|
-
callback();
|
|
1543
|
-
});
|
|
1779
|
+
attemptStub.callsFake((url, cb) => cb());
|
|
1544
1780
|
|
|
1545
|
-
// Call _connectWithBackoff with shutdown context
|
|
1546
1781
|
const context = {
|
|
1547
1782
|
isShutdownSwitchover: true,
|
|
1548
1783
|
attemptOptions: {
|
|
@@ -1551,25 +1786,25 @@ describe('plugin-mercury', () => {
|
|
|
1551
1786
|
},
|
|
1552
1787
|
};
|
|
1553
1788
|
|
|
1554
|
-
// Start the backoff
|
|
1555
1789
|
const promise = mercury._connectWithBackoff(undefined, context);
|
|
1556
1790
|
|
|
1557
|
-
// Check that _attemptConnection was called with shutdown options
|
|
1558
1791
|
return promise.then(() => {
|
|
1559
1792
|
assert.calledOnce(attemptStub);
|
|
1560
1793
|
const callArgs = attemptStub.firstCall.args;
|
|
1561
|
-
assert.isObject(callArgs[2]);
|
|
1794
|
+
assert.isObject(callArgs[2]);
|
|
1562
1795
|
assert.isTrue(callArgs[2].isShutdownSwitchover);
|
|
1563
|
-
|
|
1564
1796
|
attemptStub.restore();
|
|
1565
1797
|
});
|
|
1566
1798
|
});
|
|
1567
1799
|
|
|
1568
|
-
it('should set and clear
|
|
1569
|
-
|
|
1570
|
-
sinon.stub(mercury, '_attemptConnection').callsFake((url, callback) => callback());
|
|
1800
|
+
it('should set _shutdownSwitchoverBackoffCall during attempt and clear it after completion', () => {
|
|
1801
|
+
let backoffCallDuringAttempt;
|
|
1571
1802
|
|
|
1572
|
-
mercury.
|
|
1803
|
+
sinon.stub(mercury, '_attemptConnection').callsFake((url, cb) => {
|
|
1804
|
+
// Capture the backoff call state during the connection attempt
|
|
1805
|
+
backoffCallDuringAttempt = mercury._shutdownSwitchoverBackoffCall;
|
|
1806
|
+
cb();
|
|
1807
|
+
});
|
|
1573
1808
|
|
|
1574
1809
|
const promise = mercury._connectWithBackoff(undefined, {
|
|
1575
1810
|
isShutdownSwitchover: true,
|
|
@@ -1577,8 +1812,13 @@ describe('plugin-mercury', () => {
|
|
|
1577
1812
|
});
|
|
1578
1813
|
|
|
1579
1814
|
return promise.then(() => {
|
|
1580
|
-
//
|
|
1581
|
-
assert.
|
|
1815
|
+
// Verify backoff call was set during the operation
|
|
1816
|
+
assert.isDefined(backoffCallDuringAttempt, 'backoffCall should be set during attempt');
|
|
1817
|
+
// Verify backoff call is cleared after completion
|
|
1818
|
+
assert.notExists(
|
|
1819
|
+
mercury._shutdownSwitchoverBackoffCall,
|
|
1820
|
+
'backoffCall should be cleared after completion'
|
|
1821
|
+
);
|
|
1582
1822
|
mercury._attemptConnection.restore();
|
|
1583
1823
|
});
|
|
1584
1824
|
});
|
|
@@ -1588,30 +1828,28 @@ describe('plugin-mercury', () => {
|
|
|
1588
1828
|
let abortStub;
|
|
1589
1829
|
|
|
1590
1830
|
beforeEach(() => {
|
|
1591
|
-
mercury.socket = {
|
|
1592
|
-
close: sinon.stub().returns(Promise.resolve()),
|
|
1593
|
-
removeAllListeners: sinon.stub(),
|
|
1594
|
-
};
|
|
1595
1831
|
abortStub = sinon.stub();
|
|
1596
|
-
mercury._shutdownSwitchoverBackoffCall = {
|
|
1597
|
-
abort: abortStub,
|
|
1598
|
-
};
|
|
1832
|
+
mercury._shutdownSwitchoverBackoffCall = {abort: abortStub};
|
|
1599
1833
|
});
|
|
1600
1834
|
|
|
1601
|
-
it('should abort shutdown switchover backoff call on disconnect',
|
|
1602
|
-
|
|
1835
|
+
it('should abort shutdown switchover backoff call on disconnect', () => {
|
|
1836
|
+
// Don't set up socket so disconnect resolves immediately
|
|
1837
|
+
mercury.socket = undefined;
|
|
1603
1838
|
|
|
1604
|
-
|
|
1839
|
+
return mercury.disconnect().then(() => {
|
|
1840
|
+
assert.calledOnce(abortStub);
|
|
1841
|
+
});
|
|
1605
1842
|
});
|
|
1606
1843
|
|
|
1607
|
-
it('should handle disconnect when no switchover is in progress',
|
|
1844
|
+
it('should handle disconnect when no switchover is in progress', () => {
|
|
1608
1845
|
mercury._shutdownSwitchoverBackoffCall = undefined;
|
|
1846
|
+
// Don't set up socket so disconnect resolves immediately
|
|
1847
|
+
mercury.socket = undefined;
|
|
1609
1848
|
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
assert.calledOnce(mercury.socket.close);
|
|
1849
|
+
return mercury.disconnect().then(() => {
|
|
1850
|
+
// If we get here, disconnect completed without error
|
|
1851
|
+
assert.notExists(mercury._shutdownSwitchoverBackoffCall);
|
|
1852
|
+
});
|
|
1615
1853
|
});
|
|
1616
1854
|
});
|
|
1617
1855
|
});
|