@webex/plugin-meetings 3.11.0-next.4 → 3.11.0-next.6

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.
@@ -2,6 +2,31 @@ import { LocalCameraStream, LocalMicrophoneStream } from '@webex/media-helpers';
2
2
  import { SELF_POLICY, IP_VERSION } from '../constants';
3
3
  declare const MeetingUtil: {
4
4
  parseLocusJoin: (response: any) => any;
5
+ /**
6
+ * Sanitizes a WebSocket URL by extracting only protocol, host, and pathname
7
+ * Returns concatenated protocol + host + pathname for safe logging
8
+ * @param {string} urlString - The URL to sanitize
9
+ * @returns {string} Sanitized URL or empty string if parsing fails
10
+ */
11
+ sanitizeWebSocketUrl: (urlString: string) => string;
12
+ /**
13
+ * Compares two URLs by protocol, host, and pathname (ignoring query params and hash)
14
+ * Uses sanitizeWebSocketUrl to ensure comparison matches what gets reported
15
+ * @param {string} url1 - First URL to compare
16
+ * @param {string} url2 - Second URL to compare
17
+ * @returns {boolean} True if URLs match, false otherwise
18
+ */
19
+ _urlsMatch: (url1: string, url2: string) => boolean;
20
+ /**
21
+ * Gets socket URL information for metrics, including whether the socket URLs match
22
+ * @param {Object} webex - The webex instance
23
+ * @returns {Object} Object with hasMismatchedSocket, mercurySocketUrl, and deviceSocketUrl properties
24
+ */
25
+ getSocketUrlInfo: (webex: any) => {
26
+ hasMismatchedSocket: boolean;
27
+ mercurySocketUrl: string;
28
+ deviceSocketUrl: string;
29
+ };
5
30
  remoteUpdateAudioVideo: (meeting: any, audioMuted?: boolean, videoMuted?: boolean) => any;
6
31
  hasOwner: (info: any) => any;
7
32
  isOwnerSelf: (owner: any, selfId: any) => boolean;
@@ -506,7 +506,7 @@ var Webinar = _webexCore.WebexPlugin.extend({
506
506
  }, _callee8);
507
507
  }))();
508
508
  },
509
- version: "3.11.0-next.4"
509
+ version: "3.11.0-next.6"
510
510
  });
511
511
  var _default = exports.default = Webinar;
512
512
  //# sourceMappingURL=index.js.map
package/package.json CHANGED
@@ -93,5 +93,5 @@
93
93
  "//": [
94
94
  "TODO: upgrade jwt-decode when moving to node 18"
95
95
  ],
96
- "version": "3.11.0-next.4"
96
+ "version": "3.11.0-next.6"
97
97
  }
@@ -1,4 +1,5 @@
1
1
  import {LocalCameraStream, LocalMicrophoneStream} from '@webex/media-helpers';
2
+ import url from 'url';
2
3
 
3
4
  import {cloneDeep} from 'lodash';
4
5
  import {MeetingNotActiveError, UserNotJoinedError} from '../common/errors/webex-errors';
@@ -47,6 +48,111 @@ const MeetingUtil = {
47
48
  return parsed;
48
49
  },
49
50
 
