@webex/plugin-meetings 3.3.0 → 3.3.1-next.10

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.
Files changed (37) hide show
  1. package/dist/breakouts/breakout.js +1 -1
  2. package/dist/breakouts/index.js +1 -1
  3. package/dist/constants.js +4 -2
  4. package/dist/constants.js.map +1 -1
  5. package/dist/interpretation/index.js +1 -1
  6. package/dist/interpretation/siLanguage.js +1 -1
  7. package/dist/mediaQualityMetrics/config.js +20 -16
  8. package/dist/mediaQualityMetrics/config.js.map +1 -1
  9. package/dist/meeting/index.js +30 -13
  10. package/dist/meeting/index.js.map +1 -1
  11. package/dist/meetings/index.js +6 -1
  12. package/dist/meetings/index.js.map +1 -1
  13. package/dist/reachability/index.js +82 -9
  14. package/dist/reachability/index.js.map +1 -1
  15. package/dist/statsAnalyzer/index.js +77 -27
  16. package/dist/statsAnalyzer/index.js.map +1 -1
  17. package/dist/statsAnalyzer/mqaUtil.js +46 -7
  18. package/dist/statsAnalyzer/mqaUtil.js.map +1 -1
  19. package/dist/types/constants.d.ts +2 -1
  20. package/dist/types/mediaQualityMetrics/config.d.ts +14 -2
  21. package/dist/types/meeting/index.d.ts +8 -0
  22. package/dist/types/reachability/index.d.ts +11 -0
  23. package/dist/types/statsAnalyzer/index.d.ts +14 -6
  24. package/dist/types/statsAnalyzer/mqaUtil.d.ts +17 -4
  25. package/dist/webinar/index.js +1 -1
  26. package/package.json +22 -22
  27. package/src/constants.ts +2 -1
  28. package/src/mediaQualityMetrics/config.ts +22 -10
  29. package/src/meeting/index.ts +29 -14
  30. package/src/meetings/index.ts +7 -2
  31. package/src/reachability/index.ts +57 -0
  32. package/src/statsAnalyzer/index.ts +82 -22
  33. package/src/statsAnalyzer/mqaUtil.ts +68 -4
  34. package/test/unit/spec/meeting/index.js +28 -8
  35. package/test/unit/spec/meetings/index.js +38 -15
  36. package/test/unit/spec/reachability/index.ts +266 -0
  37. package/test/unit/spec/stats-analyzer/index.js +630 -314
@@ -2,7 +2,7 @@
2
2
 
3
3
  import {mean, max} from 'lodash';
4
4
 
5
- import {STATS} from '../constants';
5
+ import {MQA_INTERVAL, STATS} from '../constants';
6
6
 
