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

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 (146) hide show
  1. package/dist/aiEnableRequest/index.js +184 -0
  2. package/dist/aiEnableRequest/index.js.map +1 -0
  3. package/dist/aiEnableRequest/utils.js +36 -0
  4. package/dist/aiEnableRequest/utils.js.map +1 -0
  5. package/dist/annotation/index.js +3 -3
  6. package/dist/annotation/index.js.map +1 -1
  7. package/dist/breakouts/breakout.js +1 -1
  8. package/dist/breakouts/index.js +1 -1
  9. package/dist/config.js +5 -1
  10. package/dist/config.js.map +1 -1
  11. package/dist/constants.js +26 -6
  12. package/dist/constants.js.map +1 -1
  13. package/dist/hashTree/constants.js +3 -1
  14. package/dist/hashTree/constants.js.map +1 -1
  15. package/dist/hashTree/hashTree.js +18 -0
  16. package/dist/hashTree/hashTree.js.map +1 -1
  17. package/dist/hashTree/hashTreeParser.js +709 -380
  18. package/dist/hashTree/hashTreeParser.js.map +1 -1
  19. package/dist/hashTree/types.js +4 -2
  20. package/dist/hashTree/types.js.map +1 -1
  21. package/dist/hashTree/utils.js +10 -0
  22. package/dist/hashTree/utils.js.map +1 -1
  23. package/dist/index.js +11 -2
  24. package/dist/index.js.map +1 -1
  25. package/dist/interceptors/constant.js +12 -0
  26. package/dist/interceptors/constant.js.map +1 -0
  27. package/dist/interceptors/dataChannelAuthToken.js +233 -0
  28. package/dist/interceptors/dataChannelAuthToken.js.map +1 -0
  29. package/dist/interceptors/index.js +7 -0
  30. package/dist/interceptors/index.js.map +1 -1
  31. package/dist/interpretation/index.js +2 -2
  32. package/dist/interpretation/index.js.map +1 -1
  33. package/dist/interpretation/siLanguage.js +1 -1
  34. package/dist/locus-info/controlsUtils.js +5 -3
  35. package/dist/locus-info/controlsUtils.js.map +1 -1
  36. package/dist/locus-info/index.js +125 -68
  37. package/dist/locus-info/index.js.map +1 -1
  38. package/dist/locus-info/selfUtils.js +1 -0
  39. package/dist/locus-info/selfUtils.js.map +1 -1
  40. package/dist/locus-info/types.js.map +1 -1
  41. package/dist/media/MediaConnectionAwaiter.js +57 -1
  42. package/dist/media/MediaConnectionAwaiter.js.map +1 -1
  43. package/dist/media/properties.js +4 -2
  44. package/dist/media/properties.js.map +1 -1
  45. package/dist/meeting/in-meeting-actions.js +7 -1
  46. package/dist/meeting/in-meeting-actions.js.map +1 -1
  47. package/dist/meeting/index.js +209 -90
  48. package/dist/meeting/index.js.map +1 -1
  49. package/dist/meeting/request.js +50 -0
  50. package/dist/meeting/request.js.map +1 -1
  51. package/dist/meeting/request.type.js.map +1 -1
  52. package/dist/meeting/util.js +128 -2
  53. package/dist/meeting/util.js.map +1 -1
  54. package/dist/meetings/index.js +78 -36
  55. package/dist/meetings/index.js.map +1 -1
  56. package/dist/member/index.js +10 -0
  57. package/dist/member/index.js.map +1 -1
  58. package/dist/member/util.js +10 -0
  59. package/dist/member/util.js.map +1 -1
  60. package/dist/metrics/constants.js +2 -1
  61. package/dist/metrics/constants.js.map +1 -1
  62. package/dist/multistream/mediaRequestManager.js +1 -1
  63. package/dist/multistream/mediaRequestManager.js.map +1 -1
  64. package/dist/multistream/remoteMediaManager.js +11 -0
  65. package/dist/multistream/remoteMediaManager.js.map +1 -1
  66. package/dist/reactions/reactions.type.js.map +1 -1
  67. package/dist/types/aiEnableRequest/index.d.ts +5 -0
  68. package/dist/types/aiEnableRequest/utils.d.ts +2 -0
  69. package/dist/types/config.d.ts +3 -0
  70. package/dist/types/constants.d.ts +21 -1
  71. package/dist/types/hashTree/constants.d.ts +1 -0
  72. package/dist/types/hashTree/hashTree.d.ts +7 -0
  73. package/dist/types/hashTree/hashTreeParser.d.ts +99 -14
  74. package/dist/types/hashTree/types.d.ts +3 -0
  75. package/dist/types/hashTree/utils.d.ts +6 -0
  76. package/dist/types/index.d.ts +1 -0
  77. package/dist/types/interceptors/constant.d.ts +5 -0
  78. package/dist/types/interceptors/dataChannelAuthToken.d.ts +35 -0
  79. package/dist/types/interceptors/index.d.ts +2 -1
  80. package/dist/types/locus-info/index.d.ts +9 -2
  81. package/dist/types/locus-info/types.d.ts +1 -0
  82. package/dist/types/media/MediaConnectionAwaiter.d.ts +10 -1
  83. package/dist/types/media/properties.d.ts +2 -1
  84. package/dist/types/meeting/in-meeting-actions.d.ts +6 -0
  85. package/dist/types/meeting/index.d.ts +24 -2
  86. package/dist/types/meeting/request.d.ts +16 -1
  87. package/dist/types/meeting/request.type.d.ts +5 -0
  88. package/dist/types/meeting/util.d.ts +31 -0
  89. package/dist/types/meetings/index.d.ts +4 -2
  90. package/dist/types/member/index.d.ts +1 -0
  91. package/dist/types/member/util.d.ts +5 -0
  92. package/dist/types/metrics/constants.d.ts +1 -0
  93. package/dist/types/reactions/reactions.type.d.ts +1 -0
  94. package/dist/webinar/index.js +1 -1
  95. package/package.json +22 -22
  96. package/src/aiEnableRequest/README.md +84 -0
  97. package/src/aiEnableRequest/index.ts +170 -0
  98. package/src/aiEnableRequest/utils.ts +25 -0
  99. package/src/annotation/index.ts +7 -4
  100. package/src/config.ts +3 -0
  101. package/src/constants.ts +26 -1
  102. package/src/hashTree/constants.ts +1 -0
  103. package/src/hashTree/hashTree.ts +17 -0
  104. package/src/hashTree/hashTreeParser.ts +627 -249
  105. package/src/hashTree/types.ts +4 -0
  106. package/src/hashTree/utils.ts +9 -0
  107. package/src/index.ts +8 -1
  108. package/src/interceptors/constant.ts +6 -0
  109. package/src/interceptors/dataChannelAuthToken.ts +142 -0
  110. package/src/interceptors/index.ts +2 -1
  111. package/src/interpretation/index.ts +2 -2
  112. package/src/locus-info/controlsUtils.ts +11 -0
  113. package/src/locus-info/index.ts +146 -58
  114. package/src/locus-info/selfUtils.ts +1 -0
  115. package/src/locus-info/types.ts +1 -0
  116. package/src/media/MediaConnectionAwaiter.ts +41 -1
  117. package/src/media/properties.ts +3 -1
  118. package/src/meeting/in-meeting-actions.ts +12 -0
  119. package/src/meeting/index.ts +127 -17
  120. package/src/meeting/request.ts +42 -0
  121. package/src/meeting/request.type.ts +6 -0
  122. package/src/meeting/util.ts +156 -1
  123. package/src/meetings/index.ts +94 -9
  124. package/src/member/index.ts +10 -0
  125. package/src/member/util.ts +12 -0
  126. package/src/metrics/constants.ts +1 -0
  127. package/src/multistream/mediaRequestManager.ts +1 -1
  128. package/src/multistream/remoteMediaManager.ts +13 -0
  129. package/src/reactions/reactions.type.ts +1 -0
  130. package/test/unit/spec/aiEnableRequest/index.ts +981 -0
  131. package/test/unit/spec/aiEnableRequest/utils.ts +130 -0
  132. package/test/unit/spec/hashTree/hashTree.ts +66 -0
  133. package/test/unit/spec/hashTree/hashTreeParser.ts +1869 -189
  134. package/test/unit/spec/interceptors/dataChannelAuthToken.ts +141 -0
  135. package/test/unit/spec/locus-info/controlsUtils.js +29 -0
  136. package/test/unit/spec/locus-info/index.js +201 -45
  137. package/test/unit/spec/media/MediaConnectionAwaiter.ts +41 -1
  138. package/test/unit/spec/media/properties.ts +12 -3
  139. package/test/unit/spec/meeting/in-meeting-actions.ts +8 -2
  140. package/test/unit/spec/meeting/index.js +441 -75
  141. package/test/unit/spec/meeting/request.js +64 -0
  142. package/test/unit/spec/meeting/utils.js +433 -22
  143. package/test/unit/spec/meetings/index.js +550 -10
  144. package/test/unit/spec/member/index.js +28 -4
  145. package/test/unit/spec/member/util.js +65 -27
  146. package/test/unit/spec/multistream/remoteMediaManager.ts +30 -0