51
+ /**
52
+ * Sanitizes a WebSocket URL by extracting only protocol, host, and pathname
53
+ * Returns concatenated protocol + host + pathname for safe logging
54
+ * @param {string} urlString - The URL to sanitize
55
+ * @returns {string} Sanitized URL or empty string if parsing fails
56
+ */
57
+ sanitizeWebSocketUrl: (urlString: string): string => {
58
+ if (!urlString || typeof urlString !== 'string') {
59
+ return '';
60
+ }
61
+
62
+ try {
63
+ const parsedUrl = url.parse(urlString);
64
+ const protocol = parsedUrl.protocol || '';
65
+ const host = parsedUrl.host || '';
66
+
67
+ // If we don't have at least protocol and host, it's not a valid URL
68
+ if (!protocol || !host) {
69
+ return '';
70
+ }
71
+
72
+ const pathname = parsedUrl.pathname || '';
73
+
74
+ // Strip trailing slash if pathname is just '/'
75
+ const normalizedPathname = pathname === '/' ? '' : pathname;
76
+
77
+ return `${protocol}//${host}${normalizedPathname}`;
78
+ } catch (error) {
79
+ LoggerProxy.logger.warn(
80
+ `Meeting:util#sanitizeWebSocketUrl --> unable to parse URL: ${error}`
81
+ );
82
+
83
+ return '';
84
+ }
85
+ },
86
+
87
+ /**
88
+ * Compares two URLs by protocol, host, and pathname (ignoring query params and hash)
89
+ * Uses sanitizeWebSocketUrl to ensure comparison matches what gets reported
90
+ * @param {string} url1 - First URL to compare
91
+ * @param {string} url2 - Second URL to compare
92
+ * @returns {boolean} True if URLs match, false otherwise
93
+ */
94
+ _urlsMatch: (url1: string, url2: string): boolean => {
95
+ if (!url1 || !url2) {
96
+ return false;
97
+ }
98
+
99
+ try {
100
+ const sanitized1 = MeetingUtil.sanitizeWebSocketUrl(url1);
101
+ const sanitized2 = MeetingUtil.sanitizeWebSocketUrl(url2);
102
+
103
+ // If either failed to parse (empty string), they don't match
104
+ if (!sanitized1 || !sanitized2) {
105
+ return false;
106
+ }
107
+
108
+ return sanitized1 === sanitized2;
109
+ } catch (e) {
110
+ LoggerProxy.logger.warn('Meeting:util#_urlsMatch --> error comparing URLs', e);
111
+
112
+ return false;
113
+ }
114
+ },
115
+
116
+ /**
117
+ * Gets socket URL information for metrics, including whether the socket URLs match
118
+ * @param {Object} webex - The webex instance
119
+ * @returns {Object} Object with hasMismatchedSocket, mercurySocketUrl, and deviceSocketUrl properties
120
+ */
121
+ getSocketUrlInfo: (
122
+ webex: any
123
+ ): {hasMismatchedSocket: boolean; mercurySocketUrl: string; deviceSocketUrl: string} => {
124
+ try {
125
+ const mercuryUrl = webex?.internal?.mercury?.socket?.url;
126
+ const deviceUrl = webex?.internal?.device?.webSocketUrl;
127
+
128
+ const sanitizedMercuryUrl = MeetingUtil.sanitizeWebSocketUrl(mercuryUrl);
129
+ const sanitizedDeviceUrl = MeetingUtil.sanitizeWebSocketUrl(deviceUrl);
130
+
131
+ // Only report a mismatch if both URLs are present and they don't match
132
+ // If either URL is missing, we can't determine if there's a mismatch, so return false
133
+ let hasMismatchedSocket = false;
134
+ if (sanitizedMercuryUrl && sanitizedDeviceUrl) {
135
+ hasMismatchedSocket = !MeetingUtil._urlsMatch(mercuryUrl, deviceUrl);
136
+ }
137
+
138
+ return {
139
+ hasMismatchedSocket,
140
+ mercurySocketUrl: sanitizedMercuryUrl,
141
+ deviceSocketUrl: sanitizedDeviceUrl,
142
+ };
143
+ } catch (error) {
144
+ LoggerProxy.logger.warn(
145
+ `Meeting:util#getSocketUrlInfo --> error getting socket URL info: ${error}`
146
+ );
147
+
148
+ return {
149
+ hasMismatchedSocket: false,
150
+ mercurySocketUrl: '',
151
+ deviceSocketUrl: '',
152
+ };
153
+ }
154
+ },
155
+
50
156
  remoteUpdateAudioVideo: (meeting, audioMuted?: boolean, videoMuted?: boolean) => {
51
157
  if (!meeting) {
52
158
  return Promise.reject(new ParameterError('You need a meeting object.'));
@@ -203,6 +309,7 @@ const MeetingUtil = {
203
309
  const parsed = MeetingUtil.parseLocusJoin(res);
204
310
  meeting.setLocus(parsed);
205
311
  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
312
+ const socketUrlInfo = MeetingUtil.getSocketUrlInfo(webex);
206
313
  webex.internal.newMetrics.submitClientEvent({
207
314
  name: 'client.locus.join.response',
208
315
  payload: {
@@ -210,6 +317,9 @@ const MeetingUtil = {
210
317
  identifiers: {
211
318
  trackingId: res.headers.trackingid,
212
319
  },
320
+ eventData: {
321
+ ...socketUrlInfo,
322
+ },
213
323
  },
214
324
  options: {
215
325
  meetingId: meeting.id,
@@ -220,12 +330,19 @@ const MeetingUtil = {
220
330
  return parsed;
221
331
  })
222
332
  .catch((err) => {
333
+ const socketUrlInfo = MeetingUtil.getSocketUrlInfo(webex);
223
334
  webex.internal.newMetrics.submitClientEvent({
224
335
  name: 'client.locus.join.response',
225
336
  payload: {
226
337
  identifiers: {meetingLookupUrl: meeting.meetingInfo?.meetingLookupUrl},
338
+ eventData: {
339
+ ...socketUrlInfo,
340
+ },
341
+ },
342
+ options: {
343
+ meetingId: meeting.id,
344
+ rawError: err,
227
345
  },
228
- options: {meetingId: meeting.id, rawError: err},
229
346
  });
230
347
 
231
348
  throw err;
@@ -1047,11 +1047,7 @@ export default class Meetings extends WebexPlugin {
1047
1047
  return (
1048
1048
  // @ts-ignore
1049
1049
  this.webex.internal.mercury
1050
- // Use code 3050 with a non-reconnecting reason to prevent Mercury auto-reconnect
1051
- // during unregister. Without this, disconnect() defaults to code 1000/"Done" which
1052
- // force-closes as "Done (forced)" - a normalReconnectReason that triggers auto-reconnect,
1053
- // causing a race condition with device.unregister().
1054
- .disconnect({code: 3050, reason: 'meetings unregister'})
1050
+ .disconnect()
1055
1051
  // @ts-ignore
1056
1052
  .then(() => this.webex.internal.device.unregister())
1057
1053
  .catch((error) => {
@@ -61,8 +61,9 @@ describe('plugin-meetings', () => {
61
61
  meeting.trigger = sinon.stub();
62
62
  meeting.webex = webex;
63
63
  meeting.webex.internal.newMetrics.callDiagnosticMetrics =
64
- meeting.webex.internal.newMetrics.callDiagnosticMetrics || {};
65
- meeting.webex.internal.newMetrics.callDiagnosticMetrics.clearEventLimitsForCorrelationId = sinon.stub();
64
+ meeting.webex.internal.newMetrics.callDiagnosticMetrics || {};
65
+ meeting.webex.internal.newMetrics.callDiagnosticMetrics.clearEventLimitsForCorrelationId =
66
+ sinon.stub();
66
67
  });
67
68
 
68
69
  afterEach(() => {
@@ -245,7 +246,11 @@ describe('plugin-meetings', () => {
245
246
  const response = MeetingUtil.updateLocusFromApiResponse(meeting, originalResponse);
246
247
 
247
248
  assert.deepEqual(response, originalResponse);
248
- assert.calledOnceWithExactly(meeting.locusInfo.handleLocusAPIResponse, meeting, originalResponse.body);
249
+ assert.calledOnceWithExactly(
250
+ meeting.locusInfo.handleLocusAPIResponse,
251
+ meeting,
252
+ originalResponse.body
253
+ );
249
254
  });
250
255
 
251
256
  it('should handle locus being missing from the response', () => {
@@ -361,8 +366,8 @@ describe('plugin-meetings', () => {
361
366
  describe('remoteUpdateAudioVideo', () => {
362
367
  it('#Should call meetingRequest.locusMediaRequest with correct parameters and return the full response', async () => {
363
368
  const fakeResponse = {
364
- body: { locus: { url: 'locusUrl'}},
365
- headers: { },
369
+ body: {locus: {url: 'locusUrl'}},
370
+ headers: {},
366
371
  };
367
372
  const meeting = {
368
373
  id: 'meeting-id',
@@ -480,6 +485,11 @@ describe('plugin-meetings', () => {
480
485
  identifiers: {
481
486
  trackingId: 'trackingId',
482
487
  },
488
+ eventData: {
489
+ hasMismatchedSocket: false,
490
+ mercurySocketUrl: '',
491
+ deviceSocketUrl: 'ws://example.com',
492
+ },
483
493
  },
484
494
  options: {
485
495
  meetingId: meeting.id,
@@ -649,21 +659,26 @@ describe('plugin-meetings', () => {
649
659
  it('should post client event with error when join fails', async () => {
650
660
  const joinError = new Error('Join failed');
651
661
  meeting.meetingRequest.joinMeeting.rejects(joinError);
652
- meeting.meetingInfo = { meetingLookupUrl: 'test-lookup-url' };
662
+ meeting.meetingInfo = {meetingLookupUrl: 'test-lookup-url'};
653
663
 
654
664
  try {
655
665
  await MeetingUtil.joinMeeting(meeting, {});
656
666
  assert.fail('Expected joinMeeting to throw an error');
657
667
  } catch (error) {
658
668
  assert.equal(error, joinError);
659
-
669
+
660
670
  // Verify error client event was submitted
661
671
  assert.calledWith(webex.internal.newMetrics.submitClientEvent, {
662
672
  name: 'client.locus.join.response',
663
673
  payload: {
664
- identifiers: { meetingLookupUrl: 'test-lookup-url' },
674
+ identifiers: {meetingLookupUrl: 'test-lookup-url'},
675
+ eventData: {
676
+ hasMismatchedSocket: false,
677
+ mercurySocketUrl: '',
678
+ deviceSocketUrl: 'ws://example.com',
679
+ },
665
680
  },
666
- options: { meetingId: meeting.id, rawError: joinError },
681
+ options: {meetingId: meeting.id, rawError: joinError},
667
682
  });
668
683
  }
669
684
  });
@@ -721,7 +736,7 @@ describe('plugin-meetings', () => {
721
736
  assert.fail('Expected joinMeetingOptions to throw PasswordError');
722
737
  } catch (error) {
723
738
  assert.instanceOf(error, PasswordError);
724
-
739
+
725
740
  // Verify client event was submitted with error details
726
741
  assert.calledWith(webex.internal.newMetrics.submitClientEvent, {
727
742
  name: 'client.meetinginfo.response',
@@ -759,7 +774,7 @@ describe('plugin-meetings', () => {
759
774
  assert.fail('Expected joinMeetingOptions to throw CaptchaError');
760
775
  } catch (error) {
761
776
  assert.instanceOf(error, CaptchaError);
762
-
777
+
763
778
  // Verify client event was submitted with error details
764
779
  assert.calledWith(webex.internal.newMetrics.submitClientEvent, {
765
780
  name: 'client.meetinginfo.response',
@@ -972,15 +987,18 @@ describe('plugin-meetings', () => {
972
987
  {functionName: 'canStartManualCaption', displayHint: 'MANUAL_CAPTION_START'},
973
988
  {functionName: 'canStopManualCaption', displayHint: 'MANUAL_CAPTION_STOP'},
974
989
 
975
- {functionName: 'isLocalRecordingStarted',displayHint:'LOCAL_RECORDING_STATUS_STARTED'},
990
+ {functionName: 'isLocalRecordingStarted', displayHint: 'LOCAL_RECORDING_STATUS_STARTED'},
976
991
  {functionName: 'isLocalRecordingStopped', displayHint: 'LOCAL_RECORDING_STATUS_STOPPED'},
977
992
  {functionName: 'isLocalRecordingPaused', displayHint: 'LOCAL_RECORDING_STATUS_PAUSED'},
978
- {functionName: 'isLocalStreamingStarted',displayHint:'STREAMING_STATUS_STARTED'},
993
+ {functionName: 'isLocalStreamingStarted', displayHint: 'STREAMING_STATUS_STARTED'},
979
994
  {functionName: 'isLocalStreamingStopped', displayHint: 'STREAMING_STATUS_STOPPED'},
980
995
 
981
996
  {functionName: 'isManualCaptionActive', displayHint: 'MANUAL_CAPTION_STATUS_ACTIVE'},
982
997
 
983
- {functionName: 'isSpokenLanguageAutoDetectionEnabled', displayHint: 'SPOKEN_LANGUAGE_AUTO_DETECTION_ENABLED'},
998
+ {
999
+ functionName: 'isSpokenLanguageAutoDetectionEnabled',
1000
+ displayHint: 'SPOKEN_LANGUAGE_AUTO_DETECTION_ENABLED',
1001
+ },
984
1002
 
985
1003
  {functionName: 'isWebexAssistantActive', displayHint: 'WEBEX_ASSISTANT_STATUS_ACTIVE'},
986
1004
  {functionName: 'canViewCaptionPanel', displayHint: 'ENABLE_CAPTION_PANEL'},
@@ -1447,10 +1465,7 @@ describe('plugin-meetings', () => {
1447
1465
  },
1448
1466
  },
1449
1467
  dataSets: [{name: 'dataset1', url: 'http://dataset.com'}],
1450
- mediaConnections: [
1451
- {mediaId: 'mediaId456'},
1452
- {someOtherField: 'value'},
1453
- ],
1468
+ mediaConnections: [{mediaId: 'mediaId456'}, {someOtherField: 'value'}],
1454
1469
  },
1455
1470
  };
1456
1471
  });
@@ -1500,15 +1515,249 @@ describe('plugin-meetings', () => {
1500
1515
  });
1501
1516
 
1502
1517
  it('handles mediaConnections without mediaId', () => {
1503
- response.body.mediaConnections = [
1504
- {someField: 'value1'},
1505
- {anotherField: 'value2'},
1506
- ];
1518
+ response.body.mediaConnections = [{someField: 'value1'}, {anotherField: 'value2'}];
1507
1519
 
1508
1520
  const result = MeetingUtil.parseLocusJoin(response);
1509
1521
 
1510
1522
  assert.isUndefined(result.mediaId);
1511
1523
  });
1512
1524
  });
1525
+
1526
+ describe('#sanitizeWebSocketUrl', () => {
1527
+ it('extracts protocol, host, and pathname from URL', () => {
1528
+ const url = 'wss://example.com:443/mercury/path?token=secret&key=value#fragment';
1529
+ const result = MeetingUtil.sanitizeWebSocketUrl(url);
1530
+
1531
+ assert.equal(result, 'wss://example.com:443/mercury/path');
1532
+ });
1533
+
1534
+ it('handles URL without query string or hash', () => {
1535
+ const url = 'wss://example.com/path';
1536
+ const result = MeetingUtil.sanitizeWebSocketUrl(url);
1537
+
1538
+ assert.equal(result, 'wss://example.com/path');
1539
+ });
1540
+
1541
+ it('removes authentication from URL', () => {
1542
+ const url = 'wss://user:password@example.com/path?token=secret';
1543
+ const result = MeetingUtil.sanitizeWebSocketUrl(url);
1544
+
1545
+ assert.equal(result, 'wss://example.com/path');
1546
+ });
1547
+
1548
+ it('returns empty string for null or undefined', () => {
1549
+ assert.equal(MeetingUtil.sanitizeWebSocketUrl(null), '');
1550
+ assert.equal(MeetingUtil.sanitizeWebSocketUrl(undefined), '');
1551
+ });
1552
+
1553
+ it('returns empty string for non-string input', () => {
1554
+ assert.equal(MeetingUtil.sanitizeWebSocketUrl(123), '');
1555
+ assert.equal(MeetingUtil.sanitizeWebSocketUrl({}), '');
1556
+ });
1557
+
1558
+ it('returns empty string for invalid URL', () => {
1559
+ const result = MeetingUtil.sanitizeWebSocketUrl('not a valid url');
1560
+
1561
+ assert.equal(result, '');
1562
+ });
1563
+
1564
+ it('handles URL without pathname', () => {
1565
+ const url = 'wss://example.com?query=value';
1566
+ const result = MeetingUtil.sanitizeWebSocketUrl(url);
1567
+
1568
+ assert.equal(result, 'wss://example.com');
1569
+ });
1570
+ });
1571
+
1572
+ describe('#_urlsMatch', () => {
1573
+ it('returns true when URLs match (ignoring query and hash)', () => {
1574
+ const url1 = 'wss://example.com:443/path?token=abc#fragment1';
1575
+ const url2 = 'wss://example.com:443/path?token=xyz#fragment2';
1576
+
1577
+ assert.isTrue(MeetingUtil._urlsMatch(url1, url2));
1578
+ });
1579
+
1580
+ it('returns false when protocols differ', () => {
1581
+ const url1 = 'wss://example.com/path';
1582
+ const url2 = 'ws://example.com/path';
1583
+
1584
+ assert.isFalse(MeetingUtil._urlsMatch(url1, url2));
1585
+ });
1586
+
1587
+ it('returns false when hosts differ', () => {
1588
+ const url1 = 'wss://example1.com/path';
1589
+ const url2 = 'wss://example2.com/path';
1590
+
1591
+ assert.isFalse(MeetingUtil._urlsMatch(url1, url2));
1592
+ });
1593
+
1594
+ it('returns false when ports differ', () => {
1595
+ const url1 = 'wss://example.com:443/path';
1596
+ const url2 = 'wss://example.com:8443/path';
1597
+
1598
+ assert.isFalse(MeetingUtil._urlsMatch(url1, url2));
1599
+ });
1600
+
1601
+ it('returns false when pathnames differ', () => {
1602
+ const url1 = 'wss://example.com/path1';
1603
+ const url2 = 'wss://example.com/path2';
1604
+
1605
+ assert.isFalse(MeetingUtil._urlsMatch(url1, url2));
1606
+ });
1607
+
1608
+ it('returns false when either URL is null or undefined', () => {
1609
+ const url = 'wss://example.com/path';
1610
+
1611
+ assert.isFalse(MeetingUtil._urlsMatch(null, url));
1612
+ assert.isFalse(MeetingUtil._urlsMatch(url, null));
1613
+ assert.isFalse(MeetingUtil._urlsMatch(undefined, url));
1614
+ assert.isFalse(MeetingUtil._urlsMatch(url, undefined));
1615
+ });
1616
+
1617
+ it('returns false when both URLs are null', () => {
1618
+ assert.isFalse(MeetingUtil._urlsMatch(null, null));
1619
+ });
1620
+
1621
+ it('returns false when URL parsing fails', () => {
1622
+ const url1 = 'invalid url';
1623
+ const url2 = 'wss://example.com/path';
1624
+
1625
+ assert.isFalse(MeetingUtil._urlsMatch(url1, url2));
1626
+ });
1627
+
1628
+ it('compares URLs case-sensitively', () => {
1629
+ const url1 = 'wss://Example.com/path';
1630
+ const url2 = 'wss://example.com/path';
1631
+
1632
+ // URL parsing should handle host case-sensitivity
1633
+ assert.isTrue(MeetingUtil._urlsMatch(url1, url2));
1634
+ });
1635
+ });
1636
+
1637
+ describe('#getSocketUrlInfo', () => {
1638
+ it('returns socket URL info when URLs differ', () => {
1639
+ const testWebex = {
1640
+ internal: {
1641
+ mercury: {
1642
+ socket: {
1643
+ url: 'wss://mercury.example.com:443/path?token=abc',
1644
+ },
1645
+ },
1646
+ device: {
1647
+ webSocketUrl: 'wss://device.example.com:443/path?token=xyz',
1648
+ },
1649
+ },
1650
+ };
1651
+
1652
+ const result = MeetingUtil.getSocketUrlInfo(testWebex);
1653
+
1654
+ assert.isTrue(result.hasMismatchedSocket);
1655
+ assert.equal(result.mercurySocketUrl, 'wss://mercury.example.com:443/path');
1656
+ assert.equal(result.deviceSocketUrl, 'wss://device.example.com:443/path');
1657
+ });
1658
+
1659
+ it('returns socket URL info when URLs match', () => {
1660
+ const testWebex = {
1661
+ internal: {
1662
+ mercury: {
1663
+ socket: {
1664
+ url: 'wss://example.com:443/path?token=abc',
1665
+ },
1666
+ },
1667
+ device: {
1668
+ webSocketUrl: 'wss://example.com:443/path?token=xyz',
1669
+ },
1670
+ },
1671
+ };
1672
+
1673
+ const result = MeetingUtil.getSocketUrlInfo(testWebex);
1674
+
1675
+ assert.isFalse(result.hasMismatchedSocket);
1676
+ assert.equal(result.mercurySocketUrl, 'wss://example.com:443/path');
1677
+ assert.equal(result.deviceSocketUrl, 'wss://example.com:443/path');
1678
+ });
1679
+
1680
+ it('returns false for hasMismatchedSocket when mercury socket URL is missing', () => {
1681
+ const testWebex = {
1682
+ internal: {
1683
+ mercury: {
1684
+ socket: {},
1685
+ },
1686
+ device: {
1687
+ webSocketUrl: 'wss://device.example.com:443/path',
1688
+ },
1689
+ },
1690
+ };
1691
+
1692
+ const result = MeetingUtil.getSocketUrlInfo(testWebex);
1693
+
1694
+ assert.isFalse(result.hasMismatchedSocket);
1695
+ assert.equal(result.mercurySocketUrl, '');
1696
+ assert.equal(result.deviceSocketUrl, 'wss://device.example.com:443/path');
1697
+ });
1698
+
1699
+ it('returns false for hasMismatchedSocket when device socket URL is missing', () => {
1700
+ const testWebex = {
1701
+ internal: {
1702
+ mercury: {
1703
+ socket: {
1704
+ url: 'wss://mercury.example.com:443/path',
1705
+ },
1706
+ },
1707
+ device: {},
1708
+ },
1709
+ };
1710
+
1711
+ const result = MeetingUtil.getSocketUrlInfo(testWebex);
1712
+
1713
+ assert.isFalse(result.hasMismatchedSocket);
1714
+ assert.equal(result.mercurySocketUrl, 'wss://mercury.example.com:443/path');
1715
+ assert.equal(result.deviceSocketUrl, '');
1716
+ });
1717
+
1718
+ it('returns default values when webex object is missing properties', () => {
1719
+ const testWebex = {
1720
+ internal: {},
1721
+ };
1722
+
1723
+ const result = MeetingUtil.getSocketUrlInfo(testWebex);
1724
+
1725
+ assert.isFalse(result.hasMismatchedSocket);
1726
+ assert.equal(result.mercurySocketUrl, '');
1727
+ assert.equal(result.deviceSocketUrl, '');
1728
+ });
1729
+
1730
+ it('handles error gracefully and returns default values', () => {
1731
+ const testWebex = null;
1732
+
1733
+ const result = MeetingUtil.getSocketUrlInfo(testWebex);
1734
+
1735
+ assert.isFalse(result.hasMismatchedSocket);
1736
+ assert.equal(result.mercurySocketUrl, '');
1737
+ assert.equal(result.deviceSocketUrl, '');
1738
+ });
1739
+
1740
+ it('sanitizes URLs by removing query parameters', () => {
1741
+ const testWebex = {
1742
+ internal: {
1743
+ mercury: {
1744
+ socket: {
1745
+ url: 'wss://mercury.example.com/path?secret=token123&key=value',
1746
+ },
1747
+ },
1748
+ device: {
1749
+ webSocketUrl: 'wss://device.example.com/path?secret=differenttoken&key=value',
1750
+ },
1751
+ },
1752
+ };
1753
+
1754
+ const result = MeetingUtil.getSocketUrlInfo(testWebex);
1755
+
1756
+ assert.notInclude(result.mercurySocketUrl, 'secret');
1757
+ assert.notInclude(result.mercurySocketUrl, 'token123');
1758
+ assert.notInclude(result.deviceSocketUrl, 'secret');
1759
+ assert.notInclude(result.deviceSocketUrl, 'differenttoken');
1760
+ });
1761
+ });
1513
1762
  });
1514
1763
  });