@webex/internal-plugin-mercury 3.9.0 → 3.10.0-next.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/dist/mercury.js +341 -134
- package/dist/mercury.js.map +1 -1
- package/package.json +18 -18
- package/src/mercury.js +311 -71
- package/test/unit/spec/mercury.js +684 -1
- package/test/unit/spec/socket.js +6 -6
|
@@ -75,6 +75,8 @@ describe('plugin-mercury', () => {
|
|
|
75
75
|
webex.internal.services = {
|
|
76
76
|
convertUrlToPriorityHostUrl: sinon.stub().returns(Promise.resolve('ws://example-2.com')),
|
|
77
77
|
markFailedUrl: sinon.stub().returns(Promise.resolve()),
|
|
78
|
+
switchActiveClusterIds: sinon.stub(),
|
|
79
|
+
invalidateCache: sinon.stub(),
|
|
78
80
|
};
|
|
79
81
|
webex.internal.metrics.submitClientMetrics = sinon.stub();
|
|
80
82
|
webex.internal.newMetrics.callDiagnosticMetrics.setMercuryConnectedStatus = sinon.stub();
|
|
@@ -162,7 +164,6 @@ describe('plugin-mercury', () => {
|
|
|
162
164
|
},
|
|
163
165
|
},
|
|
164
166
|
};
|
|
165
|
-
|
|
166
167
|
assert.isFalse(mercury.connected, 'Mercury is not connected');
|
|
167
168
|
assert.isTrue(mercury.connecting, 'Mercury is connecting');
|
|
168
169
|
mockWebSocket.open();
|
|
@@ -191,6 +192,67 @@ describe('plugin-mercury', () => {
|
|
|
191
192
|
sinon.restore();
|
|
192
193
|
});
|
|
193
194
|
});
|
|
195
|
+
it('Mercury emit event:ActiveClusterStatusEvent, call services switchActiveClusterIds', () => {
|
|
196
|
+
const promise = mercury.connect();
|
|
197
|
+
const activeClusterEventEnvelope = {
|
|
198
|
+
data: {
|
|
199
|
+
activeClusters: {
|
|
200
|
+
wdm: 'wdm-cluster-id.com',
|
|
201
|
+
},
|
|
202
|
+
},
|
|
203
|
+
};
|
|
204
|
+
mockWebSocket.open();
|
|
205
|
+
|
|
206
|
+
return promise.then(() => {
|
|
207
|
+
mercury._emit('event:ActiveClusterStatusEvent', activeClusterEventEnvelope);
|
|
208
|
+
assert.calledOnceWithExactly(
|
|
209
|
+
webex.internal.services.switchActiveClusterIds,
|
|
210
|
+
activeClusterEventEnvelope.data.activeClusters
|
|
211
|
+
);
|
|
212
|
+
sinon.restore();
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
it('Mercury emit event:ActiveClusterStatusEvent with no data, not call services switchActiveClusterIds', () => {
|
|
216
|
+
webex.internal.feature.updateFeature = sinon.stub();
|
|
217
|
+
const promise = mercury.connect();
|
|
218
|
+
const envelope = {};
|
|
219
|
+
|
|
220
|
+
return promise.then(() => {
|
|
221
|
+
mercury._emit('event:ActiveClusterStatusEvent', envelope);
|
|
222
|
+
assert.notCalled(webex.internal.services.switchActiveClusterIds);
|
|
223
|
+
sinon.restore();
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
it('Mercury emit event:u2c.cache-invalidation, call services invalidateCache', () => {
|
|
227
|
+
const promise = mercury.connect();
|
|
228
|
+
const u2cInvalidateEventEnvelope = {
|
|
229
|
+
data: {
|
|
230
|
+
timestamp: '1759289614',
|
|
231
|
+
},
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
mockWebSocket.open();
|
|
235
|
+
|
|
236
|
+
return promise.then(() => {
|
|
237
|
+
mercury._emit('event:u2c.cache-invalidation', u2cInvalidateEventEnvelope);
|
|
238
|
+
assert.calledOnceWithExactly(
|
|
239
|
+
webex.internal.services.invalidateCache,
|
|
240
|
+
u2cInvalidateEventEnvelope.data.timestamp
|
|
241
|
+
);
|
|
242
|
+
sinon.restore();
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
it('Mercury emit event:u2c.cache-invalidation with no data, not call services switchActiveClusterIds', () => {
|
|
246
|
+
webex.internal.feature.updateFeature = sinon.stub();
|
|
247
|
+
const promise = mercury.connect();
|
|
248
|
+
const envelope = {};
|
|
249
|
+
|
|
250
|
+
return promise.then(() => {
|
|
251
|
+
mercury._emit('event:u2c.cache-invalidation', envelope);
|
|
252
|
+
assert.notCalled(webex.internal.services.invalidateCache);
|
|
253
|
+
sinon.restore();
|
|
254
|
+
});
|
|
255
|
+
});
|
|
194
256
|
|
|
195
257
|
describe('when `maxRetries` is set', () => {
|
|
196
258
|
const check = () => {
|
|
@@ -922,5 +984,626 @@ describe('plugin-mercury', () => {
|
|
|
922
984
|
});
|
|
923
985
|
});
|
|
924
986
|
});
|
|
987
|
+
|
|
988
|
+
describe('shutdown protocol', () => {
|
|
989
|
+
describe('#_handleImminentShutdown()', () => {
|
|
990
|
+
let connectWithBackoffStub;
|
|
991
|
+
|
|
992
|
+
beforeEach(() => {
|
|
993
|
+
mercury.connected = true;
|
|
994
|
+
mercury.socket = {
|
|
995
|
+
url: 'ws://old-socket.com',
|
|
996
|
+
removeAllListeners: sinon.stub(),
|
|
997
|
+
};
|
|
998
|
+
connectWithBackoffStub = sinon.stub(mercury, '_connectWithBackoff');
|
|
999
|
+
connectWithBackoffStub.returns(Promise.resolve());
|
|
1000
|
+
sinon.stub(mercury, '_emit');
|
|
1001
|
+
});
|
|
1002
|
+
|
|
1003
|
+
afterEach(() => {
|
|
1004
|
+
connectWithBackoffStub.restore();
|
|
1005
|
+
mercury._emit.restore();
|
|
1006
|
+
});
|
|
1007
|
+
|
|
1008
|
+
it('should be idempotent - no-op if already in progress', () => {
|
|
1009
|
+
mercury._shutdownSwitchoverInProgress = true;
|
|
1010
|
+
|
|
1011
|
+
mercury._handleImminentShutdown();
|
|
1012
|
+
|
|
1013
|
+
assert.notCalled(connectWithBackoffStub);
|
|
1014
|
+
});
|
|
1015
|
+
|
|
1016
|
+
it('should set switchover flags when called', () => {
|
|
1017
|
+
mercury._handleImminentShutdown();
|
|
1018
|
+
|
|
1019
|
+
assert.isTrue(mercury._shutdownSwitchoverInProgress);
|
|
1020
|
+
assert.isDefined(mercury._shutdownSwitchoverId);
|
|
1021
|
+
});
|
|
1022
|
+
|
|
1023
|
+
it('should call _connectWithBackoff with correct parameters', (done) => {
|
|
1024
|
+
mercury._handleImminentShutdown();
|
|
1025
|
+
|
|
1026
|
+
process.nextTick(() => {
|
|
1027
|
+
assert.calledOnce(connectWithBackoffStub);
|
|
1028
|
+
const callArgs = connectWithBackoffStub.firstCall.args;
|
|
1029
|
+
assert.isUndefined(callArgs[0]); // webSocketUrl
|
|
1030
|
+
assert.isObject(callArgs[1]); // context
|
|
1031
|
+
assert.isTrue(callArgs[1].isShutdownSwitchover);
|
|
1032
|
+
done();
|
|
1033
|
+
});
|
|
1034
|
+
});
|
|
1035
|
+
|
|
1036
|
+
it('should handle exceptions during switchover', () => {
|
|
1037
|
+
connectWithBackoffStub.restore();
|
|
1038
|
+
sinon.stub(mercury, '_connectWithBackoff').throws(new Error('Connection failed'));
|
|
1039
|
+
|
|
1040
|
+
mercury._handleImminentShutdown();
|
|
1041
|
+
|
|
1042
|
+
assert.isFalse(mercury._shutdownSwitchoverInProgress);
|
|
1043
|
+
});
|
|
1044
|
+
});
|
|
1045
|
+
|
|
1046
|
+
describe('#_onmessage() with shutdown message', () => {
|
|
1047
|
+
beforeEach(() => {
|
|
1048
|
+
sinon.stub(mercury, '_handleImminentShutdown');
|
|
1049
|
+
sinon.stub(mercury, '_emit');
|
|
1050
|
+
sinon.stub(mercury, '_setTimeOffset');
|
|
1051
|
+
});
|
|
1052
|
+
|
|
1053
|
+
afterEach(() => {
|
|
1054
|
+
mercury._handleImminentShutdown.restore();
|
|
1055
|
+
mercury._emit.restore();
|
|
1056
|
+
mercury._setTimeOffset.restore();
|
|
1057
|
+
});
|
|
1058
|
+
|
|
1059
|
+
it('should trigger _handleImminentShutdown on shutdown message', () => {
|
|
1060
|
+
const shutdownEvent = {
|
|
1061
|
+
data: {
|
|
1062
|
+
type: 'shutdown',
|
|
1063
|
+
},
|
|
1064
|
+
};
|
|
1065
|
+
|
|
1066
|
+
const result = mercury._onmessage(shutdownEvent);
|
|
1067
|
+
|
|
1068
|
+
assert.calledOnce(mercury._handleImminentShutdown);
|
|
1069
|
+
assert.calledWith(mercury._emit, 'event:mercury_shutdown_imminent', shutdownEvent.data);
|
|
1070
|
+
assert.instanceOf(result, Promise);
|
|
1071
|
+
});
|
|
1072
|
+
|
|
1073
|
+
it('should handle shutdown message without additional data gracefully', () => {
|
|
1074
|
+
const shutdownEvent = {
|
|
1075
|
+
data: {
|
|
1076
|
+
type: 'shutdown',
|
|
1077
|
+
},
|
|
1078
|
+
};
|
|
1079
|
+
|
|
1080
|
+
mercury._onmessage(shutdownEvent);
|
|
1081
|
+
|
|
1082
|
+
assert.calledOnce(mercury._handleImminentShutdown);
|
|
1083
|
+
});
|
|
1084
|
+
|
|
1085
|
+
it('should not trigger shutdown handling for non-shutdown messages', () => {
|
|
1086
|
+
const regularEvent = {
|
|
1087
|
+
data: {
|
|
1088
|
+
type: 'regular',
|
|
1089
|
+
data: {
|
|
1090
|
+
eventType: 'conversation.activity',
|
|
1091
|
+
},
|
|
1092
|
+
},
|
|
1093
|
+
};
|
|
1094
|
+
|
|
1095
|
+
mercury._onmessage(regularEvent);
|
|
1096
|
+
|
|
1097
|
+
assert.notCalled(mercury._handleImminentShutdown);
|
|
1098
|
+
});
|
|
1099
|
+
});
|
|
1100
|
+
|
|
1101
|
+
describe('#_onclose() with code 4001 (shutdown replacement)', () => {
|
|
1102
|
+
let mockSocket, anotherSocket;
|
|
1103
|
+
|
|
1104
|
+
beforeEach(() => {
|
|
1105
|
+
mockSocket = {
|
|
1106
|
+
url: 'ws://active-socket.com',
|
|
1107
|
+
removeAllListeners: sinon.stub(),
|
|
1108
|
+
};
|
|
1109
|
+
anotherSocket = {
|
|
1110
|
+
url: 'ws://old-socket.com',
|
|
1111
|
+
removeAllListeners: sinon.stub(),
|
|
1112
|
+
};
|
|
1113
|
+
mercury.socket = mockSocket;
|
|
1114
|
+
mercury.connected = true;
|
|
1115
|
+
sinon.stub(mercury, '_emit');
|
|
1116
|
+
sinon.stub(mercury, '_reconnect');
|
|
1117
|
+
sinon.stub(mercury, 'unset');
|
|
1118
|
+
});
|
|
1119
|
+
|
|
1120
|
+
afterEach(() => {
|
|
1121
|
+
mercury._emit.restore();
|
|
1122
|
+
mercury._reconnect.restore();
|
|
1123
|
+
mercury.unset.restore();
|
|
1124
|
+
});
|
|
1125
|
+
|
|
1126
|
+
it('should handle active socket close with 4001 - permanent failure', () => {
|
|
1127
|
+
const closeEvent = {
|
|
1128
|
+
code: 4001,
|
|
1129
|
+
reason: 'replaced during shutdown',
|
|
1130
|
+
};
|
|
1131
|
+
|
|
1132
|
+
mercury._onclose(closeEvent, mockSocket);
|
|
1133
|
+
|
|
1134
|
+
assert.calledWith(mercury._emit, 'offline.permanent', closeEvent);
|
|
1135
|
+
assert.notCalled(mercury._reconnect); // No reconnect for 4001 on active socket
|
|
1136
|
+
assert.isFalse(mercury.connected);
|
|
1137
|
+
});
|
|
1138
|
+
|
|
1139
|
+
it('should handle non-active socket close with 4001 - no reconnect needed', () => {
|
|
1140
|
+
const closeEvent = {
|
|
1141
|
+
code: 4001,
|
|
1142
|
+
reason: 'replaced during shutdown',
|
|
1143
|
+
};
|
|
1144
|
+
|
|
1145
|
+
mercury._onclose(closeEvent, anotherSocket);
|
|
1146
|
+
|
|
1147
|
+
assert.calledWith(mercury._emit, 'offline.replaced', closeEvent);
|
|
1148
|
+
assert.notCalled(mercury._reconnect);
|
|
1149
|
+
assert.isTrue(mercury.connected); // Should remain connected
|
|
1150
|
+
assert.notCalled(mercury.unset);
|
|
1151
|
+
});
|
|
1152
|
+
|
|
1153
|
+
it('should distinguish between active and non-active socket closes', () => {
|
|
1154
|
+
const closeEvent = {
|
|
1155
|
+
code: 4001,
|
|
1156
|
+
reason: 'replaced during shutdown',
|
|
1157
|
+
};
|
|
1158
|
+
|
|
1159
|
+
// Test non-active socket
|
|
1160
|
+
mercury._onclose(closeEvent, anotherSocket);
|
|
1161
|
+
assert.calledWith(mercury._emit, 'offline.replaced', closeEvent);
|
|
1162
|
+
|
|
1163
|
+
// Reset the spy call history
|
|
1164
|
+
mercury._emit.resetHistory();
|
|
1165
|
+
|
|
1166
|
+
// Test active socket
|
|
1167
|
+
mercury._onclose(closeEvent, mockSocket);
|
|
1168
|
+
assert.calledWith(mercury._emit, 'offline.permanent', closeEvent);
|
|
1169
|
+
});
|
|
1170
|
+
|
|
1171
|
+
it('should handle missing sourceSocket parameter (treats as non-active)', () => {
|
|
1172
|
+
const closeEvent = {
|
|
1173
|
+
code: 4001,
|
|
1174
|
+
reason: 'replaced during shutdown',
|
|
1175
|
+
};
|
|
1176
|
+
|
|
1177
|
+
mercury._onclose(closeEvent); // No sourceSocket parameter
|
|
1178
|
+
|
|
1179
|
+
// With simplified logic, undefined !== this.socket, so isActiveSocket = false
|
|
1180
|
+
assert.calledWith(mercury._emit, 'offline.replaced', closeEvent);
|
|
1181
|
+
assert.notCalled(mercury._reconnect);
|
|
1182
|
+
});
|
|
1183
|
+
|
|
1184
|
+
it('should clean up event listeners from non-active socket when it closes', () => {
|
|
1185
|
+
const closeEvent = {
|
|
1186
|
+
code: 4001,
|
|
1187
|
+
reason: 'replaced during shutdown',
|
|
1188
|
+
};
|
|
1189
|
+
|
|
1190
|
+
// Close non-active socket (not the active one)
|
|
1191
|
+
mercury._onclose(closeEvent, anotherSocket);
|
|
1192
|
+
|
|
1193
|
+
// Verify listeners were removed from the old socket
|
|
1194
|
+
// The _onclose method checks if sourceSocket !== this.socket (non-active)
|
|
1195
|
+
// and then calls removeAllListeners in the else branch
|
|
1196
|
+
assert.calledOnce(anotherSocket.removeAllListeners);
|
|
1197
|
+
});
|
|
1198
|
+
|
|
1199
|
+
it('should not clean up listeners from active socket listeners until close handler runs', () => {
|
|
1200
|
+
const closeEvent = {
|
|
1201
|
+
code: 4001,
|
|
1202
|
+
reason: 'replaced during shutdown',
|
|
1203
|
+
};
|
|
1204
|
+
|
|
1205
|
+
// Close active socket
|
|
1206
|
+
mercury._onclose(closeEvent, mockSocket);
|
|
1207
|
+
|
|
1208
|
+
// Verify listeners were removed from active socket
|
|
1209
|
+
assert.calledOnce(mockSocket.removeAllListeners);
|
|
1210
|
+
});
|
|
1211
|
+
});
|
|
1212
|
+
|
|
1213
|
+
describe('shutdown switchover with retry logic', () => {
|
|
1214
|
+
let connectWithBackoffStub;
|
|
1215
|
+
|
|
1216
|
+
beforeEach(() => {
|
|
1217
|
+
mercury.connected = true;
|
|
1218
|
+
mercury.socket = {
|
|
1219
|
+
url: 'ws://old-socket.com',
|
|
1220
|
+
removeAllListeners: sinon.stub(),
|
|
1221
|
+
};
|
|
1222
|
+
connectWithBackoffStub = sinon.stub(mercury, '_connectWithBackoff');
|
|
1223
|
+
sinon.stub(mercury, '_emit');
|
|
1224
|
+
});
|
|
1225
|
+
|
|
1226
|
+
afterEach(() => {
|
|
1227
|
+
connectWithBackoffStub.restore();
|
|
1228
|
+
mercury._emit.restore();
|
|
1229
|
+
});
|
|
1230
|
+
|
|
1231
|
+
it('should call _connectWithBackoff with shutdown switchover context', (done) => {
|
|
1232
|
+
connectWithBackoffStub.returns(Promise.resolve());
|
|
1233
|
+
|
|
1234
|
+
mercury._handleImminentShutdown();
|
|
1235
|
+
|
|
1236
|
+
// Give it a tick for the async call to happen
|
|
1237
|
+
process.nextTick(() => {
|
|
1238
|
+
assert.calledOnce(connectWithBackoffStub);
|
|
1239
|
+
const callArgs = connectWithBackoffStub.firstCall.args;
|
|
1240
|
+
|
|
1241
|
+
assert.isUndefined(callArgs[0]); // webSocketUrl is undefined
|
|
1242
|
+
assert.isObject(callArgs[1]); // context object
|
|
1243
|
+
assert.isTrue(callArgs[1].isShutdownSwitchover);
|
|
1244
|
+
assert.isObject(callArgs[1].attemptOptions);
|
|
1245
|
+
assert.isTrue(callArgs[1].attemptOptions.isShutdownSwitchover);
|
|
1246
|
+
done();
|
|
1247
|
+
});
|
|
1248
|
+
});
|
|
1249
|
+
|
|
1250
|
+
it('should set _shutdownSwitchoverInProgress flag during switchover', () => {
|
|
1251
|
+
connectWithBackoffStub.returns(new Promise(() => {})); // Never resolves
|
|
1252
|
+
|
|
1253
|
+
mercury._handleImminentShutdown();
|
|
1254
|
+
|
|
1255
|
+
assert.isTrue(mercury._shutdownSwitchoverInProgress);
|
|
1256
|
+
});
|
|
1257
|
+
|
|
1258
|
+
it('should emit success event when switchover completes', async () => {
|
|
1259
|
+
// We need to actually call the onSuccess callback to trigger the event
|
|
1260
|
+
connectWithBackoffStub.callsFake((url, context) => {
|
|
1261
|
+
// Simulate successful connection by calling onSuccess
|
|
1262
|
+
if (context && context.attemptOptions && context.attemptOptions.onSuccess) {
|
|
1263
|
+
const mockSocket = {url: 'ws://new-socket.com'};
|
|
1264
|
+
context.attemptOptions.onSuccess(mockSocket, 'ws://new-socket.com');
|
|
1265
|
+
}
|
|
1266
|
+
return Promise.resolve();
|
|
1267
|
+
});
|
|
1268
|
+
|
|
1269
|
+
mercury._handleImminentShutdown();
|
|
1270
|
+
|
|
1271
|
+
// Wait for async operations
|
|
1272
|
+
await promiseTick(50);
|
|
1273
|
+
|
|
1274
|
+
const emitCalls = mercury._emit.getCalls();
|
|
1275
|
+
const hasCompleteEvent = emitCalls.some(
|
|
1276
|
+
(call) => call.args[0] === 'event:mercury_shutdown_switchover_complete'
|
|
1277
|
+
);
|
|
1278
|
+
|
|
1279
|
+
assert.isTrue(hasCompleteEvent, 'Should emit switchover complete event');
|
|
1280
|
+
});
|
|
1281
|
+
|
|
1282
|
+
it('should emit failure event when switchover exhausts retries', async () => {
|
|
1283
|
+
const testError = new Error('Connection failed');
|
|
1284
|
+
|
|
1285
|
+
connectWithBackoffStub.returns(Promise.reject(testError));
|
|
1286
|
+
|
|
1287
|
+
mercury._handleImminentShutdown();
|
|
1288
|
+
await promiseTick(50);
|
|
1289
|
+
|
|
1290
|
+
// Check if failure event was emitted
|
|
1291
|
+
const emitCalls = mercury._emit.getCalls();
|
|
1292
|
+
const hasFailureEvent = emitCalls.some(
|
|
1293
|
+
(call) =>
|
|
1294
|
+
call.args[0] === 'event:mercury_shutdown_switchover_failed' &&
|
|
1295
|
+
call.args[1] &&
|
|
1296
|
+
call.args[1].reason === testError
|
|
1297
|
+
);
|
|
1298
|
+
|
|
1299
|
+
assert.isTrue(hasFailureEvent, 'Should emit switchover failed event');
|
|
1300
|
+
});
|
|
1301
|
+
|
|
1302
|
+
it('should allow old socket to be closed by server after switchover failure', async () => {
|
|
1303
|
+
connectWithBackoffStub.returns(Promise.reject(new Error('Failed')));
|
|
1304
|
+
|
|
1305
|
+
mercury._handleImminentShutdown();
|
|
1306
|
+
await promiseTick(50);
|
|
1307
|
+
|
|
1308
|
+
// Old socket should not be closed immediately - server will close it
|
|
1309
|
+
assert.equal(mercury.socket.removeAllListeners.callCount, 0);
|
|
1310
|
+
});
|
|
1311
|
+
});
|
|
1312
|
+
|
|
1313
|
+
describe('#_prepareAndOpenSocket()', () => {
|
|
1314
|
+
let mockSocket, prepareUrlStub, getUserTokenStub;
|
|
1315
|
+
|
|
1316
|
+
beforeEach(() => {
|
|
1317
|
+
mockSocket = {
|
|
1318
|
+
open: sinon.stub().returns(Promise.resolve()),
|
|
1319
|
+
};
|
|
1320
|
+
prepareUrlStub = sinon
|
|
1321
|
+
.stub(mercury, '_prepareUrl')
|
|
1322
|
+
.returns(Promise.resolve('ws://example.com'));
|
|
1323
|
+
getUserTokenStub = webex.credentials.getUserToken;
|
|
1324
|
+
getUserTokenStub.returns(
|
|
1325
|
+
Promise.resolve({
|
|
1326
|
+
toString: () => 'mock-token',
|
|
1327
|
+
})
|
|
1328
|
+
);
|
|
1329
|
+
});
|
|
1330
|
+
|
|
1331
|
+
afterEach(() => {
|
|
1332
|
+
prepareUrlStub.restore();
|
|
1333
|
+
});
|
|
1334
|
+
|
|
1335
|
+
it('should prepare URL and get user token', async () => {
|
|
1336
|
+
await mercury._prepareAndOpenSocket(mockSocket, 'ws://test.com', false);
|
|
1337
|
+
|
|
1338
|
+
assert.calledOnce(prepareUrlStub);
|
|
1339
|
+
assert.calledWith(prepareUrlStub, 'ws://test.com');
|
|
1340
|
+
assert.calledOnce(getUserTokenStub);
|
|
1341
|
+
});
|
|
1342
|
+
|
|
1343
|
+
it('should open socket with correct options for normal connection', async () => {
|
|
1344
|
+
await mercury._prepareAndOpenSocket(mockSocket, undefined, false);
|
|
1345
|
+
|
|
1346
|
+
assert.calledOnce(mockSocket.open);
|
|
1347
|
+
const callArgs = mockSocket.open.firstCall.args;
|
|
1348
|
+
|
|
1349
|
+
assert.equal(callArgs[0], 'ws://example.com');
|
|
1350
|
+
assert.isObject(callArgs[1]);
|
|
1351
|
+
assert.equal(callArgs[1].token, 'mock-token');
|
|
1352
|
+
assert.isDefined(callArgs[1].forceCloseDelay);
|
|
1353
|
+
assert.isDefined(callArgs[1].pingInterval);
|
|
1354
|
+
assert.isDefined(callArgs[1].pongTimeout);
|
|
1355
|
+
});
|
|
1356
|
+
|
|
1357
|
+
it('should log with correct prefix for normal connection', async () => {
|
|
1358
|
+
await mercury._prepareAndOpenSocket(mockSocket, undefined, false);
|
|
1359
|
+
|
|
1360
|
+
// The method should complete successfully - we're testing it runs without error
|
|
1361
|
+
// Actual log message verification is complex due to existing stubs in parent scope
|
|
1362
|
+
assert.calledOnce(mockSocket.open);
|
|
1363
|
+
});
|
|
1364
|
+
|
|
1365
|
+
it('should log with shutdown prefix for shutdown connection', async () => {
|
|
1366
|
+
await mercury._prepareAndOpenSocket(mockSocket, undefined, true);
|
|
1367
|
+
|
|
1368
|
+
// The method should complete successfully with shutdown flag
|
|
1369
|
+
assert.calledOnce(mockSocket.open);
|
|
1370
|
+
});
|
|
1371
|
+
|
|
1372
|
+
it('should merge custom mercury options when provided', async () => {
|
|
1373
|
+
webex.config.defaultMercuryOptions = {
|
|
1374
|
+
customOption: 'test-value',
|
|
1375
|
+
pingInterval: 99999,
|
|
1376
|
+
};
|
|
1377
|
+
|
|
1378
|
+
await mercury._prepareAndOpenSocket(mockSocket, undefined, false);
|
|
1379
|
+
|
|
1380
|
+
const callArgs = mockSocket.open.firstCall.args;
|
|
1381
|
+
|
|
1382
|
+
assert.equal(callArgs[1].customOption, 'test-value');
|
|
1383
|
+
assert.equal(callArgs[1].pingInterval, 99999); // Custom value overrides default
|
|
1384
|
+
});
|
|
1385
|
+
|
|
1386
|
+
it('should return the webSocketUrl after opening', async () => {
|
|
1387
|
+
const result = await mercury._prepareAndOpenSocket(mockSocket, undefined, false);
|
|
1388
|
+
|
|
1389
|
+
assert.equal(result, 'ws://example.com');
|
|
1390
|
+
});
|
|
1391
|
+
|
|
1392
|
+
it('should handle errors during socket open', async () => {
|
|
1393
|
+
mockSocket.open.returns(Promise.reject(new Error('Open failed')));
|
|
1394
|
+
|
|
1395
|
+
try {
|
|
1396
|
+
await mercury._prepareAndOpenSocket(mockSocket, undefined, false);
|
|
1397
|
+
assert.fail('Should have thrown an error');
|
|
1398
|
+
} catch (err) {
|
|
1399
|
+
assert.equal(err.message, 'Open failed');
|
|
1400
|
+
}
|
|
1401
|
+
});
|
|
1402
|
+
});
|
|
1403
|
+
|
|
1404
|
+
describe('#_attemptConnection() with shutdown switchover', () => {
|
|
1405
|
+
let mockSocket, prepareAndOpenSocketStub, callback;
|
|
1406
|
+
|
|
1407
|
+
beforeEach(() => {
|
|
1408
|
+
mockSocket = {
|
|
1409
|
+
url: 'ws://test.com',
|
|
1410
|
+
};
|
|
1411
|
+
prepareAndOpenSocketStub = sinon
|
|
1412
|
+
.stub(mercury, '_prepareAndOpenSocket')
|
|
1413
|
+
.returns(Promise.resolve('ws://new-socket.com'));
|
|
1414
|
+
callback = sinon.stub();
|
|
1415
|
+
mercury._shutdownSwitchoverBackoffCall = {}; // Mock backoff call
|
|
1416
|
+
mercury.socket = mockSocket;
|
|
1417
|
+
mercury.connected = true;
|
|
1418
|
+
sinon.stub(mercury, '_emit');
|
|
1419
|
+
sinon.stub(mercury, '_attachSocketEventListeners');
|
|
1420
|
+
});
|
|
1421
|
+
|
|
1422
|
+
afterEach(() => {
|
|
1423
|
+
prepareAndOpenSocketStub.restore();
|
|
1424
|
+
mercury._emit.restore();
|
|
1425
|
+
mercury._attachSocketEventListeners.restore();
|
|
1426
|
+
});
|
|
1427
|
+
|
|
1428
|
+
it('should not set socket reference before opening for shutdown switchover', async () => {
|
|
1429
|
+
const originalSocket = mercury.socket;
|
|
1430
|
+
|
|
1431
|
+
await mercury._attemptConnection('ws://test.com', callback, {
|
|
1432
|
+
isShutdownSwitchover: true,
|
|
1433
|
+
onSuccess: (newSocket, url) => {
|
|
1434
|
+
// During onSuccess, verify original socket is still set
|
|
1435
|
+
// (socket swap happens inside onSuccess callback in _handleImminentShutdown)
|
|
1436
|
+
assert.equal(mercury.socket, originalSocket);
|
|
1437
|
+
},
|
|
1438
|
+
});
|
|
1439
|
+
|
|
1440
|
+
// After onSuccess, socket should still be original since we only swap in _handleImminentShutdown
|
|
1441
|
+
assert.equal(mercury.socket, originalSocket);
|
|
1442
|
+
});
|
|
1443
|
+
|
|
1444
|
+
it('should call onSuccess callback with new socket and URL for shutdown', async () => {
|
|
1445
|
+
const onSuccessStub = sinon.stub();
|
|
1446
|
+
|
|
1447
|
+
await mercury._attemptConnection('ws://test.com', callback, {
|
|
1448
|
+
isShutdownSwitchover: true,
|
|
1449
|
+
onSuccess: onSuccessStub,
|
|
1450
|
+
});
|
|
1451
|
+
|
|
1452
|
+
assert.calledOnce(onSuccessStub);
|
|
1453
|
+
assert.equal(onSuccessStub.firstCall.args[1], 'ws://new-socket.com');
|
|
1454
|
+
});
|
|
1455
|
+
|
|
1456
|
+
it('should emit shutdown switchover complete event', async () => {
|
|
1457
|
+
const oldSocket = mercury.socket;
|
|
1458
|
+
|
|
1459
|
+
await mercury._attemptConnection('ws://test.com', callback, {
|
|
1460
|
+
isShutdownSwitchover: true,
|
|
1461
|
+
onSuccess: (newSocket, url) => {
|
|
1462
|
+
// Simulate the onSuccess callback behavior
|
|
1463
|
+
mercury.socket = newSocket;
|
|
1464
|
+
mercury.connected = true;
|
|
1465
|
+
mercury._emit('event:mercury_shutdown_switchover_complete', {url});
|
|
1466
|
+
},
|
|
1467
|
+
});
|
|
1468
|
+
|
|
1469
|
+
assert.calledWith(
|
|
1470
|
+
mercury._emit,
|
|
1471
|
+
'event:mercury_shutdown_switchover_complete',
|
|
1472
|
+
sinon.match.has('url', 'ws://new-socket.com')
|
|
1473
|
+
);
|
|
1474
|
+
});
|
|
1475
|
+
|
|
1476
|
+
it('should use simpler error handling for shutdown switchover failures', async () => {
|
|
1477
|
+
prepareAndOpenSocketStub.returns(Promise.reject(new Error('Connection failed')));
|
|
1478
|
+
|
|
1479
|
+
try {
|
|
1480
|
+
await mercury._attemptConnection('ws://test.com', callback, {
|
|
1481
|
+
isShutdownSwitchover: true,
|
|
1482
|
+
});
|
|
1483
|
+
} catch (err) {
|
|
1484
|
+
// Error should be caught and passed to callback
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
// Should call callback with error for retry
|
|
1488
|
+
assert.calledOnce(callback);
|
|
1489
|
+
assert.instanceOf(callback.firstCall.args[0], Error);
|
|
1490
|
+
});
|
|
1491
|
+
|
|
1492
|
+
it('should check _shutdownSwitchoverBackoffCall for shutdown connections', () => {
|
|
1493
|
+
mercury._shutdownSwitchoverBackoffCall = undefined;
|
|
1494
|
+
|
|
1495
|
+
const result = mercury._attemptConnection('ws://test.com', callback, {
|
|
1496
|
+
isShutdownSwitchover: true,
|
|
1497
|
+
});
|
|
1498
|
+
|
|
1499
|
+
return result.catch((err) => {
|
|
1500
|
+
assert.instanceOf(err, Error);
|
|
1501
|
+
assert.match(err.message, /switchover backoff call/);
|
|
1502
|
+
});
|
|
1503
|
+
});
|
|
1504
|
+
});
|
|
1505
|
+
|
|
1506
|
+
describe('#_connectWithBackoff() with shutdown switchover', () => {
|
|
1507
|
+
// Note: These tests verify the parameterization logic without running real backoff timers
|
|
1508
|
+
// to avoid test hangs. The backoff mechanism itself is tested in other test suites.
|
|
1509
|
+
|
|
1510
|
+
it('should use shutdown-specific parameters when called', () => {
|
|
1511
|
+
// Stub _connectWithBackoff to prevent real execution
|
|
1512
|
+
const connectWithBackoffStub = sinon
|
|
1513
|
+
.stub(mercury, '_connectWithBackoff')
|
|
1514
|
+
.returns(Promise.resolve());
|
|
1515
|
+
|
|
1516
|
+
mercury._handleImminentShutdown();
|
|
1517
|
+
|
|
1518
|
+
// Verify it was called with shutdown context
|
|
1519
|
+
assert.calledOnce(connectWithBackoffStub);
|
|
1520
|
+
const callArgs = connectWithBackoffStub.firstCall.args;
|
|
1521
|
+
assert.isObject(callArgs[1]); // context
|
|
1522
|
+
assert.isTrue(callArgs[1].isShutdownSwitchover);
|
|
1523
|
+
|
|
1524
|
+
connectWithBackoffStub.restore();
|
|
1525
|
+
});
|
|
1526
|
+
|
|
1527
|
+
it('should pass shutdown switchover options to _attemptConnection', () => {
|
|
1528
|
+
// Stub _attemptConnection to verify it receives correct options
|
|
1529
|
+
const attemptStub = sinon.stub(mercury, '_attemptConnection');
|
|
1530
|
+
attemptStub.callsFake((url, callback) => {
|
|
1531
|
+
// Immediately succeed
|
|
1532
|
+
callback();
|
|
1533
|
+
});
|
|
1534
|
+
|
|
1535
|
+
// Call _connectWithBackoff with shutdown context
|
|
1536
|
+
const context = {
|
|
1537
|
+
isShutdownSwitchover: true,
|
|
1538
|
+
attemptOptions: {
|
|
1539
|
+
isShutdownSwitchover: true,
|
|
1540
|
+
onSuccess: () => {},
|
|
1541
|
+
},
|
|
1542
|
+
};
|
|
1543
|
+
|
|
1544
|
+
// Start the backoff
|
|
1545
|
+
const promise = mercury._connectWithBackoff(undefined, context);
|
|
1546
|
+
|
|
1547
|
+
// Check that _attemptConnection was called with shutdown options
|
|
1548
|
+
return promise.then(() => {
|
|
1549
|
+
assert.calledOnce(attemptStub);
|
|
1550
|
+
const callArgs = attemptStub.firstCall.args;
|
|
1551
|
+
assert.isObject(callArgs[2]); // options parameter
|
|
1552
|
+
assert.isTrue(callArgs[2].isShutdownSwitchover);
|
|
1553
|
+
|
|
1554
|
+
attemptStub.restore();
|
|
1555
|
+
});
|
|
1556
|
+
});
|
|
1557
|
+
|
|
1558
|
+
it('should set and clear state flags appropriately', () => {
|
|
1559
|
+
// Stub to prevent actual connection
|
|
1560
|
+
sinon.stub(mercury, '_attemptConnection').callsFake((url, callback) => callback());
|
|
1561
|
+
|
|
1562
|
+
mercury._shutdownSwitchoverInProgress = true;
|
|
1563
|
+
|
|
1564
|
+
const promise = mercury._connectWithBackoff(undefined, {
|
|
1565
|
+
isShutdownSwitchover: true,
|
|
1566
|
+
attemptOptions: {isShutdownSwitchover: true, onSuccess: () => {}},
|
|
1567
|
+
});
|
|
1568
|
+
|
|
1569
|
+
return promise.then(() => {
|
|
1570
|
+
// Should be cleared after completion
|
|
1571
|
+
assert.isFalse(mercury._shutdownSwitchoverInProgress);
|
|
1572
|
+
mercury._attemptConnection.restore();
|
|
1573
|
+
});
|
|
1574
|
+
});
|
|
1575
|
+
});
|
|
1576
|
+
|
|
1577
|
+
describe('#disconnect() with shutdown switchover in progress', () => {
|
|
1578
|
+
let abortStub;
|
|
1579
|
+
|
|
1580
|
+
beforeEach(() => {
|
|
1581
|
+
mercury.socket = {
|
|
1582
|
+
close: sinon.stub().returns(Promise.resolve()),
|
|
1583
|
+
removeAllListeners: sinon.stub(),
|
|
1584
|
+
};
|
|
1585
|
+
abortStub = sinon.stub();
|
|
1586
|
+
mercury._shutdownSwitchoverBackoffCall = {
|
|
1587
|
+
abort: abortStub,
|
|
1588
|
+
};
|
|
1589
|
+
});
|
|
1590
|
+
|
|
1591
|
+
it('should abort shutdown switchover backoff call on disconnect', async () => {
|
|
1592
|
+
await mercury.disconnect();
|
|
1593
|
+
|
|
1594
|
+
assert.calledOnce(abortStub);
|
|
1595
|
+
});
|
|
1596
|
+
|
|
1597
|
+
it('should handle disconnect when no switchover is in progress', async () => {
|
|
1598
|
+
mercury._shutdownSwitchoverBackoffCall = undefined;
|
|
1599
|
+
|
|
1600
|
+
// Should not throw
|
|
1601
|
+
await mercury.disconnect();
|
|
1602
|
+
|
|
1603
|
+
// Should still close the socket
|
|
1604
|
+
assert.calledOnce(mercury.socket.close);
|
|
1605
|
+
});
|
|
1606
|
+
});
|
|
1607
|
+
});
|
|
925
1608
|
});
|
|
926
1609
|
});
|