@webex/plugin-meetings 3.7.0-web-workers-keepalive.1 → 3.7.0-wxcc.1

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 (49) hide show
  1. package/dist/breakouts/breakout.js +1 -1
  2. package/dist/breakouts/index.js +1 -1
  3. package/dist/common/errors/join-forbidden-error.js +52 -0
  4. package/dist/common/errors/join-forbidden-error.js.map +1 -0
  5. package/dist/constants.js +22 -2
  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/locus-info/index.js +1 -1
  10. package/dist/locus-info/index.js.map +1 -1
  11. package/dist/meeting/in-meeting-actions.js +2 -0
  12. package/dist/meeting/in-meeting-actions.js.map +1 -1
  13. package/dist/meeting/index.js +66 -41
  14. package/dist/meeting/index.js.map +1 -1
  15. package/dist/meeting-info/meeting-info-v2.js +70 -19
  16. package/dist/meeting-info/meeting-info-v2.js.map +1 -1
  17. package/dist/meeting-info/utilv2.js +1 -1
  18. package/dist/meeting-info/utilv2.js.map +1 -1
  19. package/dist/meetings/index.js +102 -53
  20. package/dist/meetings/index.js.map +1 -1
  21. package/dist/meetings/meetings.types.js +2 -0
  22. package/dist/meetings/meetings.types.js.map +1 -1
  23. package/dist/metrics/constants.js +2 -1
  24. package/dist/metrics/constants.js.map +1 -1
  25. package/dist/types/common/errors/join-forbidden-error.d.ts +15 -0
  26. package/dist/types/constants.d.ts +18 -0
  27. package/dist/types/meeting/in-meeting-actions.d.ts +2 -0
  28. package/dist/types/meeting-info/meeting-info-v2.d.ts +23 -0
  29. package/dist/types/meetings/index.d.ts +16 -1
  30. package/dist/types/meetings/meetings.types.d.ts +8 -0
  31. package/dist/types/metrics/constants.d.ts +1 -0
  32. package/dist/webinar/index.js +1 -1
  33. package/package.json +22 -22
  34. package/src/common/errors/join-forbidden-error.ts +26 -0
  35. package/src/constants.ts +20 -0
  36. package/src/locus-info/index.ts +3 -1
  37. package/src/meeting/in-meeting-actions.ts +4 -0
  38. package/src/meeting/index.ts +25 -0
  39. package/src/meeting-info/meeting-info-v2.ts +51 -0
  40. package/src/meeting-info/utilv2.ts +3 -1
  41. package/src/meetings/index.ts +72 -19
  42. package/src/meetings/meetings.types.ts +10 -0
  43. package/src/metrics/constants.ts +1 -0
  44. package/test/unit/spec/locus-info/index.js +70 -60
  45. package/test/unit/spec/meeting/in-meeting-actions.ts +2 -0
  46. package/test/unit/spec/meeting/index.js +42 -5
  47. package/test/unit/spec/meeting-info/meetinginfov2.js +37 -0
  48. package/test/unit/spec/meeting-info/utilv2.js +17 -0
  49. package/test/unit/spec/meetings/index.js +150 -13
@@ -1,5 +1,5 @@
1
1
  /* eslint no-shadow: ["error", { "allow": ["eventType"] }] */
2
- import {cloneDeep} from 'lodash';
2
+ import {cloneDeep, clone} from 'lodash';
3
3
  import '@webex/internal-plugin-mercury';
4
4
  import '@webex/internal-plugin-conversation';
5
5
  import '@webex/internal-plugin-metrics';
@@ -42,6 +42,7 @@ import {
42
42
  _ON_HOLD_LOBBY_,
43
43
  _WAIT_,
44
44
  DESTINATION_TYPE,
45
+ INITIAL_REGISTRATION_STATUS,
45
46
  } from '../constants';
46
47
  import BEHAVIORAL_METRICS from '../metrics/constants';
47
48
  import MeetingInfo from '../meeting-info';
@@ -53,12 +54,18 @@ import Request from './request';
53
54
  import PasswordError from '../common/errors/password-error';
54
55
  import CaptchaError from '../common/errors/captcha-error';
