@webex/plugin-meetings 3.7.0-next.1 → 3.7.0-next.11

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 (63) hide show
  1. package/dist/breakouts/breakout.js +1 -1
  2. package/dist/breakouts/index.js +1 -1
  3. package/dist/config.js +2 -1
  4. package/dist/config.js.map +1 -1
  5. package/dist/constants.js +10 -1
  6. package/dist/constants.js.map +1 -1
  7. package/dist/interpretation/index.js +1 -1
  8. package/dist/interpretation/siLanguage.js +1 -1
  9. package/dist/meeting/in-meeting-actions.js +11 -1
  10. package/dist/meeting/in-meeting-actions.js.map +1 -1
  11. package/dist/meeting/index.js +287 -198
  12. package/dist/meeting/index.js.map +1 -1
  13. package/dist/meeting/util.js +1 -0
  14. package/dist/meeting/util.js.map +1 -1
  15. package/dist/meetings/index.js +4 -1
  16. package/dist/meetings/index.js.map +1 -1
  17. package/dist/members/util.js +4 -2
  18. package/dist/members/util.js.map +1 -1
  19. package/dist/metrics/constants.js +3 -1
  20. package/dist/metrics/constants.js.map +1 -1
  21. package/dist/reachability/clusterReachability.js +12 -11
  22. package/dist/reachability/clusterReachability.js.map +1 -1
  23. package/dist/recording-controller/enums.js +8 -4
  24. package/dist/recording-controller/enums.js.map +1 -1
  25. package/dist/recording-controller/index.js +18 -9
  26. package/dist/recording-controller/index.js.map +1 -1
  27. package/dist/recording-controller/util.js +13 -9
  28. package/dist/recording-controller/util.js.map +1 -1
  29. package/dist/types/config.d.ts +1 -0
  30. package/dist/types/constants.d.ts +8 -0
  31. package/dist/types/meeting/in-meeting-actions.d.ts +10 -0
  32. package/dist/types/meeting/index.d.ts +20 -0
  33. package/dist/types/meetings/index.d.ts +3 -0
  34. package/dist/types/members/util.d.ts +2 -0
  35. package/dist/types/metrics/constants.d.ts +2 -0
  36. package/dist/types/recording-controller/enums.d.ts +5 -2
  37. package/dist/types/recording-controller/index.d.ts +1 -0
  38. package/dist/types/recording-controller/util.d.ts +2 -1
  39. package/dist/webinar/index.js +39 -7
  40. package/dist/webinar/index.js.map +1 -1
  41. package/package.json +21 -21
  42. package/src/config.ts +1 -0
  43. package/src/constants.ts +10 -0
  44. package/src/meeting/in-meeting-actions.ts +21 -0
  45. package/src/meeting/index.ts +94 -0
  46. package/src/meeting/util.ts +1 -0
  47. package/src/meetings/index.ts +6 -0
  48. package/src/members/util.ts +1 -0
  49. package/src/metrics/constants.ts +2 -0
  50. package/src/reachability/clusterReachability.ts +4 -1
  51. package/src/recording-controller/enums.ts +5 -2
  52. package/src/recording-controller/index.ts +17 -4
  53. package/src/recording-controller/util.ts +20 -5
  54. package/src/webinar/index.ts +40 -8
  55. package/test/unit/spec/meeting/in-meeting-actions.ts +13 -1
  56. package/test/unit/spec/meeting/index.js +72 -0
  57. package/test/unit/spec/meeting/utils.js +2 -0
  58. package/test/unit/spec/meetings/index.js +9 -5
  59. package/test/unit/spec/members/utils.js +95 -0
  60. package/test/unit/spec/reachability/clusterReachability.ts +7 -0
  61. package/test/unit/spec/recording-controller/index.js +61 -5
  62. package/test/unit/spec/recording-controller/util.js +39 -3
  63. package/test/unit/spec/webinar/index.ts +47 -0