7
7
  /**
8
8
  * Get the totals of a certain value from a certain media type.
@@ -28,6 +28,7 @@ export const getAudioReceiverMqa = ({
28
28
  statsResults,
29
29
  lastMqaDataSent,
30
30
  baseMediaType,
31
+ isMultistream,
31
32
  }) => {
32
33
  const sendrecvType = STATS.RECEIVE_DIRECTION;
33
34
 
@@ -52,6 +53,7 @@ export const getAudioReceiverMqa = ({
52
53
  statsResults[Object.keys(statsResults).find((mediaType) => mediaType.includes(baseMediaType))]
53
54
  ?.direction || 'inactive';
54
55
  audioReceiver.common.common.isMain = !baseMediaType.includes('-share');
56
+ audioReceiver.common.common.multistreamEnabled = isMultistream;
55
57
  audioReceiver.common.transportType = statsResults.connectionType.local.transport;
56
58
 
57
59
  // add rtpPacket info inside common as also for call analyzer
@@ -127,7 +129,13 @@ export const getAudioReceiverStreamMqa = ({
127
129
  ((statsResults[mediaType][sendrecvType].totalBytesReceived - lastBytesReceived) * 8) / 60 || 0;
128
130
  };
129
131
 
130
- export const getAudioSenderMqa = ({audioSender, statsResults, lastMqaDataSent, baseMediaType}) => {
132
+ export const getAudioSenderMqa = ({
133
+ audioSender,
134
+ statsResults,
135
+ lastMqaDataSent,
136
+ baseMediaType,
137
+ isMultistream,
138
+ }) => {
131
139
  const sendrecvType = STATS.SEND_DIRECTION;
132
140
 
133
141
  const getLastTotalValue = (value: string) =>
@@ -152,6 +160,7 @@ export const getAudioSenderMqa = ({audioSender, statsResults, lastMqaDataSent, b
152
160
  statsResults[Object.keys(statsResults).find((mediaType) => mediaType.includes(baseMediaType))]
153
161
  ?.direction || 'inactive';
154
162
  audioSender.common.common.isMain = !baseMediaType.includes('-share');
163
+ audioSender.common.common.multistreamEnabled = isMultistream;
155
164
  audioSender.common.transportType = statsResults.connectionType.local.transport;
156
165
 
157
166
  audioSender.common.maxRemoteJitter = max(meanRemoteJitter) * 1000 || 0;
@@ -166,8 +175,8 @@ export const getAudioSenderMqa = ({audioSender, statsResults, lastMqaDataSent, b
166
175
  baseMediaType,
167
176
  'availableOutgoingBitrate'
168
177
  );
169
- // Calculate based on how much packets lost of received compated to how to the client sent
170
178
 
179
+ // Calculate based on how much packets lost of received compated to how to the client sent
171
180
  const totalPacketsLostForaMin = totalPacketsLostOnReceiver - lastPacketsLostTotal;
172
181
  audioSender.common.remoteLossRate =
173
182
  totalPacketsSent - lastPacketsSent > 0
@@ -225,6 +234,7 @@ export const getVideoReceiverMqa = ({
225
234
  statsResults,
226
235
  lastMqaDataSent,
227
236
  baseMediaType,
237
+ isMultistream,
228
238
  }) => {
229
239
  const sendrecvType = STATS.RECEIVE_DIRECTION;
230
240
 
@@ -237,10 +247,16 @@ export const getVideoReceiverMqa = ({
237
247
  const lastPacketsLost = getLastTotalValue('totalPacketsLost');
238
248
  const lastBytesReceived = getLastTotalValue('totalBytesReceived');
239
249
 
250
+ const lastRtxPacketsReceived = getLastTotalValue('totalRtxPacketsReceived');
251
+ const lastRtxBytesReceived = getLastTotalValue('totalRtxBytesReceived');
252
+
240
253
  const packetsLost = getTotalValue('totalPacketsLost');
241
254
  const totalPacketsReceived = getTotalValue('totalPacketsReceived');
242
255
  const totalBytesReceived = getTotalValue('totalBytesReceived');
243
256
 
257
+ const totalRtxPacketsReceived = getTotalValue('totalRtxPacketsReceived');
258
+ const totalRtxBytesReceived = getTotalValue('totalRtxBytesReceived');
259
+
244
260
  const meanRemoteJitter = Object.keys(statsResults)
245
261
  .filter((mt) => mt.includes(baseMediaType))
246
262
  .reduce((acc, mt) => acc.concat(statsResults[mt][sendrecvType].meanRemoteJitter), []);
@@ -248,6 +264,7 @@ export const getVideoReceiverMqa = ({
248
264
  videoReceiver.common.common.direction =
249
265
  statsResults[Object.keys(statsResults).find((mediaType) => mediaType.includes(baseMediaType))]
250
266
  ?.direction || 'inactive';
267
+ videoReceiver.common.common.multistreamEnabled = isMultistream;
251
268
  videoReceiver.common.common.isMain = !baseMediaType.includes('-share');
252
269
  videoReceiver.common.transportType = statsResults.connectionType.local.transport;
253
270
 
@@ -266,10 +283,15 @@ export const getVideoReceiverMqa = ({
266
283
 
267
284
  // Calculate the outgoing bitrate
268
285
  const totalBytesReceivedInaMin = totalBytesReceived - lastBytesReceived;
286
+ const totalRtxBytesReceivedInaMin = totalRtxBytesReceived - lastRtxBytesReceived;
269
287
 
270
288
  videoReceiver.common.rtpBitrate = totalBytesReceivedInaMin
271
289
  ? (totalBytesReceivedInaMin * 8) / 60
272
290
  : 0;
291
+ videoReceiver.common.rtxPackets = totalRtxPacketsReceived - lastRtxPacketsReceived;
292
+ videoReceiver.common.rtxBitrate = totalRtxBytesReceivedInaMin
293
+ ? (totalRtxBytesReceivedInaMin * 8) / 60
294
+ : 0;
273
295
  };
274
296
 
275
297
  export const getVideoReceiverStreamMqa = ({
@@ -336,9 +358,23 @@ export const getVideoReceiverStreamMqa = ({
336
358
  statsResults[mediaType][sendrecvType].keyFramesDecoded - lastKeyFramesDecoded || 0;
337
359
  videoReceiverStream.requestedKeyFrames =
338
360
  statsResults[mediaType][sendrecvType].totalPliCount - lastPliCount || 0;
361
+
362
+ videoReceiverStream.isActiveSpeaker =
363
+ statsResults[mediaType][sendrecvType].isActiveSpeaker ||
364
+ ((statsResults[mediaType][sendrecvType].lastActiveSpeakerTimestamp ?? 0) > 0 &&
365
+ performance.now() +
366
+ performance.timeOrigin -
367
+ (statsResults[mediaType][sendrecvType].lastActiveSpeakerTimestamp ?? 0) <
368
+ MQA_INTERVAL);
339
369
  };
340
370
 
341
- export const getVideoSenderMqa = ({videoSender, statsResults, lastMqaDataSent, baseMediaType}) => {
371
+ export const getVideoSenderMqa = ({
372
+ videoSender,
373
+ statsResults,
374
+ lastMqaDataSent,
375
+ baseMediaType,
376
+ isMultistream,
377
+ }) => {
342
378
  const sendrecvType = STATS.SEND_DIRECTION;
343
379
 
344
380
  const getLastTotalValue = (value: string) =>
@@ -349,15 +385,20 @@ export const getVideoSenderMqa = ({videoSender, statsResults, lastMqaDataSent, b
349
385
  const lastPacketsSent = getLastTotalValue('totalPacketsSent');
350
386
  const lastBytesSent = getLastTotalValue('totalBytesSent');
351
387
  const lastPacketsLostTotal = getLastTotalValue('totalPacketsLostOnReceiver');
388
+ const lastRtxPacketsSent = getLastTotalValue('totalRtxPacketsSent');
389
+ const lastRtxBytesSent = getLastTotalValue('totalRtxBytesSent');
352
390
 
353
391
  const totalPacketsLostOnReceiver = getTotalValue('totalPacketsLostOnReceiver');
354
392
  const totalPacketsSent = getTotalValue('totalPacketsSent');
355
393
  const totalBytesSent = getTotalValue('totalBytesSent');
356
394
  const availableOutgoingBitrate = getTotalValue('availableOutgoingBitrate');
395
+ const totalRtxPacketsSent = getTotalValue('totalRtxPacketsSent');
396
+ const totalRtxBytesSent = getTotalValue('totalRtxBytesSent');
357
397
 
358
398
  videoSender.common.common.direction =
359
399
  statsResults[Object.keys(statsResults).find((mediaType) => mediaType.includes(baseMediaType))]
360
400
  ?.direction || 'inactive';
401
+ videoSender.common.common.multistreamEnabled = isMultistream;
361
402
  videoSender.common.common.isMain = !baseMediaType.includes('-share');
362
403
  videoSender.common.transportType = statsResults.connectionType.local.transport;
363
404
 
@@ -389,8 +430,11 @@ export const getVideoSenderMqa = ({videoSender, statsResults, lastMqaDataSent, b
389
430
 
390
431
  // Calculate the outgoing bitrate
391
432
  const totalBytesSentInaMin = totalBytesSent - lastBytesSent;
433
+ const totalRtxBytesSentInaMin = totalRtxBytesSent - lastRtxBytesSent;
392
434
 
393
435
  videoSender.common.rtpBitrate = totalBytesSentInaMin ? (totalBytesSentInaMin * 8) / 60 : 0;
436
+ videoSender.common.rtxPackets = totalRtxPacketsSent - lastRtxPacketsSent;
437
+ videoSender.common.rtxBitrate = totalRtxBytesSentInaMin ? (totalRtxBytesSentInaMin * 8) / 60 : 0;
394
438
  };
395
439
 
396
440
  export const getVideoSenderStreamMqa = ({
@@ -443,3 +487,23 @@ export const getVideoSenderStreamMqa = ({
443
487
  videoSenderStream.requestedFrameSize =
444
488
  statsResults[mediaType][sendrecvType].requestedFrameSize || 0;
445
489
  };
490
+
491
+ /**
492
+ * Checks if stream stats should be updated based on request status and elapsed time.
493
+ *
494
+ * @param {Object} statsResults - Stats results object.
495
+ * @param {string} mediaType - Media type (e.g., 'audio', 'video').
496
+ * @param {string} direction - Stats direction (e.g., 'send', 'receive').
497
+ * @returns {boolean} Whether stats should be updated.
498
+ */
499
+ export const isStreamRequested = (
500
+ statsResults: any,
501
+ mediaType: string,
502
+ direction: string
503
+ ): boolean => {
504
+ const now = performance.timeOrigin + performance.now();
505
+ const lastUpdateTimestamp = statsResults[mediaType][direction]?.lastRequestedUpdateTimestamp;
506
+ const isRequested = statsResults[mediaType][direction]?.isRequested;
507
+
508
+ return isRequested || (lastUpdateTimestamp && now - lastUpdateTimestamp < MQA_INTERVAL);
509
+ };
@@ -1302,6 +1302,31 @@ describe('plugin-meetings', () => {
1302
1302
  );
1303
1303
  });
