@webex/plugin-meetings 3.11.0 → 3.12.0

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