@@ -115,6 +115,7 @@ describe('plugin-meetings', () => {
115
115
  });
116
116
 
117
117
  describe('#changeVideoLayout', () => {
118
+
118
119
  const locusUrl = 'locusURL';
119
120
  const deviceUrl = 'deviceUrl';
120
121
  const layoutType = 'Equal';
@@ -918,4 +919,67 @@ describe('plugin-meetings', () => {
918
919
  });
919
920
  });
920
921
  });
922
+
923
+ describe('#fetchDatachannelToken', () => {
924
+ const locusUrl = 'https://locus.example.com/locus/api/v1/loci/123';
925
+ const participantId = 'participant-123';
926
+
927
+ it('sends GET request to regular datachannel token endpoint', async () => {
928
+ await meetingsRequest.fetchDatachannelToken({
929
+ locusUrl,
930
+ requestingParticipantId: participantId,
931
+ isPracticeSession: false,
932
+ });
933
+
934
+ assert.calledOnceWithExactly(locusDeltaRequestSpy, {
935
+ method: 'GET',
936
+ uri: `${locusUrl}/participant/${participantId}/datachannel/token`,
937
+ });
938
+ });
939
+
940
+ it('sends GET request to practice session datachannel token endpoint', async () => {
941
+ await meetingsRequest.fetchDatachannelToken({
942
+ locusUrl,
943
+ requestingParticipantId: participantId,
944
+ isPracticeSession: true,
945
+ });
946
+
947
+ assert.calledOnceWithExactly(locusDeltaRequestSpy, {
948
+ method: 'GET',
949
+ uri: `${locusUrl}/participant/${participantId}/practiceSession/datachannel/token`,
950
+ });
951
+ });
952
+
953
+ it('throws if locusUrl or participantId is missing', async () => {
954
+ await assert.isRejected(
955
+ meetingsRequest.fetchDatachannelToken({
956
+ locusUrl: null,
957
+ requestingParticipantId: participantId,
958
+ }),
959
+ /locusUrl and participantId are required/
960
+ );
961
+
962
+ await assert.isRejected(
963
+ meetingsRequest.fetchDatachannelToken({
964
+ locusUrl,
965
+ requestingParticipantId: null,
966
+ }),
967
+ /locusUrl and participantId are required/
968
+ );
969
+ });
970
+
971
+ it('logs and rethrows error when locusDeltaRequest fails', async () => {
972
+ const error = new Error('network error');
973
+ locusDeltaRequestSpy.restore();
974
+ sinon.stub(meetingsRequest, 'locusDeltaRequest').rejects(error);
975
+
976
+ await assert.isRejected(
977
+ meetingsRequest.fetchDatachannelToken({
978
+ locusUrl,
979
+ requestingParticipantId: participantId,
980
+ }),
981
+ /network error/
982
+ );
983
+ });
984
+ });
921
985
  });