1304
1304
  });
1305
+
1306
+ describe('#handleLLMOnline', () => {
1307
+ beforeEach(() => {
1308
+ webex.internal.llm.off = sinon.stub();
1309
+ });
1310
+
1311
+ it('turns off llm online, emits transcription connected events', () => {
1312
+ meeting.handleLLMOnline();
1313
+ assert.calledOnceWithExactly(
1314
+ webex.internal.llm.off,
1315
+ 'online',
1316
+ meeting.handleLLMOnline
1317
+ );
1318
+ assert.calledWith(
1319
+ TriggerProxy.trigger,
1320
+ sinon.match.instanceOf(Meeting),
1321
+ {
1322
+ file: 'meeting/index',
1323
+ function: 'handleLLMOnline',
1324
+ },
1325
+ EVENT_TRIGGERS.MEETING_TRANSCRIPTION_CONNECTED
1326
+ );
1327
+ });
1328
+ });
1329
+
1305
1330
  describe('#join', () => {
1306
1331
  let sandbox = null;
1307
1332
  let setCorrelationIdSpy;
@@ -1351,15 +1376,10 @@ describe('plugin-meetings', () => {
1351
1376
  assert.calledOnce(MeetingUtil.joinMeeting);
1352
1377
  assert.calledOnce(meeting.setLocus);
1353
1378
  assert.equal(result, joinMeetingResult);
1354
-
1355
1379
  assert.calledWith(
1356
- TriggerProxy.trigger,
1357
- sinon.match.instanceOf(Meeting),
1358
- {
1359
- file: 'meeting/index',
1360
- function: 'join',
1361
- },
1362
- EVENT_TRIGGERS.MEETING_TRANSCRIPTION_CONNECTED
1380
+ webex.internal.llm.on,
1381
+ 'online',
1382
+ meeting.handleLLMOnline
1363
1383
  );
1364
1384
  });
1365
1385
 
@@ -18,6 +18,7 @@ import TriggerProxy from '@webex/plugin-meetings/src/common/events/trigger-proxy
18
18
  import LoggerProxy from '@webex/plugin-meetings/src/common/logs/logger-proxy';
19
19
  import LoggerConfig from '@webex/plugin-meetings/src/common/logs/logger-config';
20
20
  import Meeting, {CallStateForMetrics} from '@webex/plugin-meetings/src/meeting';
21
+ import {Services} from '@webex/webex-core';
21
22
  import MeetingUtil from '@webex/plugin-meetings/src/meeting/util';
22
23
  import Meetings from '@webex/plugin-meetings/src/meetings';
23
24
  import MeetingCollection from '@webex/plugin-meetings/src/meetings/collection';
@@ -75,6 +76,8 @@ describe('plugin-meetings', () => {
75
76
  let test1;
76
77
  let test2;
77
78
  let locusInfo;
79
+ let services;
80
+ let catalog;
78
81
 
79
82
  describe('meetings index', () => {
80
83
  beforeEach(() => {
@@ -93,9 +96,13 @@ describe('plugin-meetings', () => {
93
96
  device: Device,
94
97
  mercury: Mercury,
95
98
  meetings: Meetings,
99
+ services: Services,
96
100
  },
97
101
  });
98
102
 
103
+ services = webex.internal.services;
104
+ catalog = services._getCatalog();
105
+
99
106
  Object.assign(webex, {
100
107
  logging: logger,
101
108
  });
@@ -161,6 +168,7 @@ describe('plugin-meetings', () => {
161
168
  ],
162
169
  })
163
170
  ),
171
+ _getCatalog: sinon.stub().returns(catalog),
164
172
  fetchClientRegionInfo: sinon.stub().returns(Promise.resolve()),
165
173
  },
166
174
  metrics: {
@@ -1917,34 +1925,34 @@ describe('plugin-meetings', () => {
1917
1925
  let loggerProxySpy;
1918
1926
 
1919
1927
  it('should call request.getMeetingPreferences to get the preferred webex site ', async () => {
1928
+ assert.deepEqual(webex.internal.services._getCatalog().getAllowedDomains(), []);
1920
1929
  assert.isDefined(webex.meetings.preferredWebexSite);
1921
1930
  await webex.meetings.fetchUserPreferredWebexSite();
1922
1931
 
1923
1932
  assert.equal(webex.meetings.preferredWebexSite, 'go.webex.com');
1933
+ assert.deepEqual(webex.internal.services._getCatalog().getAllowedDomains(), [
1934
+ 'go.webex.com',
1935
+ ]);
1924
1936
  });
1925
1937
 
1926
1938
  const setup = ({user} = {}) => {
1927
1939
  loggerProxySpy = sinon.spy(LoggerProxy.logger, 'error');
1940
+ assert.deepEqual(webex.internal.services._getCatalog().getAllowedDomains(), []);
1928
1941
 
1929
1942
  Object.assign(webex.internal, {
1930
- services: {
1931
- getMeetingPreferences: sinon.stub().returns(Promise.resolve({})),
1932
- },
1933
1943
  user: {
1934
1944
  get: sinon.stub().returns(Promise.resolve(user)),
1935
1945
  },
1936
1946
  });
1947
+
1948
+ Object.assign(webex.internal.services, {
1949
+ getMeetingPreferences: sinon.stub().returns(Promise.resolve({})),
1950
+ });
1937
1951
  };
1938
1952
 
1939
1953
  it('should not fail if UserPreferred info is not fetched ', async () => {
1940
1954
  setup();
1941
1955
 
1942
- Object.assign(webex.internal, {
1943
- services: {
1944
- getMeetingPreferences: sinon.stub().returns(Promise.resolve({})),
1945
- },
1946
- });
1947
-
1948
1956
  await webex.meetings.fetchUserPreferredWebexSite().then(() => {
1949
1957
  assert.equal(webex.meetings.preferredWebexSite, '');
1950
1958
  });
@@ -1952,6 +1960,7 @@ describe('plugin-meetings', () => {
1952
1960
  loggerProxySpy,
1953
1961
  'Failed to fetch preferred site from user - no site will be set'
1954
1962
  );
1963
+ assert.deepEqual(webex.internal.services._getCatalog().getAllowedDomains(), ['']);
1955
1964
  });
1956
1965
 
1957
1966
  it('should fall back to fetching the site from the user', async () => {
@@ -1968,6 +1977,10 @@ describe('plugin-meetings', () => {
1968
1977
  await webex.meetings.fetchUserPreferredWebexSite();
1969
1978
 
1970
1979
  assert.equal(webex.meetings.preferredWebexSite, 'site.webex.com');
1980
+ assert.deepEqual(webex.internal.services._getCatalog().getAllowedDomains(), [
1981
+ '',
1982
+ 'site.webex.com',
1983
+ ]);
1971
1984
  assert.notCalled(loggerProxySpy);
1972
1985
  });
1973
1986
 
@@ -1989,6 +2002,7 @@ describe('plugin-meetings', () => {
1989
2002
  loggerProxySpy,
1990
2003
  'Failed to fetch preferred site from user - no site will be set'
1991
2004
  );
2005
+ assert.deepEqual(webex.internal.services._getCatalog().getAllowedDomains(), ['']);
1992
2006
  });
1993
2007
  }
1994
2008
  );
@@ -2005,6 +2019,7 @@ describe('plugin-meetings', () => {
2005
2019
  loggerProxySpy,
2006
2020
  'Failed to fetch preferred site from user - no site will be set'
2007
2021
  );
2022
+ assert.deepEqual(webex.internal.services._getCatalog().getAllowedDomains(), ['']);
2008
2023
  });
2009
2024
 
2010
2025
  it('should fall back to fetching the site from the user', async () => {
@@ -2022,6 +2037,10 @@ describe('plugin-meetings', () => {
2022
2037
 
2023
2038
  assert.equal(webex.meetings.preferredWebexSite, 'site.webex.com');
2024
2039
  assert.notCalled(loggerProxySpy);
2040
+ assert.deepEqual(webex.internal.services._getCatalog().getAllowedDomains(), [
2041
+ '',
2042
+ 'site.webex.com',
2043
+ ]);
2025
2044
  });
2026
2045
 
2027
2046
  forEach(
@@ -2042,6 +2061,7 @@ describe('plugin-meetings', () => {
2042
2061
  loggerProxySpy,
2043
2062
  'Failed to fetch preferred site from user - no site will be set'
2044
2063
  );
2064
+ assert.deepEqual(webex.internal.services._getCatalog().getAllowedDomains(), ['']);
2045
2065
  });
2046
2066
  }
2047
2067
  );
@@ -2058,6 +2078,7 @@ describe('plugin-meetings', () => {
2058
2078
  loggerProxySpy,
2059
2079
  'Failed to fetch preferred site from user - no site will be set'
2060
2080
  );
2081
+ assert.deepEqual(webex.internal.services._getCatalog().getAllowedDomains(), ['']);
2061
2082
  });
2062
2083
  });
2063
2084
  });
@@ -2344,12 +2365,14 @@ describe('plugin-meetings', () => {
2344
2365
  sessionType: 'MAIN',
2345
2366
  };
2346
2367
  newLocus.self.state = 'JOINED';
2347
- newLocus.self.devices = [{
2348
- intent: {
2349
- reason: 'ON_HOLD_LOBBY',
2350
- type: 'WAIT',
2351
- }
2352
- }];
2368
+ newLocus.self.devices = [
2369
+ {
2370
+ intent: {
2371
+ reason: 'ON_HOLD_LOBBY',
2372
+ type: 'WAIT',
2373
+ },
2374
+ },
2375
+ ];
2353
2376
  LoggerProxy.logger.log = sinon.stub();
2354
2377
  const result = webex.meetings.isNeedHandleLocusDTO(meeting, newLocus);
2355
2378
  assert.equal(result, true);
@@ -141,6 +141,272 @@ describe('isAnyPublicClusterReachable', () => {
141
141
  });
142
142
  });