55
56
  import MeetingCollection from './collection';
56
- import {MEETING_KEY, INoiseReductionEffect, IVirtualBackgroundEffect} from './meetings.types';
57
+ import {
58
+ MEETING_KEY,
59
+ INoiseReductionEffect,
60
+ IVirtualBackgroundEffect,
61
+ MeetingRegistrationStatus,
62
+ } from './meetings.types';
57
63
  import MeetingsUtil from './util';
58
64
  import PermissionError from '../common/errors/permission';
59
65
  import JoinWebinarError from '../common/errors/join-webinar-error';
60
66
  import {SpaceIDDeprecatedError} from '../common/errors/webex-errors';
61
67
  import NoMeetingInfoError from '../common/errors/no-meeting-info';
68
+ import JoinForbiddenError from '../common/errors/join-forbidden-error';
62
69
 
63
70
  let mediaLogger;
64
71
 
@@ -179,6 +186,7 @@ export default class Meetings extends WebexPlugin {
179
186
  mediaHelpers: any;
180
187
  breakoutLocusForHandleLater: any;
181
188
  namespace = MEETINGS;
189
+ registrationStatus: MeetingRegistrationStatus;
182
190
 
183
191
  /**
184
192
  * Initializes the Meetings Plugin
@@ -692,6 +700,20 @@ export default class Meetings extends WebexPlugin {
692
700
  });
693
701
  }
694
702
 
703
+ /**
704
+ * API to change log upload interval. Setting the factor to 0 will disable periodic log uploads.
705
+ *
706
+ * @param {number} factor new factor value
707
+ * @returns {void}
708
+ */
709
+ private _setLogUploadIntervalMultiplicationFactor(factor: number) {
710
+ if (typeof factor !== 'number') {
711
+ return;
712
+ }
713
+ // @ts-ignore
714
+ this.config.logUploadIntervalMultiplicationFactor = factor;
715
+ }
716
+
695
717
  /**
696
718
  * API to toggle unified meetings
697
719
  * @param {Boolean} changeState
@@ -785,6 +807,18 @@ export default class Meetings extends WebexPlugin {
785
807
  }
786
808
  }
787
809
 
810
+ /**
811
+ * Executes a registration step and updates the registration status.
812
+ * @param {Function} step - The registration step to execute.
813
+ * @param {string} stepName - The name of the registration step.
814
+ * @returns {Promise} A promise that resolves when the step is completed.
815
+ */
816
+ executeRegistrationStep(step: () => Promise<any>, stepName: string) {
817
+ return step().then(() => {
818
+ this.registrationStatus[stepName] = true;
819
+ });
820
+ }
821
+
788
822
  /**
789
823
  * Explicitly sets up the meetings plugin by registering
790
824
  * the device, connecting to mercury, and listening for locus events.
@@ -795,6 +829,8 @@ export default class Meetings extends WebexPlugin {
795
829
  * @memberof Meetings
796
830
  */
797
831
  public register(deviceRegistrationOptions?: DeviceRegistrationOptions): Promise<any> {
832
+ this.registrationStatus = clone(INITIAL_REGISTRATION_STATUS);
833
+
798
834
  // @ts-ignore
799
835
  if (!this.webex.canAuthorize) {
800
836
  LoggerProxy.logger.error(
@@ -813,24 +849,39 @@ export default class Meetings extends WebexPlugin {
813
849
  }
814
850
 
815
851
  return Promise.all([
816
- this.fetchUserPreferredWebexSite(),
817
- this.getGeoHint(),
818
- this.startReachability('registration').catch((error) => {
819
- LoggerProxy.logger.error(`Meetings:index#register --> GDM error, ${error.message}`);
820
- }),
821
- // @ts-ignore
822
- this.webex.internal.device
823
- .register(deviceRegistrationOptions)
824
- // @ts-ignore
825
- .then(() =>
826
- LoggerProxy.logger.info(
852
+ this.executeRegistrationStep(() => this.fetchUserPreferredWebexSite(), 'fetchWebexSite'),
853
+ this.executeRegistrationStep(() => this.getGeoHint(), 'getGeoHint'),
854
+ this.executeRegistrationStep(
855
+ () =>
856
+ this.startReachability('registration').catch((error) => {
857
+ LoggerProxy.logger.error(`Meetings:index#register --> GDM error, ${error.message}`);
858
+ }),
859
+ 'startReachability'
860
+ ),
861
+ this.executeRegistrationStep(
862
+ () =>
863
+ // @ts-ignore
864
+ this.webex.internal.device
865
+ .register(deviceRegistrationOptions)
827
866
  // @ts-ignore
828
- `Meetings:index#register --> INFO, Device registered ${this.webex.internal.device.url}`
829
- )
867
+ .then(() => {
868
+ LoggerProxy.logger.info(
869
+ // @ts-ignore
870
+ `Meetings:index#register --> INFO, Device registered ${this.webex.internal.device.url}`
871
+ );
872
+ }),
873
+ 'deviceRegister'
874
+ ).then(() =>
875
+ this.executeRegistrationStep(
876
+ // @ts-ignore
877
+ () => this.webex.internal.mercury.connect(),
878
+ 'mercuryConnect'
830
879
  )
831
- // @ts-ignore
832
- .then(() => this.webex.internal.mercury.connect()),
833
- MeetingsUtil.checkH264Support.call(this),
880
+ ),
881
+ this.executeRegistrationStep(
882
+ () => Promise.resolve(MeetingsUtil.checkH264Support.call(this)),
883
+ 'checkH264Support'
884
+ ),
834
885
  ])
835
886
  .then(() => {
836
887
  this.listenForEvents();
@@ -894,6 +945,7 @@ export default class Meetings extends WebexPlugin {
894
945
  EVENT_TRIGGERS.MEETINGS_UNREGISTERED
895
946
  );
896
947
  this.registered = false;
948
+ this.registrationStatus = clone(INITIAL_REGISTRATION_STATUS);
897
949
  })
898
950
  );
899
951
  }
@@ -1412,7 +1464,8 @@ export default class Meetings extends WebexPlugin {
1412
1464
  !(err instanceof CaptchaError) &&
1413
1465
  !(err instanceof PasswordError) &&
1414
1466
  !(err instanceof PermissionError) &&
1415
- !(err instanceof JoinWebinarError)
1467
+ !(err instanceof JoinWebinarError) &&
1468
+ !(err instanceof JoinForbiddenError)
1416
1469
  ) {
1417
1470
  LoggerProxy.logger.info(
1418
1471
  `Meetings:index#createMeeting --> Info Unable to fetch meeting info for ${destination}.`
@@ -21,3 +21,13 @@ export const MEETING_KEY = {
21
21
  } as const;
22
22
 
23
23
  export type MEETING_KEY = Enum<typeof MEETING_KEY>;
24
+
25
+ // finer grained status for registration steps
26
+ export type MeetingRegistrationStatus = {
27
+ fetchWebexSite: boolean;
28
+ getGeoHint: boolean;
29
+ startReachability: boolean;
30
+ deviceRegister: boolean;
31
+ mercuryConnect: boolean;
32
+ checkH264Support: boolean;
33
+ };
@@ -73,6 +73,7 @@ const BEHAVIORAL_METRICS = {
73
73
  JOIN_WEBINAR_ERROR: 'js_sdk_join_webinar_error',
74
74
  GUEST_ENTERED_LOBBY: 'js_sdk_guest_entered_lobby',
75
75
  GUEST_EXITED_LOBBY: 'js_sdk_guest_exited_lobby',
76
+ JOIN_FORBIDDEN_ERROR: 'js_sdk_join_forbidden_error',
76
77
  };
77
78
 
78
79
  export {BEHAVIORAL_METRICS as default};
@@ -1,6 +1,6 @@
1
1
  import 'jsdom-global/register';
2
2
  import sinon from 'sinon';
3
- import {cloneDeep} from 'lodash';
3
+ import {cloneDeep, forEach} from 'lodash';
4
4
  import {assert} from '@webex/test-helper-chai';
5
5
  import MockWebex from '@webex/test-helper-mock-webex';
6
6
  import testUtils from '../../../utils/testUtils';
@@ -23,6 +23,8 @@ import {
23
23
  LOCUS,
24
24
  MEETING_STATE,
25
25
  _MEETING_,
26
+ _SIP_BRIDGE_,
27
+ _SPACE_SHARE_,
26
28
  } from '../../../../src/constants';
27
29
 
28
30
  import {self, selfWithInactivity} from './selfConstant';
@@ -102,7 +104,11 @@ describe('plugin-meetings', () => {
102
104
  },
103
105
  entryExitTone: {enabled: true, mode: 'foo'},
104
106
  video: {enabled: true},
105
- videoLayout: {overrideDefault: true, lockAttendeeViewOnStageOnly:false, stageParameters: {}},
107
+ videoLayout: {
108
+ overrideDefault: true,
109
+ lockAttendeeViewOnStageOnly: false,
110
+ stageParameters: {},
111
+ },
106
112
  webcastControl: {streaming: false},
107
113
  practiceSession: {enabled: true},
108
114
  };
@@ -529,7 +535,7 @@ describe('plugin-meetings', () => {
529
535
  manualCaptionControl: {enabled: false},
530
536
  };
531
537
 
532
- locusInfo.updateControls({manualCaptionControl: { enabled: true, }});
538
+ locusInfo.updateControls({manualCaptionControl: {enabled: true}});
533
539
 
534
540
  assert.calledWith(
535
541
  locusInfo.emitScoped,
@@ -2636,65 +2642,69 @@ describe('plugin-meetings', () => {
2636
2642
  });
2637
2643
 
2638
2644
  describe('#isMeetingActive', () => {
2639
- it('sends client event correctly for state = inactive', () => {
2640
- locusInfo.parsedLocus = {
2641
- fullState: {
2642
- type: _CALL_,
2643
- },
2644
- };
2645
-
2646
- locusInfo.fullState = {
2647
- state: LOCUS.STATE.INACTIVE,
2648
- };
2649
-
2650
- locusInfo.isMeetingActive();
2651
-
2652
- assert.calledWith(webex.internal.newMetrics.submitClientEvent, {
2653
- name: 'client.call.remote-ended',
2654
- options: {
2655
- meetingId: locusInfo.meetingId,
2656
- },
2657
- });
2658
- });
2659
-
2660
- it('sends client event correctly for state = PARTNER_LEFT', () => {
2661
- locusInfo.getLocusPartner = sinon.stub().returns({state: MEETING_STATE.STATES.LEFT});
2662
- locusInfo.parsedLocus = {
2663
- fullState: {
2664
- type: _CALL_,
2665
- },
2666
- self: {
2667
- state: MEETING_STATE.STATES.DECLINED,
2668
- },
2669
- };
2670
- locusInfo.isMeetingActive();
2671
-
2672
- assert.calledWith(webex.internal.newMetrics.submitClientEvent, {
2673
- name: 'client.call.remote-ended',
2674
- options: {
2675
- meetingId: locusInfo.meetingId,
2676
- },
2677
- });
2678
- });
2679
-
2680
- it('sends client event correctly for state = SELF_LEFT', () => {
2681
- locusInfo.getLocusPartner = sinon.stub().returns({state: MEETING_STATE.STATES.LEFT});
2682
- locusInfo.parsedLocus = {
2683
- fullState: {
2684
- type: _CALL_,
2685
- },
2686
- self: {
2687
- state: MEETING_STATE.STATES.LEFT,
2688
- },
2689
- };
2645
+ forEach([_CALL_, _SIP_BRIDGE_, _SPACE_SHARE_], (type) => {
2646
+ describe(`type = ${type}`, () => {
2647
+ it('sends client event correctly for state = inactive', () => {
2648
+ locusInfo.parsedLocus = {
2649
+ fullState: {
2650
+ type: type,
2651
+ },
2652
+ };
2653
+
2654
+ locusInfo.fullState = {
2655
+ state: LOCUS.STATE.INACTIVE,
2656
+ };
2657
+
2658
+ locusInfo.isMeetingActive();
2659
+
2660
+ assert.calledWith(webex.internal.newMetrics.submitClientEvent, {
2661
+ name: 'client.call.remote-ended',
2662
+ options: {
2663
+ meetingId: locusInfo.meetingId,
2664
+ },
2665
+ });
2666
+ });
2690
2667
 
2691
- locusInfo.isMeetingActive();
2668
+ it('sends client event correctly for state = PARTNER_LEFT', () => {
2669
+ locusInfo.getLocusPartner = sinon.stub().returns({state: MEETING_STATE.STATES.LEFT});
2670
+ locusInfo.parsedLocus = {
2671
+ fullState: {
2672
+ type: type,
2673
+ },
2674
+ self: {
2675
+ state: MEETING_STATE.STATES.DECLINED,
2676
+ },
2677
+ };
2678
+ locusInfo.isMeetingActive();
2679
+
2680
+ assert.calledWith(webex.internal.newMetrics.submitClientEvent, {
2681
+ name: 'client.call.remote-ended',
2682
+ options: {
2683
+ meetingId: locusInfo.meetingId,
2684
+ },
2685
+ });
2686
+ });
2692
2687
 
2693
- assert.calledWith(webex.internal.newMetrics.submitClientEvent, {
2694
- name: 'client.call.remote-ended',
2695
- options: {
2696
- meetingId: locusInfo.meetingId,
2697
- },
2688
+ it('sends client event correctly for state = SELF_LEFT', () => {
2689
+ locusInfo.getLocusPartner = sinon.stub().returns({state: MEETING_STATE.STATES.LEFT});
2690
+ locusInfo.parsedLocus = {
2691
+ fullState: {
2692
+ type: type,
2693
+ },
2694
+ self: {
2695
+ state: MEETING_STATE.STATES.LEFT,
2696
+ },
2697
+ };
2698
+
2699
+ locusInfo.isMeetingActive();
2700
+
2701
+ assert.calledWith(webex.internal.newMetrics.submitClientEvent, {
2702
+ name: 'client.call.remote-ended',
2703
+ options: {
2704
+ meetingId: locusInfo.meetingId,
2705
+ },
2706
+ });
2707
+ });
2698
2708
  });
2699
2709
  });
2700
2710
 
@@ -42,6 +42,7 @@ describe('plugin-meetings', () => {
42
42
  waitingForOthersToJoin: null,
43
43
  canSendReactions: null,
44
44
  canManageBreakout: null,
45
+ canStartBreakout: null,
45
46
  canBroadcastMessageToBreakout: null,
46
47
  canAdmitLobbyToBreakout: null,
47
48
  canUserAskForHelp: null,
@@ -141,6 +142,7 @@ describe('plugin-meetings', () => {
141
142
  'waitingForOthersToJoin',
142
143
  'canSendReactions',
143
144
  'canManageBreakout',
145
+ 'canStartBreakout',
144
146
  'canBroadcastMessageToBreakout',
145
147
  'canAdmitLobbyToBreakout',
146
148
  'canUserAskForHelp',
@@ -99,7 +99,7 @@ import {
99
99
  MeetingInfoV2CaptchaError,
100
100
  MeetingInfoV2PasswordError,
101
101
  MeetingInfoV2PolicyError,
102
- MeetingInfoV2JoinWebinarError,
102
+ MeetingInfoV2JoinWebinarError, MeetingInfoV2JoinForbiddenError,
103
103
  } from '../../../../src/meeting-info/meeting-info-v2';
104
104
  import {
105
105
  DTLS_HANDSHAKE_FAILED_CLIENT_CODE,
@@ -114,6 +114,7 @@ import {ERROR_DESCRIPTIONS} from '@webex/internal-plugin-metrics/src/call-diagno
114
114
  import MeetingCollection from '@webex/plugin-meetings/src/meetings/collection';
115
115
 
116
116
  import {EVENT_TRIGGERS as VOICEAEVENTS} from '@webex/internal-plugin-voicea';
117
+ import JoinForbiddenError from '../../../../src/common/errors/join-forbidden-error';
117
118
 
118
119
  describe('plugin-meetings', () => {
119
120
  const logger = {
@@ -6339,6 +6340,38 @@ describe('plugin-meetings', () => {
6339
6340
  assert.equal(meeting.passwordStatus, PASSWORD_STATUS.REQUIRED);
6340
6341
  });
6341
6342
 
6343
+ it('handles meetingInfoProvider not reach JBH', async () => {
6344
+ meeting.destination = FAKE_DESTINATION;
6345
+ meeting.destinationType = FAKE_TYPE;
6346
+ meeting.attrs.meetingInfoProvider = {
6347
+ fetchMeetingInfo: sinon
6348
+ .stub()
6349
+ .throws(new MeetingInfoV2JoinForbiddenError(403003, FAKE_MEETING_INFO)),
6350
+ };
6351
+
6352
+ await assert.isRejected(meeting.fetchMeetingInfo({sendCAevents: true}), JoinForbiddenError);
6353
+
6354
+ assert.calledWith(
6355
+ meeting.attrs.meetingInfoProvider.fetchMeetingInfo,
6356
+ FAKE_DESTINATION,
6357
+ FAKE_TYPE,
6358
+ null,
6359
+ null,
6360
+ undefined,
6361
+ 'locus-id',
6362
+ {},
6363
+ {meetingId: meeting.id, sendCAevents: true}
6364
+ );
6365
+
6366
+ assert.deepEqual(meeting.meetingInfo, FAKE_MEETING_INFO);
6367
+ assert.equal(meeting.meetingInfoFailureCode, 403003);
6368
+ assert.equal(
6369
+ meeting.meetingInfoFailureReason,
6370
+ MEETING_INFO_FAILURE_REASON.NOT_REACH_JBH
6371
+ );
6372
+ assert.equal(meeting.requiredCaptcha, null);
6373
+ });
6374
+
6342
6375
  it('handles meetingInfoProvider policy error', async () => {
6343
6376
  meeting.destination = FAKE_DESTINATION;
6344
6377
  meeting.destinationType = FAKE_TYPE;
@@ -9996,7 +10029,8 @@ describe('plugin-meetings', () => {
9996
10029
  describe('#closePeerConnections', () => {
9997
10030
  it('should close the webrtc media connection, and return a promise', async () => {
9998
10031
  const setNetworkStatusSpy = sinon.spy(meeting, 'setNetworkStatus');
9999
- meeting.mediaProperties.webrtcMediaConnection = {close: sinon.stub()};
10032
+ const fakeWebrtcMediaConnection = {close: sinon.stub()};
10033
+ meeting.mediaProperties.webrtcMediaConnection = fakeWebrtcMediaConnection;
10000
10034
 
10001
10035
  meeting.audio = {id: 'fakeAudioMuteState'};
10002
10036
  meeting.video = {id: 'fakeVideoMuteState'};
@@ -10005,15 +10039,17 @@ describe('plugin-meetings', () => {
10005
10039
 
10006
10040
  assert.exists(pcs.then);
10007
10041
  await pcs;
10008
- assert.calledOnce(meeting.mediaProperties.webrtcMediaConnection.close);
10042
+ assert.calledOnce(fakeWebrtcMediaConnection.close);
10009
10043
  assert.calledOnceWithExactly(setNetworkStatusSpy, undefined);
10010
10044
  assert.equal(meeting.audio, null);
10011
10045
  assert.equal(meeting.video, null);
10046
+ assert.equal(meeting.mediaProperties.webrtcMediaConnection, null);
10012
10047
  });
10013
10048
 
10014
10049
  it('should close the webrtc media connection, but keep audio and video props unchanged if called with resetMuteStates=false', async () => {
10015
10050
  const setNetworkStatusSpy = sinon.spy(meeting, 'setNetworkStatus');
10016
- meeting.mediaProperties.webrtcMediaConnection = {close: sinon.stub()};
10051
+ const fakeWebrtcMediaConnection = {close: sinon.stub()};
10052
+ meeting.mediaProperties.webrtcMediaConnection = fakeWebrtcMediaConnection;
10017
10053
 
10018
10054
  const fakeAudio = {id: 'fakeAudioMuteState'};
10019
10055
  const fakeVideo = {id: 'fakeVideoMuteState'};
@@ -10023,10 +10059,11 @@ describe('plugin-meetings', () => {
10023
10059
 
10024
10060
  await meeting.closePeerConnections(false);
10025
10061
 
10026
- assert.calledOnce(meeting.mediaProperties.webrtcMediaConnection.close);
10062
+ assert.calledOnce(fakeWebrtcMediaConnection.close);
10027
10063
  assert.calledOnceWithExactly(setNetworkStatusSpy, undefined);
10028
10064
  assert.equal(meeting.audio, fakeAudio);
10029
10065
  assert.equal(meeting.video, fakeVideo);
10066
+ assert.equal(meeting.mediaProperties.webrtcMediaConnection, null);
10030
10067
  });
10031
10068
  });
10032
10069
 
@@ -19,6 +19,7 @@ import MeetingInfo, {
19
19
  MeetingInfoV2AdhocMeetingError,
20
20
  MeetingInfoV2PolicyError,
21
21
  MeetingInfoV2JoinWebinarError,
22
+ MeetingInfoV2JoinForbiddenError,
22
23
  } from '@webex/plugin-meetings/src/meeting-info/meeting-info-v2';
23
24
  import MeetingInfoUtil from '@webex/plugin-meetings/src/meeting-info/utilv2';
24
25
  import Metrics from '@webex/plugin-meetings/src/metrics';
@@ -930,6 +931,42 @@ describe('plugin-meetings', () => {
930
931
  });
931
932
  }
932
933
  );
934
+
935
+ forEach(
936
+ [
937
+ {errorCode: 403003},
938
+ ],
939
+ ({errorCode}) => {
940
+ it(`should throw a MeetingInfoV2JoinForbiddenError for error code ${errorCode}`, async () => {
941
+ const message = 'a message';
942
+ const meetingInfoData = 'meeting info';
943
+
944
+ webex.request = sinon.stub().rejects({
945
+ statusCode: 403,
946
+ body: {message, code: errorCode, data: {meetingInfo: meetingInfoData}},
947
+ });
948
+ try {
949
+ await meetingInfo.fetchMeetingInfo('1234323', DESTINATION_TYPE.MEETING_ID, 'abc', {
950
+ id: '999',
951
+ code: 'aabbcc11',
952
+ });
953
+ } catch (err) {
954
+ assert.instanceOf(err, MeetingInfoV2JoinForbiddenError);
955
+ assert.deepEqual(err.message, `${message}, code=${errorCode}`);
956
+ assert.equal(err.wbxAppApiCode, errorCode);
957
+ assert.deepEqual(err.meetingInfo, meetingInfoData);
958
+
959
+ assert(Metrics.sendBehavioralMetric.calledOnce);
960
+ assert.calledWith(
961
+ Metrics.sendBehavioralMetric,
962
+ BEHAVIORAL_METRICS.JOIN_FORBIDDEN_ERROR,
963
+ {code: errorCode}
964
+ );
965
+
966
+ }
967
+ });
968
+ }
969
+ );
933
970
  });
934
971
  });
935
972
  });
@@ -345,5 +345,22 @@ describe('plugin-meetings', () => {
345
345
  );
346
346
  });
347
347
  });
348
+
349
+ describe('#isMeetingLink', () => {
350
+ it('should return true for valid join meeting link with MTID', () => {
351
+ const result = MeetingInfoUtil.isMeetingLink('https://cisco.webex.com/cisco/j.php?MTID=m9fe0afd8c435e892afcce9ea25b97046');
352
+ expect(result).to.be.true;
353
+ });
354
+
355
+ it('should return true for valid join meeting link without cisco domain', () => {
356
+ const result = MeetingInfoUtil.isMeetingLink('https://test.webex.com/test/j.php?MTID=m9fe0afd8c435e892afcce9ea25b97046');
357
+ expect(result).to.be.true;
358
+ });
359
+
360
+ it('should return false for an invalid meeting link', () => {
361
+ const result = MeetingInfoUtil.isMeetingLink('https://test.webex.com/test/j.php?MiD=m9fe0afd8c435e892afcce9ea25b97046');
362
+ expect(result).to.be.false;
363
+ });
364
+ });
348
365
  });
349
366
  });