@@ -11,6 +11,7 @@ import MockWebex from '@webex/test-helper-mock-webex';
11
11
  import * as BrowserDetectionModule from '@webex/plugin-meetings/src/common/browser-detection';
12
12
  import PasswordError from '@webex/plugin-meetings/src/common/errors/password-error';
13
13
  import CaptchaError from '@webex/plugin-meetings/src/common/errors/captcha-error';
14
+ import {ServerRoles} from '@webex/plugin-meetings/src/member/types';
14
15
 
15
16
  describe('plugin-meetings', () => {
16
17
  let webex;
@@ -61,8 +62,9 @@ describe('plugin-meetings', () => {
61
62
  meeting.trigger = sinon.stub();
62
63
  meeting.webex = webex;
63
64
  meeting.webex.internal.newMetrics.callDiagnosticMetrics =
64
- meeting.webex.internal.newMetrics.callDiagnosticMetrics || {};
65
- meeting.webex.internal.newMetrics.callDiagnosticMetrics.clearEventLimitsForCorrelationId = sinon.stub();
65
+ meeting.webex.internal.newMetrics.callDiagnosticMetrics || {};
66
+ meeting.webex.internal.newMetrics.callDiagnosticMetrics.clearEventLimitsForCorrelationId =
67
+ sinon.stub();
66
68
  });
67
69
 
68
70
  afterEach(() => {
@@ -245,7 +247,11 @@ describe('plugin-meetings', () => {
245
247
  const response = MeetingUtil.updateLocusFromApiResponse(meeting, originalResponse);
246
248
 
247
249
  assert.deepEqual(response, originalResponse);
248
- assert.calledOnceWithExactly(meeting.locusInfo.handleLocusAPIResponse, meeting, originalResponse.body);
250
+ assert.calledOnceWithExactly(
251
+ meeting.locusInfo.handleLocusAPIResponse,
252
+ meeting,
253
+ originalResponse.body
254
+ );
249
255
  });
250
256
 
251
257
  it('should handle locus being missing from the response', () => {
@@ -361,8 +367,8 @@ describe('plugin-meetings', () => {
361
367
  describe('remoteUpdateAudioVideo', () => {
362
368
  it('#Should call meetingRequest.locusMediaRequest with correct parameters and return the full response', async () => {
363
369
  const fakeResponse = {
364
- body: { locus: { url: 'locusUrl'}},
365
- headers: { },
370
+ body: {locus: {url: 'locusUrl'}},
371
+ headers: {},
366
372
  };
367
373
  const meeting = {
368
374
  id: 'meeting-id',
@@ -480,6 +486,11 @@ describe('plugin-meetings', () => {
480
486
  identifiers: {
481
487
  trackingId: 'trackingId',
482
488
  },
489
+ eventData: {
490
+ hasMismatchedSocket: false,
491
+ mercurySocketUrl: '',
492
+ deviceSocketUrl: 'ws://example.com',
493
+ },
483
494
  },
484
495
  options: {
485
496
  meetingId: meeting.id,
@@ -649,21 +660,26 @@ describe('plugin-meetings', () => {
649
660
  it('should post client event with error when join fails', async () => {
650
661
  const joinError = new Error('Join failed');
651
662
  meeting.meetingRequest.joinMeeting.rejects(joinError);
652
- meeting.meetingInfo = { meetingLookupUrl: 'test-lookup-url' };
663
+ meeting.meetingInfo = {meetingLookupUrl: 'test-lookup-url'};
653
664
 
654
665
  try {
655
666
  await MeetingUtil.joinMeeting(meeting, {});
656
667
  assert.fail('Expected joinMeeting to throw an error');
657
668
  } catch (error) {
658
669
  assert.equal(error, joinError);
659
-
670
+
660
671
  // Verify error client event was submitted
661
672
  assert.calledWith(webex.internal.newMetrics.submitClientEvent, {
662
673
  name: 'client.locus.join.response',
663
674
  payload: {
664
- identifiers: { meetingLookupUrl: 'test-lookup-url' },
675
+ identifiers: {meetingLookupUrl: 'test-lookup-url'},
676
+ eventData: {
677
+ hasMismatchedSocket: false,
678
+ mercurySocketUrl: '',
679
+ deviceSocketUrl: 'ws://example.com',
680
+ },
665
681
  },
666
- options: { meetingId: meeting.id, rawError: joinError },
682
+ options: {meetingId: meeting.id, rawError: joinError},
667
683
  });
668
684
  }
669
685
  });
@@ -721,7 +737,7 @@ describe('plugin-meetings', () => {
721
737
  assert.fail('Expected joinMeetingOptions to throw PasswordError');
722
738
  } catch (error) {
723
739
  assert.instanceOf(error, PasswordError);
724
-
740
+
725
741
  // Verify client event was submitted with error details
726
742
  assert.calledWith(webex.internal.newMetrics.submitClientEvent, {
727
743
  name: 'client.meetinginfo.response',
@@ -759,7 +775,7 @@ describe('plugin-meetings', () => {
759
775
  assert.fail('Expected joinMeetingOptions to throw CaptchaError');
760
776
  } catch (error) {
761
777
  assert.instanceOf(error, CaptchaError);
762
-
778
+
763
779
  // Verify client event was submitted with error details
764
780
  assert.calledWith(webex.internal.newMetrics.submitClientEvent, {
765
781
  name: 'client.meetinginfo.response',
@@ -921,6 +937,104 @@ describe('plugin-meetings', () => {
921
937
  });
922
938
  });
923
939
 
940
+ describe('canAttendeeRequestAiAssistantEnabled', () => {
941
+ it('returns false when user is a cohost', () => {
942
+ assert.deepEqual(
943
+ MeetingUtil.canAttendeeRequestAiAssistantEnabled(
944
+ ['ATTENDEE_REQUEST_AI_ASSISTANT_ENABLED'],
945
+ [ServerRoles.Cohost]
946
+ ),
947
+ false
948
+ );
949
+ });
950
+
951
+ it('returns false when user is a moderator', () => {
952
+ assert.deepEqual(
953
+ MeetingUtil.canAttendeeRequestAiAssistantEnabled(
954
+ ['ATTENDEE_REQUEST_AI_ASSISTANT_ENABLED'],
955
+ [ServerRoles.Moderator]
956
+ ),
957
+ false
958
+ );
959
+ });
960
+
961
+ it('returns false when user is both cohost and moderator', () => {
962
+ assert.deepEqual(
963
+ MeetingUtil.canAttendeeRequestAiAssistantEnabled(
964
+ ['ATTENDEE_REQUEST_AI_ASSISTANT_ENABLED'],
965
+ [ServerRoles.Cohost, ServerRoles.Moderator]
966
+ ),
967
+ false
968
+ );
969
+ });
970
+
971
+ it('returns true when user is an attendee and display hint is present', () => {
972
+ assert.deepEqual(
973
+ MeetingUtil.canAttendeeRequestAiAssistantEnabled(
974
+ ['ATTENDEE_REQUEST_AI_ASSISTANT_ENABLED'],
975
+ []
976
+ ),
977
+ true
978
+ );
979
+ });
980
+
981
+ it('returns true when user has other roles (not host/cohost) and display hint is present', () => {
982
+ assert.deepEqual(
983
+ MeetingUtil.canAttendeeRequestAiAssistantEnabled(
984
+ ['ATTENDEE_REQUEST_AI_ASSISTANT_ENABLED'],
985
+ ['SomeOtherRole']
986
+ ),
987
+ true
988
+ );
989
+ });
990
+
991
+ it('returns false when user is an attendee but display hint is not present', () => {
992
+ assert.deepEqual(MeetingUtil.canAttendeeRequestAiAssistantEnabled([], []), false);
993
+ });
994
+
995
+ it('returns false when user is an attendee with other display hints but not the AI assistant one', () => {
996
+ assert.deepEqual(
997
+ MeetingUtil.canAttendeeRequestAiAssistantEnabled(['SOME_OTHER_HINT', 'ANOTHER_HINT'], []),
998
+ false
999
+ );
1000
+ });
1001
+
1002
+ it('returns false when host/cohost even if display hint is not present', () => {
1003
+ assert.deepEqual(
1004
+ MeetingUtil.canAttendeeRequestAiAssistantEnabled([], [ServerRoles.Cohost]),
1005
+ false
1006
+ );
1007
+ assert.deepEqual(
1008
+ MeetingUtil.canAttendeeRequestAiAssistantEnabled([], [ServerRoles.Moderator]),
1009
+ false
1010
+ );
1011
+ });
1012
+ });
1013
+
1014
+ describe('attendeeRequestAiAssistantDeclinedAll', () => {
1015
+ it('returns true when display hint is present', () => {
1016
+ assert.isTrue(
1017
+ MeetingUtil.attendeeRequestAiAssistantDeclinedAll([
1018
+ 'ATTENDEE_REQUEST_AI_ASSISTANT_DECLINED_ALL',
1019
+ ])
1020
+ );
1021
+ });
1022
+
1023
+ it('returns false when display hint is not present', () => {
1024
+ assert.isFalse(MeetingUtil.attendeeRequestAiAssistantDeclinedAll([]));
1025
+ });
1026
+
1027
+ it('returns false when display hint is absent among other hints', () => {
1028
+ assert.isFalse(
1029
+ MeetingUtil.attendeeRequestAiAssistantDeclinedAll(['SOME_OTHER_HINT', 'ANOTHER_HINT'])
1030
+ );
1031
+ });
1032
+
1033
+ it('returns false when called with no arguments', () => {
1034
+ assert.isFalse(MeetingUtil.attendeeRequestAiAssistantDeclinedAll());
1035
+ });
1036
+ });
1037
+
924
1038
  describe('bothLeaveAndEndMeetingAvailable', () => {
925
1039
  it('works as expected', () => {
926
1040
  assert.deepEqual(
@@ -939,6 +1053,46 @@ describe('plugin-meetings', () => {
939
1053
  });
940
1054
  });
941
1055
 
1056
+ describe('requireHostEndMeetingBeforeLeave', () => {
1057
+ it('works as expected', () => {
1058
+ assert.deepEqual(
1059
+ MeetingUtil.requireHostEndMeetingBeforeLeave(['REQUIRE_HOST_END_MEETING_BEFORE_LEAVE']),
1060
+ true
1061
+ );
1062
+ assert.deepEqual(
1063
+ MeetingUtil.requireHostEndMeetingBeforeLeave([
1064
+ 'LEAVE_TRANSFER_HOST_END_MEETING',
1065
+ 'END_MEETING',
1066
+ ]),
1067
+ false
1068
+ );
1069
+ assert.deepEqual(
1070
+ MeetingUtil.requireHostEndMeetingBeforeLeave([
1071
+ 'REQUIRE_HOST_END_MEETING_BEFORE_LEAVE',
1072
+ 'END_MEETING',
1073
+ ]),
1074
+ true
1075
+ );
1076
+ assert.deepEqual(
1077
+ MeetingUtil.requireHostEndMeetingBeforeLeave([
1078
+ 'REQUIRE_HOST_END_MEETING_BEFORE_LEAVE',
1079
+ 'LEAVE_MEETING',
1080
+ ]),
1081
+ true
1082
+ );
1083
+ assert.deepEqual(
1084
+ MeetingUtil.requireHostEndMeetingBeforeLeave([
1085
+ 'REQUIRE_HOST_END_MEETING_BEFORE_LEAVE',
1086
+ 'LEAVE_MEETING',
1087
+ 'END_MEETING',
1088
+ ]),
1089
+ true
1090
+ );
1091
+ assert.deepEqual(MeetingUtil.requireHostEndMeetingBeforeLeave(['END_MEETING']), true);
1092
+ assert.deepEqual(MeetingUtil.requireHostEndMeetingBeforeLeave([]), false);
1093
+ });
1094
+ });
1095
+
942
1096
  describe('canUserLock', () => {
943
1097
  it('works as expected', () => {
944
1098
  assert.deepEqual(
@@ -972,15 +1126,18 @@ describe('plugin-meetings', () => {
972
1126
  {functionName: 'canStartManualCaption', displayHint: 'MANUAL_CAPTION_START'},
973
1127
  {functionName: 'canStopManualCaption', displayHint: 'MANUAL_CAPTION_STOP'},
974
1128
 
975
- {functionName: 'isLocalRecordingStarted',displayHint:'LOCAL_RECORDING_STATUS_STARTED'},
1129
+ {functionName: 'isLocalRecordingStarted', displayHint: 'LOCAL_RECORDING_STATUS_STARTED'},
976
1130
  {functionName: 'isLocalRecordingStopped', displayHint: 'LOCAL_RECORDING_STATUS_STOPPED'},
977
1131
  {functionName: 'isLocalRecordingPaused', displayHint: 'LOCAL_RECORDING_STATUS_PAUSED'},
978
- {functionName: 'isLocalStreamingStarted',displayHint:'STREAMING_STATUS_STARTED'},
1132
+ {functionName: 'isLocalStreamingStarted', displayHint: 'STREAMING_STATUS_STARTED'},
979
1133
  {functionName: 'isLocalStreamingStopped', displayHint: 'STREAMING_STATUS_STOPPED'},
980
1134
 
981
1135
  {functionName: 'isManualCaptionActive', displayHint: 'MANUAL_CAPTION_STATUS_ACTIVE'},
982
1136
 
983
- {functionName: 'isSpokenLanguageAutoDetectionEnabled', displayHint: 'SPOKEN_LANGUAGE_AUTO_DETECTION_ENABLED'},
1137
+ {
1138
+ functionName: 'isSpokenLanguageAutoDetectionEnabled',
1139
+ displayHint: 'SPOKEN_LANGUAGE_AUTO_DETECTION_ENABLED',
1140
+ },
984
1141
 
985
1142
  {functionName: 'isWebexAssistantActive', displayHint: 'WEBEX_ASSISTANT_STATUS_ACTIVE'},
986
1143
  {functionName: 'canViewCaptionPanel', displayHint: 'ENABLE_CAPTION_PANEL'},
@@ -1446,11 +1603,9 @@ describe('plugin-meetings', () => {
1446
1603
  id: 'selfId123',
1447
1604
  },
1448
1605
  },
1606
+ metaData: {id: 'some hash tree metadata'},
1449
1607
  dataSets: [{name: 'dataset1', url: 'http://dataset.com'}],
1450
- mediaConnections: [
1451
- {mediaId: 'mediaId456'},
1452
- {someOtherField: 'value'},
1453
- ],
1608
+ mediaConnections: [{mediaId: 'mediaId456'}, {someOtherField: 'value'}],
1454
1609
  },
1455
1610
  };
1456
1611
  });
@@ -1466,6 +1621,7 @@ describe('plugin-meetings', () => {
1466
1621
  locusId: '12345',
1467
1622
  selfId: 'selfId123',
1468
1623
  mediaId: 'mediaId456',
1624
+ metadata: {id: 'some hash tree metadata'},
1469
1625
  });
1470
1626
  });
1471
1627
 
@@ -1495,20 +1651,275 @@ describe('plugin-meetings', () => {
1495
1651
  locusUrl: 'https://locus-a.wbx2.com/locus/api/v1/loci/12345',
1496
1652
  locusId: '12345',
1497
1653
  selfId: 'selfId123',
1654
+ metadata: {id: 'some hash tree metadata'},
1498
1655
  });
1499
1656
  assert.isUndefined(result.mediaId);
1500
1657
  });
1501
1658
 
1502
1659
  it('handles mediaConnections without mediaId', () => {
1503
- response.body.mediaConnections = [
1504
- {someField: 'value1'},
1505
- {anotherField: 'value2'},
1506
- ];
1660
+ response.body.mediaConnections = [{someField: 'value1'}, {anotherField: 'value2'}];
1507
1661
 
1508
1662
  const result = MeetingUtil.parseLocusJoin(response);
1509
1663
 
1510
1664
  assert.isUndefined(result.mediaId);
1511
1665
  });
1512
1666
  });
1667
+
1668
+ describe('#sanitizeWebSocketUrl', () => {
1669
+ it('extracts protocol, host, and pathname from URL', () => {
1670
+ const url = 'wss://example.com:443/mercury/path?token=secret&key=value#fragment';
1671
+ const result = MeetingUtil.sanitizeWebSocketUrl(url);
1672
+
1673
+ assert.equal(result, 'wss://example.com:443/mercury/path');
1674
+ });
1675
+
1676
+ it('handles URL without query string or hash', () => {
1677
+ const url = 'wss://example.com/path';
1678
+ const result = MeetingUtil.sanitizeWebSocketUrl(url);
1679
+
1680
+ assert.equal(result, 'wss://example.com/path');
1681
+ });
1682
+
1683
+ it('removes authentication from URL', () => {
1684
+ const url = 'wss://user:password@example.com/path?token=secret';
1685
+ const result = MeetingUtil.sanitizeWebSocketUrl(url);
1686
+
1687
+ assert.equal(result, 'wss://example.com/path');
1688
+ });
1689
+
1690
+ it('returns empty string for null or undefined', () => {
1691
+ assert.equal(MeetingUtil.sanitizeWebSocketUrl(null), '');
1692
+ assert.equal(MeetingUtil.sanitizeWebSocketUrl(undefined), '');
1693
+ });
1694
+
1695
+ it('returns empty string for non-string input', () => {
1696
+ assert.equal(MeetingUtil.sanitizeWebSocketUrl(123), '');
1697
+ assert.equal(MeetingUtil.sanitizeWebSocketUrl({}), '');
1698
+ });
1699
+
1700
+ it('returns empty string for invalid URL', () => {
1701
+ const result = MeetingUtil.sanitizeWebSocketUrl('not a valid url');
1702
+
1703
+ assert.equal(result, '');
1704
+ });
1705
+
1706
+ it('handles URL without pathname', () => {
1707
+ const url = 'wss://example.com?query=value';
1708
+ const result = MeetingUtil.sanitizeWebSocketUrl(url);
1709
+
1710
+ assert.equal(result, 'wss://example.com');
1711
+ });
1712
+ });
1713
+
1714
+ describe('#_urlsPartiallyMatch', () => {
1715
+ it('returns true when URLs match exactly (ignoring query and hash)', () => {
1716
+ const url1 = 'wss://example.com:443/path?token=abc#fragment1';
1717
+ const url2 = 'wss://example.com:443/path?token=xyz#fragment2';
1718
+
1719
+ assert.isTrue(MeetingUtil._urlsPartiallyMatch(url1, url2));
1720
+ });
1721
+
1722
+ it('returns true when one URL is proxied and ends with the other', () => {
1723
+ const url1 = 'wss://other.example.com/somepath/mercury.example.com/v1/path';
1724
+ const url2 = 'wss://mercury.example.com/v1/path';
1725
+
1726
+ assert.isTrue(MeetingUtil._urlsPartiallyMatch(url1, url2));
1727
+ });
1728
+
1729
+ it('returns true when the second URL is proxied', () => {
1730
+ const url1 = 'wss://mercury.example.com/v1/path';
1731
+ const url2 = 'wss://other.example.com/somepath/mercury.example.com/v1/path';
1732
+
1733
+ assert.isTrue(MeetingUtil._urlsPartiallyMatch(url1, url2));
1734
+ });
1735
+
1736
+ it('returns false when hosts differ and no partial match', () => {
1737
+ const url1 = 'wss://example1.com/path';
1738
+ const url2 = 'wss://example2.com/path';
1739
+
1740
+ assert.isFalse(MeetingUtil._urlsPartiallyMatch(url1, url2));
1741
+ });
1742
+
1743
+ it('returns false when pathnames differ and no partial match', () => {
1744
+ const url1 = 'wss://example.com/path1';
1745
+ const url2 = 'wss://example.com/path2';
1746
+
1747
+ assert.isFalse(MeetingUtil._urlsPartiallyMatch(url1, url2));
1748
+ });
1749
+
1750
+ it('returns false when either URL is null or undefined', () => {
1751
+ const url = 'wss://example.com/path';
1752
+
1753
+ assert.isFalse(MeetingUtil._urlsPartiallyMatch(null, url));
1754
+ assert.isFalse(MeetingUtil._urlsPartiallyMatch(url, null));
1755
+ assert.isFalse(MeetingUtil._urlsPartiallyMatch(undefined, url));
1756
+ assert.isFalse(MeetingUtil._urlsPartiallyMatch(url, undefined));
1757
+ });
1758
+
1759
+ it('returns false when both URLs are null', () => {
1760
+ assert.isFalse(MeetingUtil._urlsPartiallyMatch(null, null));
1761
+ });
1762
+
1763
+ it('returns false when URL parsing fails', () => {
1764
+ const url1 = 'invalid url';
1765
+ const url2 = 'wss://example.com/path';
1766
+
1767
+ assert.isFalse(MeetingUtil._urlsPartiallyMatch(url1, url2));
1768
+ });
1769
+ });
1770
+
1771
+ describe('#getSocketUrlInfo', () => {
1772
+ it('returns socket URL info when URLs differ', () => {
1773
+ const testWebex = {
1774
+ internal: {
1775
+ mercury: {
1776
+ socket: {
1777
+ url: 'wss://mercury.example.com:443/path?token=abc',
1778
+ },
1779
+ },
1780
+ device: {
1781
+ webSocketUrl: 'wss://device.example.com:443/path?token=xyz',
1782
+ },
1783
+ },
1784
+ };
1785
+
1786
+ const result = MeetingUtil.getSocketUrlInfo(testWebex);
1787
+
1788
+ assert.isTrue(result.hasMismatchedSocket);
1789
+ assert.equal(result.mercurySocketUrl, 'wss://mercury.example.com:443/path');
1790
+ assert.equal(result.deviceSocketUrl, 'wss://device.example.com:443/path');
1791
+ });
1792
+
1793
+ it('returns socket URL info when URLs match', () => {
1794
+ const testWebex = {
1795
+ internal: {
1796
+ mercury: {
1797
+ socket: {
1798
+ url: 'wss://example.com:443/path?token=abc',
1799
+ },
1800
+ },
1801
+ device: {
1802
+ webSocketUrl: 'wss://example.com:443/path?token=xyz',
1803
+ },
1804
+ },
1805
+ };
1806
+
1807
+ const result = MeetingUtil.getSocketUrlInfo(testWebex);
1808
+
1809
+ assert.isFalse(result.hasMismatchedSocket);
1810
+ assert.equal(result.mercurySocketUrl, 'wss://example.com:443/path');
1811
+ assert.equal(result.deviceSocketUrl, 'wss://example.com:443/path');
1812
+ });
1813
+
1814
+ it('returns hasMismatchedSocket as false when one URL is proxied (partial match)', () => {
1815
+ const testWebex = {
1816
+ internal: {
1817
+ mercury: {
1818
+ socket: {
1819
+ url: 'wss://other.example.com/somepath/mercury.example.com/v1/apps/wx2/registrations/00000000-0000-0000-0000-000000000000/messages',
1820
+ },
1821
+ },
1822
+ device: {
1823
+ webSocketUrl:
1824
+ 'wss://mercury.example.com/v1/apps/wx2/registrations/00000000-0000-0000-0000-000000000000/messages',
1825
+ },
1826
+ },
1827
+ };
1828
+
1829
+ const result = MeetingUtil.getSocketUrlInfo(testWebex);
1830
+
1831
+ assert.isFalse(result.hasMismatchedSocket);
1832
+ assert.equal(
1833
+ result.mercurySocketUrl,
1834
+ 'wss://other.example.com/somepath/mercury.example.com/v1/apps/wx2/registrations/00000000-0000-0000-0000-000000000000/messages'
1835
+ );
1836
+ assert.equal(
1837
+ result.deviceSocketUrl,
1838
+ 'wss://mercury.example.com/v1/apps/wx2/registrations/00000000-0000-0000-0000-000000000000/messages'
1839
+ );
1840
+ });
1841
+
1842
+ it('returns false for hasMismatchedSocket when mercury socket URL is missing', () => {
1843
+ const testWebex = {
1844
+ internal: {
1845
+ mercury: {
1846
+ socket: {},
1847
+ },
1848
+ device: {
1849
+ webSocketUrl: 'wss://device.example.com:443/path',
1850
+ },
1851
+ },
1852
+ };
1853
+
1854
+ const result = MeetingUtil.getSocketUrlInfo(testWebex);
1855
+
1856
+ assert.isFalse(result.hasMismatchedSocket);
1857
+ assert.equal(result.mercurySocketUrl, '');
1858
+ assert.equal(result.deviceSocketUrl, 'wss://device.example.com:443/path');
1859
+ });
1860
+
1861
+ it('returns false for hasMismatchedSocket when device socket URL is missing', () => {
1862
+ const testWebex = {
1863
+ internal: {
1864
+ mercury: {
1865
+ socket: {
1866
+ url: 'wss://mercury.example.com:443/path',
1867
+ },
1868
+ },
1869
+ device: {},
1870
+ },
1871
+ };
1872
+
1873
+ const result = MeetingUtil.getSocketUrlInfo(testWebex);
1874
+
1875
+ assert.isFalse(result.hasMismatchedSocket);
1876
+ assert.equal(result.mercurySocketUrl, 'wss://mercury.example.com:443/path');
1877
+ assert.equal(result.deviceSocketUrl, '');
1878
+ });
1879
+
1880
+ it('returns default values when webex object is missing properties', () => {
1881
+ const testWebex = {
1882
+ internal: {},
1883
+ };
1884
+
1885
+ const result = MeetingUtil.getSocketUrlInfo(testWebex);
1886
+
1887
+ assert.isFalse(result.hasMismatchedSocket);
1888
+ assert.equal(result.mercurySocketUrl, '');
1889
+ assert.equal(result.deviceSocketUrl, '');
1890
+ });
1891
+
1892
+ it('handles error gracefully and returns default values', () => {
1893
+ const testWebex = null;
1894
+
1895
+ const result = MeetingUtil.getSocketUrlInfo(testWebex);
1896
+
1897
+ assert.isFalse(result.hasMismatchedSocket);
1898
+ assert.equal(result.mercurySocketUrl, '');
1899
+ assert.equal(result.deviceSocketUrl, '');
1900
+ });
1901
+
1902
+ it('sanitizes URLs by removing query parameters', () => {
1903
+ const testWebex = {
1904
+ internal: {
1905
+ mercury: {
1906
+ socket: {
1907
+ url: 'wss://mercury.example.com/path?secret=token123&key=value',
1908
+ },
1909
+ },
1910
+ device: {
1911
+ webSocketUrl: 'wss://device.example.com/path?secret=differenttoken&key=value',
1912
+ },
1913
+ },
1914
+ };
1915
+
1916
+ const result = MeetingUtil.getSocketUrlInfo(testWebex);
1917
+
1918
+ assert.notInclude(result.mercurySocketUrl, 'secret');
1919
+ assert.notInclude(result.mercurySocketUrl, 'token123');
1920
+ assert.notInclude(result.deviceSocketUrl, 'secret');
1921
+ assert.notInclude(result.deviceSocketUrl, 'differenttoken');
1922
+ });
1923
+ });
1513
1924
  });
1514
1925
  });