143
143
 
144
+
145
+ describe('isWebexMediaBackendUnreachable', () => {
146
+ let webex;
147
+
148
+ beforeEach(() => {
149
+ webex = new MockWebex();
150
+
151
+ sinon.stub(MeetingUtil, 'getIpVersion').returns(IP_VERSION.unknown);
152
+ });
153
+
154
+ afterEach(() => {
155
+ sinon.restore();
156
+ });
157
+
158
+ const runCheck = async (mockStorage: any, expectedValue: boolean) => {
159
+ if (mockStorage) {
160
+ await webex.boundedStorage.put(
161
+ 'Reachability',
162
+ 'reachability.result',
163
+ JSON.stringify(mockStorage)
164
+ );
165
+ }
166
+ const reachability = new Reachability(webex);
167
+
168
+ const result = await reachability.isWebexMediaBackendUnreachable();
169
+
170
+ assert.equal(result, expectedValue);
171
+ };
172
+
173
+ [
174
+ {
175
+ title: 'no clusters at all',
176
+ mockStorage: {},
177
+ expectedResult: false,
178
+ },
179
+ {
180
+ title: 'clusters without results',
181
+ mockStorage: {a: {}, b: {}},
182
+ expectedResult: false,
183
+ },
184
+ {
185
+ title: 'all clusters untested',
186
+ mockStorage: {
187
+ a: {udp: 'untested'},
188
+ b: {udp: 'untested', tcp: 'untested'},
189
+ },
190
+ expectedResult: false,
191
+ },
192
+ {
193
+ title: 'one cluster with udp reachable',
194
+ mockStorage: {x: {udp: {result: 'reachable'}, tcp: {result: 'unreachable'}}},
195
+ expectedResult: false,
196
+ },
197
+ {
198
+ title: 'one cluster with tcp reachable',
199
+ mockStorage: {x: {tcp: {result: 'reachable'}}},
200
+ expectedResult: false,
201
+ },
202
+ {
203
+ title: 'one cluster with xtls reachable',
204
+ mockStorage: {x: {xtls: {result: 'reachable'}}, y: {xtls: {result: 'unreachable'}}},
205
+ expectedResult: false,
206
+ },
207
+ {
208
+ title: 'multiple clusters with various protocols reachable',
209
+ mockStorage: {
210
+ a: {udp: {result: 'reachable'}, tcp: {result: 'reachable'}},
211
+ b: {udp: {result: 'unreachable'}, tcp: {result: 'reachable'}},
212
+ c: {tcp: {result: 'reachable'}},
213
+ d: {xtls: {result: 'reachable'}},
214
+ },
215
+ expectedResult: false,
216
+ },
217
+ {
218
+ title: 'multiple clusters with all protocols unreachable',
219
+ mockStorage: {
220
+ a: {
221
+ udp: {result: 'unreachable'},
222
+ tcp: {result: 'unreachable'},
223
+ xtls: {result: 'unreachable'},
224
+ },
225
+ b: {
226
+ udp: {result: 'unreachable'},
227
+ tcp: {result: 'unreachable'},
228
+ xtls: {result: 'unreachable'},
229
+ },
230
+ c: {
231
+ udp: {result: 'unreachable'},
232
+ tcp: {result: 'unreachable'},
233
+ xtls: {result: 'unreachable'},
234
+ },
235
+ },
236
+ expectedResult: true,
237
+ },
238
+ {
239
+ title: 'multiple clusters with UDP and TCP protocols unreachable, but TLS not tested',
240
+ mockStorage: {
241
+ a: {
242
+ udp: {result: 'unreachable'},
243
+ tcp: {result: 'unreachable'},
244
+ xtls: {result: 'untested'},
245
+ },
246
+ b: {
247
+ udp: {result: 'unreachable'},
248
+ tcp: {result: 'unreachable'},
249
+ xtls: {result: 'untested'},
250
+ },
251
+ c: {
252
+ udp: {result: 'unreachable'},
253
+ tcp: {result: 'unreachable'},
254
+ xtls: {result: 'untested'},
255
+ },
256
+ },
257
+ expectedResult: false,
258
+ },
259
+ {
260
+ title: 'multiple clusters with UDP and TCP protocols unreachable, but TLS missing',
261
+ mockStorage: {
262
+ a: {
263
+ udp: {result: 'unreachable'},
264
+ tcp: {result: 'unreachable'},
265
+ },
266
+ b: {
267
+ udp: {result: 'unreachable'},
268
+ tcp: {result: 'unreachable'},
269
+ },
270
+ c: {
271
+ udp: {result: 'unreachable'},
272
+ tcp: {result: 'unreachable'},
273
+ },
274
+ },
275
+ expectedResult: false,
276
+ },
277
+ {
278
+ title: 'multiple clusters with UDP and TLS protocols unreachable, but TCP not tested',
279
+ mockStorage: {
280
+ a: {
281
+ udp: {result: 'unreachable'},
282
+ tcp: {result: 'untested'},
283
+ xtls: {result: 'unreachable'},
284
+ },
285
+ b: {
286
+ udp: {result: 'unreachable'},
287
+ tcp: {result: 'untested'},
288
+ xtls: {result: 'unreachable'},
289
+ },
290
+ c: {
291
+ udp: {result: 'unreachable'},
292
+ tcp: {result: 'untested'},
293
+ xtls: {result: 'unreachable'},
294
+ },
295
+ },
296
+ expectedResult: false,
297
+ },
298
+ {
299
+ title: 'multiple clusters with UDP and TLS protocols unreachable, but TCP missing',
300
+ mockStorage: {
301
+ a: {
302
+ udp: {result: 'unreachable'},
303
+ xtls: {result: 'unreachable'},
304
+ },
305
+ b: {
306
+ udp: {result: 'unreachable'},
307
+ xtls: {result: 'unreachable'},
308
+ },
309
+ c: {
310
+ udp: {result: 'unreachable'},
311
+ xtls: {result: 'unreachable'},
312
+ },
313
+ },
314
+ expectedResult: false,
315
+ },
316
+ {
317
+ title: 'multiple clusters with all protocols unreachable, some untested',
318
+ mockStorage: {
319
+ a: {
320
+ udp: {result: 'unreachable'},
321
+ tcp: {result: 'unreachable'},
322
+ xtls: {result: 'unreachable'},
323
+ },
324
+ b: {udp: {result: 'unreachable'}, tcp: {result: 'untested'}, xtls: {result: 'unreachable'}},
325
+ c: {udp: {result: 'unreachable'}, tcp: {result: 'unreachable'}, xtls: {result: 'untested'}},
326
+ },
327
+ expectedResult: true,
328
+ },
329
+ {
330
+ title: 'multiple clusters with all protocols unreachable, except for 1 reachable on udp',
331
+ mockStorage: {
332
+ a: {
333
+ udp: {result: 'reachable'},
334
+ tcp: {result: 'unreachable'},
335
+ xtls: {result: 'unreachable'},
336
+ },
337
+ b: {
338
+ udp: {result: 'unreachable'},
339
+ tcp: {result: 'unreachable'},
340
+ xtls: {result: 'unreachable'},
341
+ },
342
+ c: {
343
+ udp: {result: 'unreachable'},
344
+ tcp: {result: 'unreachable'},
345
+ xtls: {result: 'unreachable'},
346
+ },
347
+ },
348
+ expectedResult: false,
349
+ },
350
+ {
351
+ title: 'multiple clusters with all protocols unreachable, except for 1 reachable on tcp',
352
+ mockStorage: {
353
+ a: {
354
+ udp: {result: 'unreachable'},
355
+ tcp: {result: 'unreachable'},
356
+ xtls: {result: 'unreachable'},
357
+ },
358
+ b: {
359
+ udp: {result: 'unreachable'},
360
+ tcp: {result: 'unreachable'},
361
+ xtls: {result: 'unreachable'},
362
+ },
363
+ c: {
364
+ udp: {result: 'unreachable'},
365
+ tcp: {result: 'reachable'},
366
+ xtls: {result: 'unreachable'},
367
+ },
368
+ },
369
+ expectedResult: false,
370
+ },
371
+ {
372
+ title: 'multiple clusters with all protocols unreachable, except for 1 reachable on xtls',
373
+ mockStorage: {
374
+ a: {
375
+ udp: {result: 'unreachable'},
376
+ tcp: {result: 'unreachable'},
377
+ xtls: {result: 'unreachable'},
378
+ },
379
+ b: {
380
+ udp: {result: 'unreachable'},
381
+ tcp: {result: 'unreachable'},
382
+ xtls: {result: 'reachable'},
383
+ },
384
+ c: {
385
+ udp: {result: 'unreachable'},
386
+ tcp: {result: 'unreachable'},
387
+ xtls: {result: 'unreachable'},
388
+ },
389
+ },
390
+ expectedResult: false,
391
+ },
392
+ {
393
+ title: 'multiple clusters with some missing results',
394
+ mockStorage: {
395
+ a: {udp: {result: 'unreachable'}},
396
+ b: {tcp: {result: 'unreachable'}},
397
+ c: {xtls: {result: 'unreachable'}},
398
+ d: {},
399
+ },
400
+ expectedResult: true,
401
+ },
402
+ ].forEach(({mockStorage, expectedResult, title}) => {
403
+ it(`returns ${expectedResult} when ${title}`, async () => {
404
+ await runCheck(mockStorage, expectedResult);
405
+ });
406
+ });
407
+ });
408
+
409
+
144
410
  describe('gatherReachability', () => {
145
411
  let webex;
146
412