@webex/plugin-meetings 3.8.0-next.14 → 3.8.0-next.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,7 @@
1
1
  import { ClusterNode } from './request';
2
2
  import EventsScope from '../common/events/events-scope';
3
3
  import { Enum } from '../constants';
4
- import { ClusterReachabilityResult } from './reachability.types';
4
+ import { ClusterReachabilityResult, NatType } from './reachability.types';
5
5
  export type ResultEventData = {
6
6
  protocol: 'udp' | 'tcp' | 'xtls';
7
7
  result: 'reachable' | 'unreachable' | 'untested';
@@ -12,9 +12,13 @@ export type ClientMediaIpsUpdatedEventData = {
12
12
  protocol: 'udp' | 'tcp' | 'xtls';
13
13
  clientMediaIPs: string[];
14
14
  };
15
+ export type NatTypeUpdatedEventData = {
16
+ natType: NatType;
17
+ };
15
18
  export declare const Events: {
16
19
  readonly resultReady: "resultReady";
17
20
  readonly clientMediaIpsUpdated: "clientMediaIpsUpdated";
21
+ readonly natTypeUpdated: "natTypeUpdated";
18
22
  };
19
23
  export type Events = Enum<typeof Events>;
20
24
  /**
@@ -29,6 +33,7 @@ export declare class ClusterReachability extends EventsScope {
29
33
  private pc?;
30
34
  private defer;
31
35
  private startTimestamp;
36
+ private srflxIceCandidates;
32
37
  readonly isVideoMesh: boolean;
33
38
  readonly name: any;
34
39
  /**
@@ -107,6 +112,13 @@ export declare class ClusterReachability extends EventsScope {
107
112
  * @returns {void}
108
113
  */
109
114
  private saveResult;
115
+ /**
116
+ * Determines NAT Type.
117
+ *
118
+ * @param {RTCIceCandidate} candidate
119
+ * @returns {void}
120
+ */
121
+ private determineNatType;
110
122
  /**
111
123
  * Registers a listener for the icecandidate event
112
124
  *
@@ -4,7 +4,7 @@
4
4
  import { Defer } from '@webex/common';
5
5
  import { IP_VERSION } from '../constants';
6
6
  import ReachabilityRequest, { ClusterList } from './request';
7
- import { ClusterReachabilityResult, ClientMediaPreferences, ReachabilityMetrics, ReachabilityReportV0, ReachabilityReportV1, ReachabilityResults, ReachabilityResultsForBackend, GetClustersTrigger } from './reachability.types';
7
+ import { ClusterReachabilityResult, ClientMediaPreferences, ReachabilityMetrics, ReachabilityReportV0, ReachabilityReportV1, ReachabilityResults, ReachabilityResultsForBackend, GetClustersTrigger, NatType } from './reachability.types';
8
8
  import { ClusterReachability } from './clusterReachability';
9
9
  import EventsScope from '../common/events/events-scope';
10
10
  /**
@@ -46,6 +46,7 @@ export default class Reachability extends EventsScope {
46
46
  };
47
47
  startTime: any;
48
48
  totalDuration: any;
49
+ natType: NatType;
49
50
  protected lastTrigger?: string;
50
51
  /**
51
52
  * Creates an instance of Reachability.
@@ -4,6 +4,10 @@ export type TransportResult = {
4
4
  latencyInMilliseconds?: number;
5
5
  clientMediaIPs?: string[];
6
6
  };
7
+ export declare enum NatType {
8
+ Unknown = "unknown",
9
+ SymmetricNat = "symmetric-nat"
10
+ }
7
11
  export type ClusterReachabilityResult = {
8
12
  udp: TransportResult;
9
13
  tcp: TransportResult;
@@ -22,6 +26,7 @@ export type ReachabilityMetrics = {
22
26
  reachability_vmn_tcp_failed: number;
23
27
  reachability_vmn_xtls_success: number;
24
28
  reachability_vmn_xtls_failed: number;
29
+ natType: NatType;
25
30
  };
26
31
  /**
27
32
  * This is the type that matches what backend expects us to send to them. It is a bit weird, because
@@ -458,7 +458,7 @@ var Webinar = _webexCore.WebexPlugin.extend({
458
458
  }, _callee7);
459
459
  }))();
460
460
  },
461
- version: "3.8.0-next.14"
461
+ version: "3.8.0-next.16"
462
462
  });
463
463
  var _default = exports.default = Webinar;
464
464
  //# sourceMappingURL=index.js.map
package/package.json CHANGED
@@ -43,13 +43,13 @@
43
43
  "@webex/eslint-config-legacy": "0.0.0",
44
44
  "@webex/jest-config-legacy": "0.0.0",
45
45
  "@webex/legacy-tools": "0.0.0",
46
- "@webex/plugin-meetings": "3.8.0-next.14",
47
- "@webex/plugin-rooms": "3.8.0-next.8",
48
- "@webex/test-helper-chai": "3.8.0-next.7",
49
- "@webex/test-helper-mocha": "3.8.0-next.7",
50
- "@webex/test-helper-mock-webex": "3.8.0-next.7",
51
- "@webex/test-helper-retry": "3.8.0-next.7",
52
- "@webex/test-helper-test-users": "3.8.0-next.7",
46
+ "@webex/plugin-meetings": "3.8.0-next.16",
47
+ "@webex/plugin-rooms": "3.8.0-next.9",
48
+ "@webex/test-helper-chai": "3.8.0-next.8",
49
+ "@webex/test-helper-mocha": "3.8.0-next.8",
50
+ "@webex/test-helper-mock-webex": "3.8.0-next.8",
51
+ "@webex/test-helper-retry": "3.8.0-next.8",
52
+ "@webex/test-helper-test-users": "3.8.0-next.8",
53
53
  "chai": "^4.3.4",
54
54
  "chai-as-promised": "^7.1.1",
55
55
  "eslint": "^8.24.0",
@@ -61,22 +61,22 @@
61
61
  "typescript": "^4.7.4"
62
62
  },
63
63
  "dependencies": {
64
- "@webex/common": "3.8.0-next.7",
64
+ "@webex/common": "3.8.0-next.8",
65
65
  "@webex/event-dictionary-ts": "^1.0.1688",
66
66
  "@webex/internal-media-core": "2.14.4",
67
- "@webex/internal-plugin-conversation": "3.8.0-next.8",
68
- "@webex/internal-plugin-device": "3.8.0-next.7",
69
- "@webex/internal-plugin-llm": "3.8.0-next.9",
70
- "@webex/internal-plugin-mercury": "3.8.0-next.8",
71
- "@webex/internal-plugin-metrics": "3.8.0-next.7",
72
- "@webex/internal-plugin-support": "3.8.0-next.8",
73
- "@webex/internal-plugin-user": "3.8.0-next.7",
74
- "@webex/internal-plugin-voicea": "3.8.0-next.14",
75
- "@webex/media-helpers": "3.8.0-next.7",
76
- "@webex/plugin-people": "3.8.0-next.8",
77
- "@webex/plugin-rooms": "3.8.0-next.8",
67
+ "@webex/internal-plugin-conversation": "3.8.0-next.9",
68
+ "@webex/internal-plugin-device": "3.8.0-next.8",
69
+ "@webex/internal-plugin-llm": "3.8.0-next.10",
70
+ "@webex/internal-plugin-mercury": "3.8.0-next.9",
71
+ "@webex/internal-plugin-metrics": "3.8.0-next.8",
72
+ "@webex/internal-plugin-support": "3.8.0-next.9",
73
+ "@webex/internal-plugin-user": "3.8.0-next.8",
74
+ "@webex/internal-plugin-voicea": "3.8.0-next.16",
75
+ "@webex/media-helpers": "3.8.0-next.8",
76
+ "@webex/plugin-people": "3.8.0-next.9",
77
+ "@webex/plugin-rooms": "3.8.0-next.9",
78
78
  "@webex/web-capabilities": "^1.4.0",
79
- "@webex/webex-core": "3.8.0-next.7",
79
+ "@webex/webex-core": "3.8.0-next.8",
80
80
  "ampersand-collection": "^2.0.2",
81
81
  "bowser": "^2.11.0",
82
82
  "btoa": "^1.2.1",
@@ -92,5 +92,5 @@
92
92
  "//": [
93
93
  "TODO: upgrade jwt-decode when moving to node 18"
94
94
  ],
95
- "version": "3.8.0-next.14"
95
+ "version": "3.8.0-next.16"
96
96
  }
@@ -1686,6 +1686,19 @@ export default class Meeting extends StatelessWebexPlugin {
1686
1686
  return this.#isoLocalClientMeetingJoinTime;
1687
1687
  }
1688
1688
 
1689
+ /**
1690
+ * Setter - sets isoLocalClientMeetingJoinTime
1691
+ * This will be set once on meeting join, and not updated again
1692
+ * @param {string | undefined} time in ISO format
1693
+ */
1694
+ set isoLocalClientMeetingJoinTime(time: string | undefined) {
1695
+ if (!time) {
1696
+ this.#isoLocalClientMeetingJoinTime = new Date().toISOString();
1697
+ } else {
1698
+ this.#isoLocalClientMeetingJoinTime = time;
1699
+ }
1700
+ }
1701
+
1689
1702
  /**
1690
1703
  * Set meeting info and trigger `MEETING_INFO_AVAILABLE` event
1691
1704
  * @param {any} info
@@ -5718,8 +5731,6 @@ export default class Meeting extends StatelessWebexPlugin {
5718
5731
  // @ts-ignore
5719
5732
  this.webex.internal.device.meetingStarted();
5720
5733
 
5721
- this.#isoLocalClientMeetingJoinTime = new Date().toISOString();
5722
-
5723
5734
  LoggerProxy.logger.log('Meeting:index#join --> Success');
5724
5735
 
5725
5736
  Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.JOIN_SUCCESS, {
@@ -180,7 +180,7 @@ const MeetingUtil = {
180
180
  .then((res) => {
181
181
  const parsed = MeetingUtil.parseLocusJoin(res);
182
182
  meeting.setLocus(parsed);
183
-
183
+ meeting.isoLocalClientMeetingJoinTime = res?.headers?.date; // read from header if exist, else fall back to system clock : https://jira-eng-gpk2.cisco.com/jira/browse/SPARK-555657
184
184
  webex.internal.newMetrics.submitClientEvent({
185
185
  name: 'client.locus.join.response',
186
186
  payload: {
@@ -6,7 +6,7 @@ import {convertStunUrlToTurn, convertStunUrlToTurnTls} from './util';
6
6
  import EventsScope from '../common/events/events-scope';
7
7
 
8
8
  import {CONNECTION_STATE, Enum, ICE_GATHERING_STATE} from '../constants';
9
- import {ClusterReachabilityResult} from './reachability.types';
9
+ import {ClusterReachabilityResult, NatType} from './reachability.types';
10
10
 
11
11
  // data for the Events.resultReady event
12
12
  export type ResultEventData = {
@@ -22,9 +22,14 @@ export type ClientMediaIpsUpdatedEventData = {
22
22
  clientMediaIPs: string[];
23
23
  };
24
24
 
25
+ export type NatTypeUpdatedEventData = {
26
+ natType: NatType;
27
+ };
28
+
25
29
  export const Events = {
26
30
  resultReady: 'resultReady', // emitted when a cluster is reached successfully using specific protocol
27
31
  clientMediaIpsUpdated: 'clientMediaIpsUpdated', // emitted when more public IPs are found after resultReady was already sent for a given protocol
32
+ natTypeUpdated: 'natTypeUpdated', // emitted when NAT type is determined
28
33
  } as const;
29
34
 
30
35
  export type Events = Enum<typeof Events>;
@@ -41,6 +46,7 @@ export class ClusterReachability extends EventsScope {
41
46
  private pc?: RTCPeerConnection;
42
47
  private defer: Defer; // this defer is resolved once reachability checks for this cluster are completed
43
48
  private startTimestamp: number;
49
+ private srflxIceCandidates: RTCIceCandidate[] = [];
44
50
  public readonly isVideoMesh: boolean;
45
51
  public readonly name;
46
52
 
@@ -290,6 +296,44 @@ export class ClusterReachability extends EventsScope {
290
296
  }
291
297
  }
292
298
 
299
+ /**
300
+ * Determines NAT Type.
301
+ *
302
+ * @param {RTCIceCandidate} candidate
303
+ * @returns {void}
304
+ */
305
+ private determineNatType(candidate: RTCIceCandidate) {
306
+ this.srflxIceCandidates.push(candidate);
307
+
308
+ if (this.srflxIceCandidates.length > 1) {
309
+ const portsFound: Record<string, Set<number>> = {};
310
+
311
+ this.srflxIceCandidates.forEach((c) => {
312
+ const key = `${c.address}:${c.relatedPort}`;
313
+ if (!portsFound[key]) {
314
+ portsFound[key] = new Set();
315
+ }
316
+ portsFound[key].add(c.port);
317
+ });
318
+
319
+ Object.entries(portsFound).forEach(([, ports]) => {
320
+ if (ports.size > 1) {
321
+ // Found candidates with the same address and relatedPort, but different ports
322
+ this.emit(
323
+ {
324
+ file: 'clusterReachability',
325
+ function: 'determineNatType',
326
+ },
327
+ Events.natTypeUpdated,
328
+ {
329
+ natType: NatType.SymmetricNat,
330
+ }
331
+ );
332
+ }
333
+ });
334
+ }
335
+ }
336
+
293
337
  /**
294
338
  * Registers a listener for the icecandidate event
295
339
  *
@@ -308,6 +352,8 @@ export class ClusterReachability extends EventsScope {
308
352
  if (e.candidate) {
309
353
  if (e.candidate.type === CANDIDATE_TYPES.SERVER_REFLEXIVE) {
310
354
  this.saveResult('udp', latencyInMilliseconds, e.candidate.address);
355
+
356
+ this.determineNatType(e.candidate);
311
357
  }
312
358
 
313
359
  if (e.candidate.type === CANDIDATE_TYPES.RELAY) {
@@ -23,11 +23,13 @@ import {
23
23
  ReachabilityResultsForBackend,
24
24
  TransportResultForBackend,
25
25
  GetClustersTrigger,
26
+ NatType,
26
27
  } from './reachability.types';
27
28
  import {
28
29
  ClientMediaIpsUpdatedEventData,
29
30
  ClusterReachability,
30
31
  Events,
32
+ NatTypeUpdatedEventData,
31
33
  ResultEventData,
32
34
  } from './clusterReachability';
33
35
  import EventsScope from '../common/events/events-scope';
@@ -64,6 +66,7 @@ export default class Reachability extends EventsScope {
64
66
  resultsCount = {videoMesh: {udp: 0}, public: {udp: 0, tcp: 0, xtls: 0}};
65
67
  startTime = undefined;
66
68
  totalDuration = undefined;
69
+ natType = NatType.Unknown;
67
70
 
68
71
  protected lastTrigger?: string;
69
72
 
@@ -309,6 +312,7 @@ export default class Reachability extends EventsScope {
309
312
  reachability_vmn_tcp_failed: 0,
310
313
  reachability_vmn_xtls_success: 0,
311
314
  reachability_vmn_xtls_failed: 0,
315
+ natType: this.natType,
312
316
  };
313
317
 
314
318
  const updateStats = (clusterType: 'public' | 'vmn', result: ClusterReachabilityResult) => {
@@ -967,6 +971,13 @@ export default class Reachability extends EventsScope {
967
971
  }
968
972
  );
969
973
 
974
+ this.clusterReachability[key].on(
975
+ Events.natTypeUpdated,
976
+ async (data: NatTypeUpdatedEventData) => {
977
+ this.natType = data.natType;
978
+ }
979
+ );
980
+
970
981
  this.clusterReachability[key].start(); // not awaiting on purpose
971
982
  });
972
983
  }
@@ -7,6 +7,11 @@ export type TransportResult = {
7
7
  clientMediaIPs?: string[];
8
8
  };
9
9
 
10
+ export enum NatType {
11
+ Unknown = 'unknown',
12
+ SymmetricNat = 'symmetric-nat',
13
+ }
14
+
10
15
  // reachability result for a specific media cluster
11
16
  export type ClusterReachabilityResult = {
12
17
  udp: TransportResult;
@@ -27,6 +32,7 @@ export type ReachabilityMetrics = {
27
32
  reachability_vmn_tcp_failed: number;
28
33
  reachability_vmn_xtls_success: number;
29
34
  reachability_vmn_xtls_failed: number;
35
+ natType: NatType;
30
36
  };
31
37
 
32
38
  /**
@@ -210,6 +210,7 @@ describe('plugin-meetings', () => {
210
210
  let membersSpy;
211
211
  let meetingRequestSpy;
212
212
  let correlationId;
213
+ let isoLocalClientMeetingJoinTime;
213
214
  let uploadEvent;
214
215
 
215
216
  beforeEach(() => {
@@ -1692,10 +1693,6 @@ describe('plugin-meetings', () => {
1692
1693
  sandbox.stub(MeetingUtil, 'joinMeeting').returns(Promise.resolve(joinMeetingResult));
1693
1694
  });
1694
1695
 
1695
- afterEach(() => {
1696
- assert.exists(meeting.isoLocalClientMeetingJoinTime);
1697
- });
1698
-
1699
1696
  it('should join the meeting and return promise', async () => {
1700
1697
  const join = meeting.join({pstnAudioType: 'dial-in'});
1701
1698
  meeting.config.enableAutomaticLLM = true;
@@ -7530,6 +7527,25 @@ describe('plugin-meetings', () => {
7530
7527
  });
7531
7528
  });
7532
7529
 
7530
+ describe('#setIsoLocalClientMeetingJoinTime', () => {
7531
+ it('should set the isoLocalClientMeetingJoinTime when passed in', () => {
7532
+ assert.equal(meeting.isoLocalClientMeetingJoinTime, isoLocalClientMeetingJoinTime);
7533
+ meeting.isoLocalClientMeetingJoinTime = 'test';
7534
+ assert.equal(meeting.isoLocalClientMeetingJoinTime, 'test');
7535
+ meeting.isoLocalClientMeetingJoinTime = 'test2';
7536
+ assert.equal(meeting.isoLocalClientMeetingJoinTime, 'test2');
7537
+ });
7538
+
7539
+ it('should set the isoLocalClientMeetingJoin time once and only once when not passed in', () => {
7540
+ assert.equal(meeting.isoLocalClientMeetingJoinTime, isoLocalClientMeetingJoinTime);
7541
+ meeting.isoLocalClientMeetingJoinTime = undefined;
7542
+ const time = meeting.isoLocalClientMeetingJoinTime;
7543
+ assert.equal(meeting.isoLocalClientMeetingJoinTime, time);
7544
+ meeting.isoLocalClientMeetingJoinTime = 'test2';
7545
+ assert.equal(meeting.isoLocalClientMeetingJoinTime, 'test2');
7546
+ });
7547
+ });
7548
+
7533
7549
  describe('#updateCallStateForMetrics', () => {
7534
7550
  it('should update the callState, overriding existing values', () => {
7535
7551
  assert.deepEqual(meeting.callStateForMetrics, {correlationId, sessionCorrelationId: ''});
@@ -460,6 +460,50 @@ describe('plugin-meetings', () => {
460
460
  });
461
461
  });
462
462
 
463
+
464
+ it('#Should call `meetingRequest.joinMeeting and handle a date header in the response : isoLocalClientMeetingJoinedTime', async () => {
465
+ meeting.isMultistream = true;
466
+
467
+ const FAKE_REACHABILITY_REPORT = {
468
+ id: 'fake reachability report',
469
+ };
470
+ const FAKE_CLIENT_MEDIA_PREFERENCES = {
471
+ id: 'fake client media preferences',
472
+ };
473
+
474
+ webex.meetings.reachability.getReachabilityReportToAttachToRoap.resolves(FAKE_REACHABILITY_REPORT);
475
+ webex.meetings.reachability.getClientMediaPreferences.resolves(FAKE_CLIENT_MEDIA_PREFERENCES);
476
+
477
+ sinon
478
+ .stub(webex.internal.device.ipNetworkDetector, 'supportsIpV4')
479
+ .get(() => true);
480
+ sinon
481
+ .stub(webex.internal.device.ipNetworkDetector, 'supportsIpV6')
482
+ .get(() => true);
483
+
484
+ meeting.meetingRequest.joinMeeting.resolves({
485
+ headers: {
486
+ date: 'test'
487
+ },
488
+ body: {
489
+ mediaConnections: [{mediaId: 'test'}],
490
+ locus: {
491
+ url: 'test',
492
+ self: {
493
+ id: 'test'
494
+ }
495
+ }
496
+ }
497
+ })
498
+
499
+ await MeetingUtil.joinMeeting(meeting, {
500
+ reachability: 'reachability',
501
+ roapMessage: 'roapMessage',
502
+ });
503
+
504
+ assert.equal(meeting.isoLocalClientMeetingJoinTime, 'test');
505
+ });
506
+
463
507
  it('should handle failed reachability report retrieval', async () => {
464
508
  webex.meetings.reachability.getReachabilityReportToAttachToRoap.rejects(
465
509
  new Error('fake error')
@@ -9,7 +9,9 @@ import {
9
9
  ResultEventData,
10
10
  Events,
11
11
  ClientMediaIpsUpdatedEventData,
12
+ NatTypeUpdatedEventData,
12
13
  } from '@webex/plugin-meetings/src/reachability/clusterReachability'; // replace with actual path
14
+ import { NatType } from 'packages/@webex/plugin-meetings/dist/reachability/reachability.types';
13
15
 
14
16
  describe('ClusterReachability', () => {
15
17
  let previousRTCPeerConnection;
@@ -17,15 +19,17 @@ describe('ClusterReachability', () => {
17
19
  let fakePeerConnection;
18
20
  let gatherIceCandidatesSpy;
19
21
 
20
- const emittedEvents: Record<Events, (ResultEventData | ClientMediaIpsUpdatedEventData)[]> = {
22
+ const emittedEvents: Record<Events, (ResultEventData | ClientMediaIpsUpdatedEventData | NatTypeUpdatedEventData)[]> = {
21
23
  [Events.resultReady]: [],
22
24
  [Events.clientMediaIpsUpdated]: [],
25
+ [Events.natTypeUpdated]: [],
23
26
  };
24
27
  const FAKE_OFFER = {type: 'offer', sdp: 'fake sdp'};
25
28
 
26
29
  const resetEmittedEvents = () => {
27
30
  emittedEvents[Events.resultReady].length = 0;
28
31
  emittedEvents[Events.clientMediaIpsUpdated].length = 0;
32
+ emittedEvents[Events.natTypeUpdated].length = 0;
29
33
  };
30
34
  beforeEach(() => {
31
35
  fakePeerConnection = {
@@ -56,6 +60,10 @@ describe('ClusterReachability', () => {
56
60
  clusterReachability.on(Events.clientMediaIpsUpdated, (data: ClientMediaIpsUpdatedEventData) => {
57
61
  emittedEvents[Events.clientMediaIpsUpdated].push(data);
58
62
  });
63
+
64
+ clusterReachability.on(Events.natTypeUpdated, (data: NatTypeUpdatedEventData) => {
65
+ emittedEvents[Events.natTypeUpdated].push(data);
66
+ });
59
67
  });
60
68
 
61
69
  afterEach(() => {
@@ -440,5 +448,43 @@ describe('ClusterReachability', () => {
440
448
  xtls: {result: 'reachable', latencyInMilliseconds: 40},
441
449
  });
442
450
  });
451
+
452
+ it('determines correctly if symmetric-nat is detected', async () => {
453
+ const promise = clusterReachability.start();
454
+
455
+ // generate candidates with duplicate addresses
456
+ await clock.tickAsync(10);
457
+ fakePeerConnection.onicecandidate({candidate: {type: 'srflx', address: 'somePublicIp1', relatedPort: 3478, port: 1000}});
458
+
459
+ // check events emitted: there shouldn't be any natTypeUpdated emitted
460
+ assert.equal(emittedEvents[Events.natTypeUpdated].length, 0);
461
+
462
+ await clock.tickAsync(10);
463
+ fakePeerConnection.onicecandidate({candidate: {type: 'srflx', address: 'somePublicIp1', relatedPort: 3478, port: 2000}});
464
+
465
+ // should emit natTypeUpdated event
466
+ assert.equal(emittedEvents[Events.natTypeUpdated].length, 1);
467
+ assert.deepEqual(emittedEvents[Events.natTypeUpdated][0], {
468
+ natType: 'symmetric-nat',
469
+ });
470
+
471
+ // send also a relay candidate so that the reachability check finishes
472
+ fakePeerConnection.onicecandidate({candidate: {type: 'relay', address: 'someTurnRelayIp'}});
473
+ fakePeerConnection.onicecandidate({
474
+ candidate: {type: 'relay', address: 'someTurnRelayIp', port: 443},
475
+ });
476
+
477
+ await promise;
478
+
479
+ assert.deepEqual(clusterReachability.getResult(), {
480
+ udp: {
481
+ result: 'reachable',
482
+ latencyInMilliseconds: 10,
483
+ clientMediaIPs: ['somePublicIp1'],
484
+ },
485
+ tcp: {result: 'reachable', latencyInMilliseconds: 20},
486
+ xtls: {result: 'reachable', latencyInMilliseconds: 20},
487
+ });
488
+ });
443
489
  });
444
490
  });
@@ -2156,6 +2156,7 @@ describe('getReachabilityMetrics', () => {
2156
2156
  reachability_vmn_tcp_failed: 0,
2157
2157
  reachability_vmn_xtls_success: 0,
2158
2158
  reachability_vmn_xtls_failed: 0,
2159
+ natType: 'unknown'
2159
2160
  });
2160
2161
  });
2161
2162
 
@@ -2223,6 +2224,7 @@ describe('getReachabilityMetrics', () => {
2223
2224
  reachability_vmn_tcp_failed: 1,
2224
2225
  reachability_vmn_xtls_success: 0,
2225
2226
  reachability_vmn_xtls_failed: 0,
2227
+ natType: 'unknown'
2226
2228
  }
2227
2229
  );
2228
2230
  });
@@ -2284,6 +2286,7 @@ describe('getReachabilityMetrics', () => {
2284
2286
  reachability_vmn_tcp_failed: 0,
2285
2287
  reachability_vmn_xtls_success: 0,
2286
2288
  reachability_vmn_xtls_failed: 0,
2289
+ natType: 'unknown'
2287
2290
  }
2288
2291
  );
2289
2292
  });
@@ -2345,6 +2348,7 @@ describe('getReachabilityMetrics', () => {
2345
2348
  reachability_vmn_tcp_failed: 3,
2346
2349
  reachability_vmn_xtls_success: 1,
2347
2350
  reachability_vmn_xtls_failed: 1,
2351
+ natType: 'unknown'
2348
2352
  }
2349
2353
  );
2350
2354
  });