@@ -197,6 +197,7 @@ const MeetingUtil = {
197
197
 
198
198
  cleanUp: (meeting) => {
199
199
  meeting.getWebexObject().internal.device.meetingEnded();
200
+ meeting.stopPeriodicLogUpload();
200
201
 
201
202
  meeting.breakouts.cleanUp();
202
203
  meeting.simultaneousInterpretation.cleanUp();
@@ -155,6 +155,9 @@ export type BasicMeetingInformation = {
155
155
  };
156
156
  meetingInfo: any;
157
157
  sessionCorrelationId: string;
158
+ roles: string[];
159
+ getCurUserType: () => string | null;
160
+ callStateForMetrics: CallStateForMetrics;
158
161
  };
159
162
 
160
163
  /**
@@ -1143,6 +1146,9 @@ export default class Meetings extends WebexPlugin {
1143
1146
  sessionId: meeting.locusInfo?.fullState?.sessionId,
1144
1147
  },
1145
1148
  },
1149
+ roles: meeting.roles,
1150
+ callStateForMetrics: meeting.callStateForMetrics,
1151
+ getCurUserType: meeting.getCurUserType,
1146
1152
  });
1147
1153
  this.meetingCollection.delete(meeting.id);
1148
1154
  Trigger.trigger(
@@ -46,6 +46,7 @@ const MembersUtil = {
46
46
  {
47
47
  address:
48
48
  options.invitee.emailAddress || options.invitee.email || options.invitee.phoneNumber,
49
+ ...(options.invitee.roles ? {roles: options.invitee.roles} : {}),
49
50
  },
50
51
  ],
51
52
  alertIfActive: options.alertIfActive,
@@ -71,6 +71,8 @@ const BEHAVIORAL_METRICS = {
71
71
  TURN_DISCOVERY_REQUIRES_OK: 'js_sdk_turn_discovery_requires_ok',
72
72
  REACHABILITY_COMPLETED: 'js_sdk_reachability_completed',
73
73
  WEBINAR_REGISTRATION_ERROR: 'js_sdk_webinar_registration_error',
74
+ GUEST_ENTERED_LOBBY: 'js_sdk_guest_entered_lobby',
75
+ GUEST_EXITED_LOBBY: 'js_sdk_guest_exited_lobby',
74
76
  };
75
77
 
76
78
  export {BEHAVIORAL_METRICS as default};
@@ -357,11 +357,14 @@ export class ClusterReachability extends EventsScope {
357
357
 
358
358
  this.startTimestamp = performance.now();
359
359
 
360
+ // Set up the state change listeners before triggering the ICE gathering
361
+ const gatherIceCandidatePromise = this.gatherIceCandidates();
362
+
360
363
  // not awaiting the next call on purpose, because we're not sending the offer anywhere and there won't be any answer
361
364
  // we just need to make this call to trigger the ICE gathering process
362
365
  this.pc.setLocalDescription(offer);
363
366
 
364
- await this.gatherIceCandidates();
367
+ await gatherIceCandidatePromise;
365
368
  } catch (error) {
366
369
  LoggerProxy.logger.warn(`Reachability:ClusterReachability#start --> Error: `, error);
367
370
  }
@@ -1,8 +1,11 @@
1
- enum RecordingAction {
1
+ export enum RecordingAction {
2
2
  Start = 'Start',
3
3
  Stop = 'Stop',
4
4
  Pause = 'Pause',
5
5
  Resume = 'Resume',
6
6
  }
7
7
 
8
- export default RecordingAction;
8
+ export enum RecordingType {
9
+ Premise = 'premise',
10
+ Cloud = 'cloud',
11
+ }
@@ -1,9 +1,9 @@
1
1
  import PermissionError from '../common/errors/permission';
2
+ import LoggerProxy from '../common/logs/logger-proxy';
2
3
  import {CONTROLS, HTTP_VERBS, SELF_POLICY} from '../constants';
3
4
  import MeetingRequest from '../meeting/request';
4
- import RecordingAction from './enums';
5
+ import {RecordingAction, RecordingType} from './enums';
5
6
  import Util from './util';
6
- import LoggerProxy from '../common/logs/logger-proxy';
7
7
 
8
8
  /**
9
9
  * @description Recording manages the recording functionality of the meeting object, there should only be one instantation of recording per meeting
@@ -228,11 +228,12 @@ export default class RecordingController {
228
228
 
229
229
  /**
230
230
  * @param {RecordingAction} action
231
+ * @param {RecordingType} recordingType
231
232
  * @private
232
233
  * @memberof RecordingController
233
234
  * @returns {Promise}
234
235
  */
235
- private recordingService(action: RecordingAction): Promise<any> {
236
+ private recordingService(action: RecordingAction, recordingType: RecordingType): Promise<any> {
236
237
  // @ts-ignore
237
238
  return this.request.request({
238
239
  body: {
@@ -242,6 +243,7 @@ export default class RecordingController {
242
243
  recording: {
243
244
  action: action.toLowerCase(),
244
245
  },
246
+ recordingType,
245
247
  },
246
248
  uri: `${this.serviceUrl}/loci/${this.locusId}/recording`,
247
249
  method: HTTP_VERBS.PUT,
@@ -276,14 +278,25 @@ export default class RecordingController {
276
278
  * @returns {Promise}
277
279
  */
278
280
  private recordingFacade(action: RecordingAction): Promise<any> {
281
+ const isPremiseRecordingEnabled = Util.isPremiseRecordingEnabled(
282
+ this.displayHints,
283
+ this.selfUserPolicies
284
+ );
279
285
  LoggerProxy.logger.log(
280
286
  `RecordingController:index#recordingFacade --> recording action [${action}]`
281
287
  );
282
288
 
289
+ let recordingType: RecordingType;
290
+ if (isPremiseRecordingEnabled) {
291
+ recordingType = RecordingType.Premise;
292
+ } else {
293
+ recordingType = RecordingType.Cloud;
294
+ }
295
+
283
296
  // assumes action is proper cased (i.e., Example)
284
297
  if (Util?.[`canUser${action}`](this.displayHints, this.selfUserPolicies)) {
285
298
  if (this.serviceUrl) {
286
- return this.recordingService(action);
299
+ return this.recordingService(action, recordingType);
287
300
  }
288
301
 
289
302
  return this.recordingControls(action);
@@ -1,33 +1,47 @@
1
1
  import {DISPLAY_HINTS, SELF_POLICY} from '../constants';
2
- import RecordingAction from './enums';
2
+ import {RecordingAction} from './enums';
3
3
  import MeetingUtil from '../meeting/util';
4
4
 
5
5
  const canUserStart = (
6
6
  displayHints: Array<string>,
7
7
  userPolicies: Record<SELF_POLICY, boolean>
8
8
  ): boolean =>
9
- displayHints.includes(DISPLAY_HINTS.RECORDING_CONTROL_START) &&
9
+ (displayHints.includes(DISPLAY_HINTS.RECORDING_CONTROL_START) ||
10
+ displayHints.includes(DISPLAY_HINTS.PREMISE_RECORDING_CONTROL_START)) &&
10
11
  MeetingUtil.selfSupportsFeature(SELF_POLICY.SUPPORT_NETWORK_BASED_RECORD, userPolicies);
11
12
 
12
13
  const canUserPause = (
13
14
  displayHints: Array<string>,
14
15
  userPolicies: Record<SELF_POLICY, boolean>
15
16
  ): boolean =>
16
- displayHints.includes(DISPLAY_HINTS.RECORDING_CONTROL_PAUSE) &&
17
+ (displayHints.includes(DISPLAY_HINTS.RECORDING_CONTROL_PAUSE) ||
18
+ displayHints.includes(DISPLAY_HINTS.PREMISE_RECORDING_CONTROL_PAUSE)) &&
17
19
  MeetingUtil.selfSupportsFeature(SELF_POLICY.SUPPORT_NETWORK_BASED_RECORD, userPolicies);
18
20
 
19
21
  const canUserResume = (
20
22
  displayHints: Array<string>,
21
23
  userPolicies: Record<SELF_POLICY, boolean>
22
24
  ): boolean =>
23
- displayHints.includes(DISPLAY_HINTS.RECORDING_CONTROL_RESUME) &&
25
+ (displayHints.includes(DISPLAY_HINTS.RECORDING_CONTROL_RESUME) ||
26
+ displayHints.includes(DISPLAY_HINTS.PREMISE_RECORDING_CONTROL_RESUME)) &&
24
27
  MeetingUtil.selfSupportsFeature(SELF_POLICY.SUPPORT_NETWORK_BASED_RECORD, userPolicies);
25
28
 
26
29
  const canUserStop = (
27
30
  displayHints: Array<string>,
28
31
  userPolicies: Record<SELF_POLICY, boolean>
29
32
  ): boolean =>
30
- displayHints.includes(DISPLAY_HINTS.RECORDING_CONTROL_STOP) &&
33
+ (displayHints.includes(DISPLAY_HINTS.RECORDING_CONTROL_STOP) ||
34
+ displayHints.includes(DISPLAY_HINTS.PREMISE_RECORDING_CONTROL_STOP)) &&
35
+ MeetingUtil.selfSupportsFeature(SELF_POLICY.SUPPORT_NETWORK_BASED_RECORD, userPolicies);
36
+
37
+ const isPremiseRecordingEnabled = (
38
+ displayHints: Array<string>,
39
+ userPolicies: Record<SELF_POLICY, boolean>
40
+ ): boolean =>
41
+ (displayHints.includes(DISPLAY_HINTS.PREMISE_RECORDING_CONTROL_START) ||
42
+ displayHints.includes(DISPLAY_HINTS.PREMISE_RECORDING_CONTROL_PAUSE) ||
43
+ displayHints.includes(DISPLAY_HINTS.PREMISE_RECORDING_CONTROL_STOP) ||
44
+ displayHints.includes(DISPLAY_HINTS.PREMISE_RECORDING_CONTROL_RESUME)) &&
31
45
  MeetingUtil.selfSupportsFeature(SELF_POLICY.SUPPORT_NETWORK_BASED_RECORD, userPolicies);
32
46
 
33
47
  const extractLocusId = (url: string) => {
@@ -70,6 +84,7 @@ export default {
70
84
  canUserPause,
71
85
  canUserResume,
72
86
  canUserStop,
87
+ isPremiseRecordingEnabled,
73
88
  deriveRecordingStates,
74
89
  extractLocusId,
75
90
  };
@@ -3,9 +3,10 @@
3
3
  */
4
4
  import {WebexPlugin} from '@webex/webex-core';
5
5
  import {get} from 'lodash';
6
- import {MEETINGS, SELF_ROLES} from '../constants';
6
+ import {HTTP_VERBS, MEETINGS, SELF_ROLES} from '../constants';
7
7
 
8
8
  import WebinarCollection from './collection';
9
+ import LoggerProxy from '../common/logs/logger-proxy';
9
10
 
10
11
  /**
11
12
  * @class Webinar
@@ -22,6 +23,7 @@ const Webinar = WebexPlugin.extend({
22
23
  canManageWebcast: 'boolean', // appears the ability to manage webcast
23
24
  selfIsPanelist: 'boolean', // self is panelist
24
25
  selfIsAttendee: 'boolean', // self is attendee
26
+ practiceSessionEnabled: 'boolean', // practice session enabled
25
27
  },
26
28
 
27
29
  /**
@@ -59,18 +61,48 @@ const Webinar = WebexPlugin.extend({
59
61
  * @returns {{isPromoted: boolean, isDemoted: boolean}} Role transition states
60
62
  */
61
63
  updateRoleChanged(payload) {
64
+ const oldRoles = get(payload, 'oldRoles', []);
65
+ const newRoles = get(payload, 'newRoles', []);
66
+
62
67
  const isPromoted =
63
- get(payload, 'oldRoles', []).includes(SELF_ROLES.ATTENDEE) &&
64
- get(payload, 'newRoles', []).includes(SELF_ROLES.PANELIST);
68
+ oldRoles.includes(SELF_ROLES.ATTENDEE) && newRoles.includes(SELF_ROLES.PANELIST);
65
69
  const isDemoted =
66
- get(payload, 'oldRoles', []).includes(SELF_ROLES.PANELIST) &&
67
- get(payload, 'newRoles', []).includes(SELF_ROLES.ATTENDEE);
68
- this.set('selfIsPanelist', get(payload, 'newRoles', []).includes(SELF_ROLES.PANELIST));
69
- this.set('selfIsAttendee', get(payload, 'newRoles', []).includes(SELF_ROLES.ATTENDEE));
70
- this.updateCanManageWebcast(get(payload, 'newRoles', []).includes(SELF_ROLES.MODERATOR));
70
+ oldRoles.includes(SELF_ROLES.PANELIST) && newRoles.includes(SELF_ROLES.ATTENDEE);
71
+ this.set('selfIsPanelist', newRoles.includes(SELF_ROLES.PANELIST));
72
+ this.set('selfIsAttendee', newRoles.includes(SELF_ROLES.ATTENDEE));
73
+ this.updateCanManageWebcast(newRoles.includes(SELF_ROLES.MODERATOR));
71
74
 
72
75
  return {isPromoted, isDemoted};
73
76
  },
77
+
78
+ /**
79
+ * start or stop practice session for webinar
80
+ * @param {boolean} enabled
81
+ * @returns {Promise}
82
+ */
83
+ setPracticeSessionState(enabled) {
84
+ return this.request({
85
+ method: HTTP_VERBS.PATCH,
86
+ uri: `${this.locusUrl}/controls`,
87
+ body: {
88
+ practiceSession: {
89
+ enabled,
90
+ },
91
+ },
92
+ }).catch((error) => {
93
+ LoggerProxy.logger.error('Meeting:webinar#setPracticeSessionState failed', error);
94
+ throw error;
95
+ });
96
+ },
97
+
98
+ /**
99
+ * update practice session status
100
+ * @param {object} payload
101
+ * @returns {void}
102
+ */
103
+ updatePracticeSessionStatus(payload) {
104
+ this.set('practiceSessionEnabled', payload.enabled);
105
+ },
74
106
  });
75
107
 
76
108
  export default Webinar;
@@ -33,6 +33,7 @@ describe('plugin-meetings', () => {
33
33
  canStartManualCaption: null,
34
34
  canStopManualCaption: null,
35
35
  isManualCaptionActive: null,
36
+ isPremiseRecordingEnabled: null,
36
37
  isSaveTranscriptsEnabled: null,
37
38
  isWebexAssistantActive: null,
38
39
  canViewCaptionPanel: null,
@@ -88,6 +89,11 @@ describe('plugin-meetings', () => {
88
89
  canShowStageView: null,
89
90
  canEnableStageView: null,
90
91
  canDisableStageView: null,
92
+ isPracticeSessionOn : null,
93
+ isPracticeSessionOff : null,
94
+ canStartPracticeSession: null,
95
+ canStopPracticeSession: null,
96
+
91
97
  ...expected,
92
98
  };
93
99
 
@@ -126,6 +132,7 @@ describe('plugin-meetings', () => {
126
132
  'canStartManualCaption',
127
133
  'canStopManualCaption',
128
134
  'isManualCaptionActive',
135
+ 'isPremiseRecordingEnabled',
129
136
  'isSaveTranscriptsEnabled',
130
137
  'isWebexAssistantActive',
131
138
  'canViewCaptionPanel',
@@ -181,7 +188,12 @@ describe('plugin-meetings', () => {
181
188
  'canShowStageView',
182
189
  'canEnableStageView',
183
190
  'canDisableStageView',
184
- ].forEach((key) => {
191
+ 'isPracticeSessionOn',
192
+ 'isPracticeSessionOff',
193
+ 'canStartPracticeSession',
194
+ 'canStopPracticeSession',
195
+
196
+ ].forEach((key) => {
185
197
  it(`get and set for ${key} work as expected`, () => {
186
198
  const inMeetingActions = new InMeetingActions();
187
199
 
@@ -2465,6 +2465,61 @@ describe('plugin-meetings', () => {
2465
2465
  checkWorking();
2466
2466
  });
2467
2467
 
2468
+ it('should upload logs periodically', async () => {
2469
+ const clock = sinon.useFakeTimers();
2470
+
2471
+ meeting.roap.doTurnDiscovery = sinon
2472
+ .stub()
2473
+ .resolves({turnServerInfo: undefined, turnDiscoverySkippedReason: undefined});
2474
+
2475
+ let logUploadCounter = 0;
2476
+
2477
+ TriggerProxy.trigger.callsFake((meetingObject, options, event) => {
2478
+ if (
2479
+ meetingObject === meeting &&
2480
+ options.file === 'meeting/index' &&
2481
+ options.function === 'uploadLogs' &&
2482
+ event === 'REQUEST_UPLOAD_LOGS'
2483
+ ) {
2484
+ logUploadCounter += 1;
2485
+ }
2486
+ });
2487
+
2488
+ meeting.config.logUploadIntervalMultiplicationFactor = 1;
2489
+ meeting.meetingState = 'ACTIVE';
2490
+
2491
+ await meeting.addMedia({
2492
+ mediaSettings: {},
2493
+ });
2494
+
2495
+ const checkLogCounter = (delayInMinutes, expectedCounter) => {
2496
+ const delayInMilliseconds = delayInMinutes * 60 * 1000;
2497
+
2498
+ // first check that the counter is not increased just before the delay
2499
+ clock.tick(delayInMilliseconds - 50);
2500
+ assert.equal(logUploadCounter, expectedCounter - 1);
2501
+
2502
+ // and now check that it has reached expected value after the delay
2503
+ clock.tick(50);
2504
+ assert.equal(logUploadCounter, expectedCounter);
2505
+ };
2506
+
2507
+ checkLogCounter(0.1, 1);
2508
+ checkLogCounter(15, 2);
2509
+ checkLogCounter(30, 3);
2510
+ checkLogCounter(60, 4);
2511
+ checkLogCounter(60, 5);
2512
+
2513
+ // simulate media connection being removed -> 1 more upload should happen, but nothing more afterwards
2514
+ meeting.mediaProperties.webrtcMediaConnection = undefined;
2515
+ checkLogCounter(60, 6);
2516
+
2517
+ clock.tick(120*1000*60);
2518
+ assert.equal(logUploadCounter, 6);
2519
+
2520
+ clock.restore();
2521
+ });
2522
+
2468
2523
  it('should attach the media and return promise when in the lobby if allowMediaInLobby is set', async () => {
2469
2524
  meeting.roap.doTurnDiscovery = sinon
2470
2525
  .stub()
@@ -8620,6 +8675,13 @@ describe('plugin-meetings', () => {
8620
8675
  {payload: test1}
8621
8676
  );
8622
8677
  assert.calledOnce(meeting.updateLLMConnection);
8678
+ assert.calledOnceWithExactly(
8679
+ Metrics.sendBehavioralMetric,
8680
+ BEHAVIORAL_METRICS.GUEST_ENTERED_LOBBY,
8681
+ {
8682
+ correlation_id: meeting.correlationId,
8683
+ }
8684
+ );
8623
8685
  done();
8624
8686
  });
8625
8687
  it('listens to the self admitted guest event', (done) => {
@@ -8641,6 +8703,13 @@ describe('plugin-meetings', () => {
8641
8703
  assert.calledOnce(meeting.updateLLMConnection);
8642
8704
  assert.calledOnceWithExactly(meeting.rtcMetrics.sendNextMetrics);
8643
8705
 
8706
+ assert.calledOnceWithExactly(
8707
+ Metrics.sendBehavioralMetric,
8708
+ BEHAVIORAL_METRICS.GUEST_EXITED_LOBBY,
8709
+ {
8710
+ correlation_id: meeting.correlationId,
8711
+ }
8712
+ );
8644
8713
  done();
8645
8714
  });
8646
8715
 
@@ -8973,6 +9042,8 @@ describe('plugin-meetings', () => {
8973
9042
  });
8974
9043
 
8975
9044
  it('listens to MEETING_CONTROLS_PRACTICE_SESSION_STATUS_UPDATED', async () => {
9045
+ meeting.webinar.updatePracticeSessionStatus = sinon.stub();
9046
+
8976
9047
  const state = {example: 'value'};
8977
9048
 
8978
9049
  await meeting.locusInfo.emitScoped(
@@ -8981,6 +9052,7 @@ describe('plugin-meetings', () => {
8981
9052
  {state}
8982
9053
  );
8983
9054
 
9055
+ assert.calledOnceWithExactly( meeting.webinar.updatePracticeSessionStatus, state);
8984
9056
  assert.calledWith(
8985
9057
  TriggerProxy.trigger,
8986
9058
  meeting,
@@ -45,6 +45,7 @@ describe('plugin-meetings', () => {
45
45
  meeting.cleanupLocalStreams = sinon.stub().returns(Promise.resolve());
46
46
  meeting.closeRemoteStreams = sinon.stub().returns(Promise.resolve());
47
47
  meeting.closePeerConnections = sinon.stub().returns(Promise.resolve());
48
+ meeting.stopPeriodicLogUpload = sinon.stub();
48
49
 
49
50
  meeting.unsetRemoteStreams = sinon.stub();
50
51
  meeting.unsetPeerConnections = sinon.stub();
@@ -70,6 +71,7 @@ describe('plugin-meetings', () => {
70
71
  assert.calledOnce(meeting.cleanupLocalStreams);
71
72
  assert.calledOnce(meeting.closeRemoteStreams);
72
73
  assert.calledOnce(meeting.closePeerConnections);
74
+ assert.calledOnce(meeting.stopPeriodicLogUpload);
73
75
 
74
76
  assert.calledOnce(meeting.unsetRemoteStreams);
75
77
  assert.calledOnce(meeting.unsetPeerConnections);
@@ -131,9 +131,9 @@ describe('plugin-meetings', () => {
131
131
  logger,
132
132
  people: {
133
133
  _getMe: sinon.stub().resolves({
134
- type: 'validuser',
134
+ type: 'validuser',
135
135
  }),
136
- }
136
+ },
137
137
  });
138
138
 
139
139
  startReachabilityStub = sinon.stub(webex.meetings, 'startReachability').resolves();
@@ -1985,6 +1985,8 @@ describe('plugin-meetings', () => {
1985
1985
  const meetingIds = {
1986
1986
  meetingId: meeting.id,
1987
1987
  correlationId: meeting.correlationId,
1988
+ roles: meeting.roles,
1989
+ callStateForMetrics: meeting.callStateForMetrics,
1988
1990
  };
1989
1991
 
1990
1992
  webex.meetings.destroy(meeting, test1);
@@ -2021,6 +2023,8 @@ describe('plugin-meetings', () => {
2021
2023
 
2022
2024
  assert.equal(deletedMeetingInfo.id, meetingIds.meetingId);
2023
2025
  assert.equal(deletedMeetingInfo.correlationId, meetingIds.correlationId);
2026
+ assert.equal(deletedMeetingInfo.roles, meetingIds.roles);
2027
+ assert.equal(deletedMeetingInfo.callStateForMetrics, meetingIds.callStateForMetrics);
2024
2028
  });
2025
2029
  });
2026
2030
 
@@ -2092,7 +2096,7 @@ describe('plugin-meetings', () => {
2092
2096
  );
2093
2097
  });
2094
2098
 
2095
- const setup = ({me = { type: 'validuser'}, user} = {}) => {
2099
+ const setup = ({me = {type: 'validuser'}, user} = {}) => {
2096
2100
  loggerProxySpy = sinon.spy(LoggerProxy.logger, 'error');
2097
2101
  assert.deepEqual(webex.internal.services._getCatalog().getAllowedDomains(), []);
2098
2102
 
@@ -2113,9 +2117,9 @@ describe('plugin-meetings', () => {
2113
2117
 
2114
2118
  it('should not call request.getMeetingPreferences if user is a guest', async () => {
2115
2119
  setup({me: {type: 'appuser'}});
2116
-
2120
+
2117
2121
  await webex.meetings.fetchUserPreferredWebexSite();
2118
-
2122
+
2119
2123
  assert.equal(webex.meetings.preferredWebexSite, '');
2120
2124
  assert.deepEqual(webex.internal.services._getCatalog().getAllowedDomains(), []);
2121
2125
  assert.notCalled(webex.internal.services.getMeetingPreferences);
@@ -262,5 +262,100 @@ describe('plugin-meetings', () => {
262
262
  testParams(false);
263
263
  });
264
264
  });
265
+
266
+ describe('#getAddMemberBody', () => {
267
+ it('returns the correct body with email address and roles', () => {
268
+ const options = {
269
+ invitee: {
270
+ emailAddress: 'test@example.com',
271
+ roles: ['role1', 'role2'],
272
+ },
273
+ alertIfActive: true,
274
+ };
275
+
276
+ assert.deepEqual(MembersUtil.getAddMemberBody(options), {
277
+ invitees: [
278
+ {
279
+ address: 'test@example.com',
280
+ roles: ['role1', 'role2'],
281
+ },
282
+ ],
283
+ alertIfActive: true,
284
+ });
285
+ });
286
+
287
+ it('returns the correct body with phone number and no roles', () => {
288
+ const options = {
289
+ invitee: {
290
+ phoneNumber: '1234567890',
291
+ },
292
+ alertIfActive: false,
293
+ };
294
+
295
+ assert.deepEqual(MembersUtil.getAddMemberBody(options), {
296
+ invitees: [
297
+ {
298
+ address: '1234567890',
299
+ },
300
+ ],
301
+ alertIfActive: false,
302
+ });
303
+ });
304
+
305
+ it('returns the correct body with fallback to email', () => {
306
+ const options = {
307
+ invitee: {
308
+ email: 'fallback@example.com',
309
+ },
310
+ alertIfActive: true,
311
+ };
312
+
313
+ assert.deepEqual(MembersUtil.getAddMemberBody(options), {
314
+ invitees: [
315
+ {
316
+ address: 'fallback@example.com',
317
+ },
318
+ ],
319
+ alertIfActive: true,
320
+ });
321
+ });
322
+
323
+ it('handles missing `alertIfActive` gracefully', () => {
324
+ const options = {
325
+ invitee: {
326
+ emailAddress: 'test@example.com',
327
+ roles: ['role1'],
328
+ },
329
+ };
330
+
331
+ assert.deepEqual(MembersUtil.getAddMemberBody(options), {
332
+ invitees: [
333
+ {
334
+ address: 'test@example.com',
335
+ roles: ['role1'],
336
+ },
337
+ ],
338
+ alertIfActive: undefined,
339
+ });
340
+ });
341
+
342
+ it('ignores roles if not provided', () => {
343
+ const options = {
344
+ invitee: {
345
+ emailAddress: 'test@example.com',
346
+ },
347
+ alertIfActive: false,
348
+ };
349
+
350
+ assert.deepEqual(MembersUtil.getAddMemberBody(options), {
351
+ invitees: [
352
+ {
353
+ address: 'test@example.com',
354
+ },
355
+ ],
356
+ alertIfActive: false,
357
+ });
358
+ });
359
+ });
265
360
  });
266
361
  });
@@ -15,6 +15,7 @@ describe('ClusterReachability', () => {
15
15
  let previousRTCPeerConnection;
16
16
  let clusterReachability;
17
17
  let fakePeerConnection;
18
+ let gatherIceCandidatesSpy;
18
19
 
19
20
  const emittedEvents: Record<Events, (ResultEventData | ClientMediaIpsUpdatedEventData)[]> = {
20
21
  [Events.resultReady]: [],
@@ -44,6 +45,8 @@ describe('ClusterReachability', () => {
44
45
  xtls: ['stun:xtls1.webex.com', 'stun:xtls2.webex.com:443'],
45
46
  });
46
47
 
48
+ gatherIceCandidatesSpy = sinon.spy(clusterReachability, 'gatherIceCandidates');
49
+
47
50
  resetEmittedEvents();
48
51
 
49
52
  clusterReachability.on(Events.resultReady, (data: ResultEventData) => {
@@ -151,6 +154,10 @@ describe('ClusterReachability', () => {
151
154
  assert.calledOnceWithExactly(fakePeerConnection.createOffer, {offerToReceiveAudio: true});
152
155
  assert.calledOnce(fakePeerConnection.setLocalDescription);
153
156
 
157
+ // Make sure that gatherIceCandidates is called before setLocalDescription
158
+ // as setLocalDescription triggers the ICE gathering process
159
+ assert.isTrue(gatherIceCandidatesSpy.calledBefore(fakePeerConnection.setLocalDescription));
160
+
154
161
  clusterReachability.abort();
155
162
  await promise;
156
163