@webex/plugin-meetings 1.151.7 → 1.152.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.
@@ -3,6 +3,52 @@ import {HTTP_VERBS} from '../constants';
3
3
 
4
4
  import MeetingInfoUtil from './utilv2';
5
5
 
6
+ const PASSWORD_ERROR_DEFAULT_MESSAGE = 'Password required. Call fetchMeetingInfo() with password argument';
7
+ const CAPTCHA_ERROR_DEFAULT_MESSAGE = 'Captcha required. Call fetchMeetingInfo() with captchaInfo argument';
8
+
9
+ /**
10
+ * Error to indicate that wbxappapi requires a password
11
+ */
12
+ export class MeetingInfoV2PasswordError extends Error {
13
+ /**
14
+ *
15
+ * @constructor
16
+ * @param {Number} [wbxAppApiErrorCode]
17
+ * @param {Object} [meetingInfo]
18
+ * @param {String} [message]
19
+ */
20
+ constructor(wbxAppApiErrorCode, meetingInfo, message = PASSWORD_ERROR_DEFAULT_MESSAGE) {
21
+ super(`${message}, code=${wbxAppApiErrorCode}`);
22
+ this.name = 'MeetingInfoV2PasswordError';
23
+ this.sdkMessage = message;
24
+ this.stack = (new Error()).stack;
25
+ this.wbxAppApiCode = wbxAppApiErrorCode;
26
+ this.meetingInfo = meetingInfo;
27
+ }
28
+ }
29
+
30
+ /**
31
+ * Error to indicate that wbxappapi requires a captcha
32
+ */
33
+ export class MeetingInfoV2CaptchaError extends Error {
34
+ /**
35
+ *
36
+ * @constructor
37
+ * @param {Number} [wbxAppApiErrorCode]
38
+ * @param {Object} [captchaInfo]
39
+ * @param {String} [message]
40
+ */
41
+ constructor(wbxAppApiErrorCode, captchaInfo, message = CAPTCHA_ERROR_DEFAULT_MESSAGE) {
42
+ super(`${message}, code=${wbxAppApiErrorCode}`);
43
+ this.name = 'MeetingInfoV2PasswordError';
44
+ this.sdkMessage = message;
45
+ this.stack = (new Error()).stack;
46
+ this.wbxAppApiCode = wbxAppApiErrorCode;
47
+ this.isPasswordRequired = wbxAppApiErrorCode === 423005;
48
+ this.captchaInfo = captchaInfo;
49
+ }
50
+ }
51
+
6
52
  /**
7
53
  * @class MeetingInfo
8
54
  */
@@ -35,24 +81,42 @@ export default class MeetingInfoV2 {
35
81
  * Fetches meeting info from the server
36
82
  * @param {String} destination one of many different types of destinations to look up info for
37
83
  * @param {String} [type] to match up with the destination value
84
+ * @param {String} password
85
+ * @param {Object} captchaInfo
86
+ * @param {String} captchaInfo.code
87
+ * @param {String} captchaInfo.id
38
88
  * @returns {Promise} returns a meeting info object
39
89
  * @public
40
90
  * @memberof MeetingInfo
41
91
  */
42
- async fetchMeetingInfo(destination, type = null) {
92
+ async fetchMeetingInfo(destination, type = null, password = null, captchaInfo = null) {
43
93
  const destinationType = await MeetingInfoUtil.getDestinationType({
44
94
  destination,
45
95
  type,
46
96
  webex: this.webex
47
97
  });
48
- const body = await MeetingInfoUtil.getRequestBody(destinationType);
98
+ const body = await MeetingInfoUtil.getRequestBody({...destinationType, password, captchaInfo});
49
99
 
50
100
  return this.webex.request({
51
101
  method: HTTP_VERBS.POST,
52
102
  service: 'webex-appapi-service',
53
103
  resource: 'meetingInfo',
54
104
  body
55
- });
105
+ })
106
+ .catch((err) => {
107
+ if (err?.statusCode === 403) {
108
+ throw new MeetingInfoV2PasswordError(err.body?.code, err.body?.data?.meetingInfo);
109
+ }
110
+ if (err?.statusCode === 423) {
111
+ throw new MeetingInfoV2CaptchaError(err.body?.code, {
112
+ captchaId: err.body.captchaID,
113
+ verificationImageURL: err.body.verificationImageURL,
114
+ verificationAudioURL: err.body.verificationAudioURL,
115
+ refreshURL: err.body.refreshURL
116
+ });
117
+ }
118
+ throw err;
119
+ });
56
120
  }
57
121
  }
58
122
 
@@ -212,7 +212,9 @@ MeetingInfoUtil.getDestinationType = async (from) => {
212
212
  * @returns {Object} returns an object with {resource, method}
213
213
  */
214
214
  MeetingInfoUtil.getRequestBody = (options) => {
215
- const {type, destination} = options;
215
+ const {
216
+ type, destination, password, captchaInfo
217
+ } = options;
216
218
  const body = {
217
219
  supportHostKey: true
218
220
  };
@@ -250,6 +252,15 @@ MeetingInfoUtil.getRequestBody = (options) => {
250
252
  default:
251
253
  }
252
254
 
255
+ if (password) {
256
+ body.password = password;
257
+ }
258
+
259
+ if (captchaInfo) {
260
+ body.captchaID = captchaInfo.id;
261
+ body.captchaVerifyCode = captchaInfo.code;
262
+ }
263
+
253
264
  return body;
254
265
  };
255
266
 
@@ -48,6 +48,8 @@ import Reachability from '../reachability';
48
48
  import Request from '../meetings/request';
49
49
  import StatsAnalyzer from '../analyzer/analyzer';
50
50
  import StatsCalculator from '../analyzer/calculator';
51
+ import PasswordError from '../common/errors/password-error';
52
+ import CaptchaError from '../common/errors/captcha-error';
51
53
 
52
54
  import MeetingCollection from './collection';
53
55
  import MeetingsUtil from './util';
@@ -716,7 +718,8 @@ export default class Meetings extends WebexPlugin {
716
718
  deviceUrl: this.webex.internal.device.url,
717
719
  orgId: this.webex.internal.device.orgId,
718
720
  roapSeq: 0,
719
- locus: type === _LOCUS_ID_ ? destination : null // pass the locus object if present
721
+ locus: type === _LOCUS_ID_ ? destination : null, // pass the locus object if present
722
+ meetingInfoProvider: this.meetingInfo
720
723
  },
721
724
  {
722
725
  parent: this.webex
@@ -726,15 +729,14 @@ export default class Meetings extends WebexPlugin {
726
729
  this.meetingCollection.set(meeting);
727
730
 
728
731
  try {
729
- const info = await this.meetingInfo.fetchMeetingInfo(destination, type);
730
-
731
- meeting.parseMeetingInfo(info);
732
- meeting.meetingInfo = info ? info.body : null;
732
+ await meeting.fetchMeetingInfo({destination, type});
733
733
  }
734
734
  catch (err) {
735
- // if there is no meeting info we assume its a 1:1 call or wireless share
736
- LoggerProxy.logger.info(`Meetings:index#createMeeting --> Info Unable to fetch meeting info for ${destination}.`);
737
- LoggerProxy.logger.info('Meetings:index#createMeeting --> Info assuming this destination is a 1:1 or wireless share');
735
+ if (!(err instanceof CaptchaError) && !(err instanceof PasswordError)) {
736
+ // if there is no meeting info we assume its a 1:1 call or wireless share
737
+ LoggerProxy.logger.info(`Meetings:index#createMeeting --> Info Unable to fetch meeting info for ${destination}.`);
738
+ LoggerProxy.logger.info('Meetings:index#createMeeting --> Info assuming this destination is a 1:1 or wireless share');
739
+ }
738
740
  LoggerProxy.logger.debug(`Meetings:index#createMeeting --> Debug ${err} fetching /meetingInfo for creation.`);
739
741
  // We need to save this info for future reference
740
742
  meeting.destination = destination;
@@ -31,8 +31,11 @@ import {
31
31
  FLOOR_ACTION,
32
32
  SHARE_STATUS,
33
33
  METRICS_OPERATIONAL_MEASURES,
34
+ MEETING_INFO_FAILURE_REASON,
35
+ PASSWORD_STATUS,
34
36
  EVENTS,
35
- EVENT_TRIGGERS
37
+ EVENT_TRIGGERS,
38
+ _SIP_URI_
36
39
  } from '@webex/plugin-meetings/src/constants';
37
40
 
38
41
  import {
@@ -43,8 +46,11 @@ import {
43
46
  } from '../../../../src/common/errors/webex-errors';
44
47
  import WebExMeetingsErrors from '../../../../src/common/errors/webex-meetings-error';
45
48
  import ParameterError from '../../../../src/common/errors/parameter';
49
+ import PasswordError from '../../../../src/common/errors/password-error';
50
+ import CaptchaError from '../../../../src/common/errors/captcha-error';
46
51
  import DefaultSDKConfig from '../../../../src/config';
47
52
  import testUtils from '../../../utils/testUtils';
53
+ import {MeetingInfoV2CaptchaError, MeetingInfoV2PasswordError} from '../../../../src/meeting-info/meeting-info-v2';
48
54
 
49
55
  const {
50
56
  getBrowserName
@@ -98,7 +104,8 @@ describe('plugin-meetings', () => {
98
104
  },
99
105
  mediaSettings: {},
100
106
  metrics: {},
101
- stats: {}
107
+ stats: {},
108
+ experimental: {enableUnifiedMeetings: true}
102
109
  }
103
110
  }
104
111
  });
@@ -173,6 +180,9 @@ describe('plugin-meetings', () => {
173
180
  assert.instanceOf(meeting.meetingRequest, MeetingRequest);
174
181
  assert.instanceOf(meeting.locusInfo, LocusInfo);
175
182
  assert.instanceOf(meeting.mediaProperties, MediaProperties);
183
+ assert.equal(meeting.passwordStatus, PASSWORD_STATUS.UNKNOWN);
184
+ assert.equal(meeting.requiredCaptcha, null);
185
+ assert.equal(meeting.meetingInfoFailureReason, undefined);
176
186
  });
177
187
  });
178
188
  describe('#invite', () => {
@@ -653,6 +663,14 @@ describe('plugin-meetings', () => {
653
663
  });
654
664
  });
655
665
  });
666
+ it('should fail if password is required', async () => {
667
+ meeting.passwordStatus = PASSWORD_STATUS.REQUIRED;
668
+ await assert.isRejected(meeting.join(), PasswordError);
669
+ });
670
+ it('should fail if captcha is required', async () => {
671
+ meeting.requiredCaptcha = {captchaId: 'aaa'};
672
+ await assert.isRejected(meeting.join(), CaptchaError);
673
+ });
656
674
  describe('total failure', () => {
657
675
  beforeEach(() => {
658
676
  MeetingUtil.isPinOrGuest = sinon.stub().returns(false);
@@ -2010,6 +2028,279 @@ describe('plugin-meetings', () => {
2010
2028
  });
2011
2029
  });
2012
2030
 
2031
+ describe('#fetchMeetingInfo', () => {
2032
+ const FAKE_DESTINATION = 'something@somecompany.com';
2033
+ const FAKE_TYPE = _SIP_URI_;
2034
+ const FAKE_PASSWORD = '123abc';
2035
+ const FAKE_CAPTCHA_CODE = 'a1b2c3XYZ';
2036
+ const FAKE_CAPTCHA_ID = '987654321';
2037
+ const FAKE_CAPTCHA_IMAGE_URL = 'http://captchaimage';
2038
+ const FAKE_CAPTCHA_AUDIO_URL = 'http://captchaaudio';
2039
+ const FAKE_CAPTCHA_REFRESH_URL = 'http://captcharefresh';
2040
+ const FAKE_MEETING_INFO = {
2041
+ conversationUrl: 'some_convo_url',
2042
+ locusUrl: 'some_locus_url',
2043
+ sipUrl: 'some_sip_url', // or sipMeetingUri
2044
+ meetingNumber: '123456', // this.config.experimental.enableUnifiedMeetings
2045
+ hostId: 'some_host_id' // this.owner;
2046
+ };
2047
+ const FAKE_SDK_CAPTCHA_INFO = {
2048
+ captchaId: FAKE_CAPTCHA_ID,
2049
+ verificationImageURL: FAKE_CAPTCHA_IMAGE_URL,
2050
+ verificationAudioURL: FAKE_CAPTCHA_AUDIO_URL,
2051
+ refreshURL: FAKE_CAPTCHA_REFRESH_URL
2052
+ };
2053
+ const FAKE_WBXAPPAPI_CAPTCHA_INFO = {
2054
+ captchaID: `${FAKE_CAPTCHA_ID}-2`,
2055
+ verificationImageURL: `${FAKE_CAPTCHA_IMAGE_URL}-2`,
2056
+ verificationAudioURL: `${FAKE_CAPTCHA_AUDIO_URL}-2`,
2057
+ refreshURL: `${FAKE_CAPTCHA_REFRESH_URL}-2`
2058
+ };
2059
+
2060
+
2061
+ it('calls meetingInfoProvider with all the right parameters and parses the result', async () => {
2062
+ meeting.attrs.meetingInfoProvider = {fetchMeetingInfo: sinon.stub().resolves({body: FAKE_MEETING_INFO})};
2063
+ meeting.requiredCaptcha = FAKE_SDK_CAPTCHA_INFO;
2064
+ await meeting.fetchMeetingInfo({
2065
+ destination: FAKE_DESTINATION, type: FAKE_TYPE, password: FAKE_PASSWORD, captchaCode: FAKE_CAPTCHA_CODE
2066
+ });
2067
+
2068
+ assert.calledWith(meeting.attrs.meetingInfoProvider.fetchMeetingInfo, FAKE_DESTINATION, FAKE_TYPE, FAKE_PASSWORD, {code: FAKE_CAPTCHA_CODE, id: FAKE_CAPTCHA_ID});
2069
+
2070
+ assert.deepEqual(meeting.meetingInfo, FAKE_MEETING_INFO);
2071
+ assert.equal(meeting.passwordStatus, PASSWORD_STATUS.NOT_REQUIRED);
2072
+ assert.equal(meeting.meetingInfoFailureReason, MEETING_INFO_FAILURE_REASON.NONE);
2073
+ assert.equal(meeting.requiredCaptcha, null);
2074
+ });
2075
+
2076
+ it('fails if captchaCode is provided when captcha not needed', async () => {
2077
+ meeting.attrs.meetingInfoProvider = {fetchMeetingInfo: sinon.stub().resolves({body: FAKE_MEETING_INFO})};
2078
+ meeting.requiredCaptcha = null;
2079
+
2080
+ await assert.isRejected(meeting.fetchMeetingInfo({
2081
+ destination: FAKE_DESTINATION, type: FAKE_TYPE, captchaCode: FAKE_CAPTCHA_CODE
2082
+ }), Error, 'fetchMeetingInfo() called with captchaCode when captcha was not required');
2083
+
2084
+ assert.notCalled(meeting.attrs.meetingInfoProvider.fetchMeetingInfo);
2085
+ });
2086
+
2087
+ it('fails if password is provided when not required', async () => {
2088
+ meeting.attrs.meetingInfoProvider = {fetchMeetingInfo: sinon.stub().resolves({body: FAKE_MEETING_INFO})};
2089
+ meeting.passwordStatus = PASSWORD_STATUS.NOT_REQUIRED;
2090
+
2091
+ await assert.isRejected(meeting.fetchMeetingInfo({
2092
+ destination: FAKE_DESTINATION, type: FAKE_TYPE, password: FAKE_PASSWORD
2093
+ }), Error, 'fetchMeetingInfo() called with password when password was not required');
2094
+
2095
+ assert.notCalled(meeting.attrs.meetingInfoProvider.fetchMeetingInfo);
2096
+ });
2097
+
2098
+ it('handles meetingInfoProvider requiring password', async () => {
2099
+ meeting.attrs.meetingInfoProvider = {
2100
+ fetchMeetingInfo: sinon.stub().throws(new MeetingInfoV2PasswordError(403004, FAKE_MEETING_INFO))
2101
+ };
2102
+
2103
+ await assert.isRejected(meeting.fetchMeetingInfo({
2104
+ destination: FAKE_DESTINATION, type: FAKE_TYPE
2105
+ }), PasswordError);
2106
+
2107
+ assert.calledWith(meeting.attrs.meetingInfoProvider.fetchMeetingInfo, FAKE_DESTINATION, FAKE_TYPE, null, null);
2108
+
2109
+ assert.deepEqual(meeting.meetingInfo, FAKE_MEETING_INFO);
2110
+ assert.equal(meeting.meetingInfoFailureReason, MEETING_INFO_FAILURE_REASON.WRONG_PASSWORD);
2111
+ assert.equal(meeting.requiredCaptcha, null);
2112
+ assert.equal(meeting.passwordStatus, PASSWORD_STATUS.REQUIRED);
2113
+ });
2114
+
2115
+ it('handles meetingInfoProvider requiring captcha because of wrong password', async () => {
2116
+ meeting.attrs.meetingInfoProvider = {
2117
+ fetchMeetingInfo: sinon.stub().throws(new MeetingInfoV2CaptchaError(423005, FAKE_SDK_CAPTCHA_INFO))
2118
+ };
2119
+ meeting.requiredCaptcha = null;
2120
+
2121
+ await assert.isRejected(meeting.fetchMeetingInfo({
2122
+ destination: FAKE_DESTINATION, type: FAKE_TYPE, password: 'aaa'
2123
+ }), CaptchaError);
2124
+
2125
+ assert.calledWith(meeting.attrs.meetingInfoProvider.fetchMeetingInfo, FAKE_DESTINATION, FAKE_TYPE, 'aaa', null);
2126
+
2127
+ assert.deepEqual(meeting.meetingInfo, {});
2128
+ assert.equal(meeting.meetingInfoFailureReason, MEETING_INFO_FAILURE_REASON.WRONG_PASSWORD);
2129
+ assert.equal(meeting.passwordStatus, PASSWORD_STATUS.REQUIRED);
2130
+ assert.deepEqual(meeting.requiredCaptcha, {
2131
+ captchaId: FAKE_CAPTCHA_ID,
2132
+ verificationImageURL: FAKE_CAPTCHA_IMAGE_URL,
2133
+ verificationAudioURL: FAKE_CAPTCHA_AUDIO_URL,
2134
+ refreshURL: FAKE_CAPTCHA_REFRESH_URL
2135
+ });
2136
+ });
2137
+
2138
+ it('handles meetingInfoProvider requiring captcha because of wrong captcha', async () => {
2139
+ meeting.attrs.meetingInfoProvider = {
2140
+ fetchMeetingInfo: sinon.stub().throws(new MeetingInfoV2CaptchaError(423005, FAKE_SDK_CAPTCHA_INFO))
2141
+ };
2142
+ meeting.requiredCaptcha = FAKE_SDK_CAPTCHA_INFO;
2143
+
2144
+ await assert.isRejected(meeting.fetchMeetingInfo({
2145
+ destination: FAKE_DESTINATION, type: FAKE_TYPE, password: 'aaa', captchaCode: 'bbb'
2146
+ }), CaptchaError);
2147
+
2148
+ assert.calledWith(meeting.attrs.meetingInfoProvider.fetchMeetingInfo, FAKE_DESTINATION, FAKE_TYPE, 'aaa', {code: 'bbb', id: FAKE_CAPTCHA_ID});
2149
+
2150
+ assert.deepEqual(meeting.meetingInfo, {});
2151
+ assert.equal(meeting.meetingInfoFailureReason, MEETING_INFO_FAILURE_REASON.WRONG_CAPTCHA);
2152
+ assert.equal(meeting.passwordStatus, PASSWORD_STATUS.REQUIRED);
2153
+ assert.deepEqual(meeting.requiredCaptcha, FAKE_SDK_CAPTCHA_INFO);
2154
+ });
2155
+
2156
+ it('handles successful response when good password is passed', async () => {
2157
+ meeting.attrs.meetingInfoProvider = {
2158
+ fetchMeetingInfo: sinon.stub().resolves(
2159
+ {
2160
+ statusCode: 200,
2161
+ body: FAKE_MEETING_INFO
2162
+ }
2163
+ )
2164
+ };
2165
+ meeting.passwordStatus = PASSWORD_STATUS.REQUIRED;
2166
+
2167
+ await meeting.fetchMeetingInfo({
2168
+ destination: FAKE_DESTINATION, type: FAKE_TYPE, password: 'aaa'
2169
+ });
2170
+
2171
+ assert.calledWith(meeting.attrs.meetingInfoProvider.fetchMeetingInfo, FAKE_DESTINATION, FAKE_TYPE, 'aaa', null);
2172
+
2173
+ assert.deepEqual(meeting.meetingInfo, FAKE_MEETING_INFO);
2174
+ assert.equal(meeting.meetingInfoFailureReason, MEETING_INFO_FAILURE_REASON.NONE);
2175
+ assert.equal(meeting.passwordStatus, PASSWORD_STATUS.VERIFIED);
2176
+ assert.equal(meeting.requiredCaptcha, null);
2177
+ });
2178
+
2179
+ it('refreshes captcha when captcha was required and we received 403 error code', async () => {
2180
+ const refreshedCaptcha = {
2181
+ captchaID: FAKE_WBXAPPAPI_CAPTCHA_INFO.captchaID,
2182
+ verificationImageURL: FAKE_WBXAPPAPI_CAPTCHA_INFO.verificationImageURL,
2183
+ verificationAudioURL: FAKE_WBXAPPAPI_CAPTCHA_INFO.verificationAudioURL
2184
+ };
2185
+
2186
+ meeting.attrs.meetingInfoProvider = {
2187
+ fetchMeetingInfo: sinon.stub().throws(new MeetingInfoV2PasswordError(403004, FAKE_MEETING_INFO))
2188
+ };
2189
+ meeting.meetingRequest.refreshCaptcha = sinon.stub().returns(Promise.resolve(
2190
+ {
2191
+ body: refreshedCaptcha
2192
+ }
2193
+ ));
2194
+ meeting.passwordStatus = PASSWORD_STATUS.REQUIRED;
2195
+ meeting.requiredCaptcha = FAKE_SDK_CAPTCHA_INFO;
2196
+
2197
+ await assert.isRejected(meeting.fetchMeetingInfo({
2198
+ destination: FAKE_DESTINATION, type: FAKE_TYPE, password: 'aaa', captchaCode: 'bbb'
2199
+ }));
2200
+
2201
+ assert.calledWith(meeting.attrs.meetingInfoProvider.fetchMeetingInfo, FAKE_DESTINATION, FAKE_TYPE, 'aaa', {code: 'bbb', id: FAKE_CAPTCHA_ID});
2202
+
2203
+ assert.deepEqual(meeting.meetingInfo, FAKE_MEETING_INFO);
2204
+ assert.equal(meeting.meetingInfoFailureReason, MEETING_INFO_FAILURE_REASON.WRONG_PASSWORD);
2205
+ assert.equal(meeting.passwordStatus, PASSWORD_STATUS.REQUIRED);
2206
+ assert.deepEqual(meeting.requiredCaptcha, {
2207
+ captchaId: refreshedCaptcha.captchaID,
2208
+ verificationImageURL: refreshedCaptcha.verificationImageURL,
2209
+ verificationAudioURL: refreshedCaptcha.verificationAudioURL,
2210
+ refreshURL: FAKE_SDK_CAPTCHA_INFO.refreshURL // refresh url doesn't change
2211
+ });
2212
+ });
2213
+ });
2214
+
2215
+ describe('#refreshCaptcha', () => {
2216
+ it('fails if no captcha required', async () => {
2217
+ assert.isRejected(meeting.refreshCaptcha(), Error);
2218
+ });
2219
+ it('sends correct request to captcha service refresh url', async () => {
2220
+ const REFRESH_URL = 'https://something.webex.com/captchaservice/v1/captchas/refresh?blablabla=something&captchaID=xxx';
2221
+ const EXPECTED_REFRESH_URL = 'https://something.webex.com/captchaservice/v1/captchas/refresh?blablabla=something&captchaID=xxx&siteFullName=something.webex.com';
2222
+
2223
+ const FAKE_SDK_CAPTCHA_INFO = {
2224
+ captchaId: 'some id',
2225
+ verificationImageURL: 'some image url',
2226
+ verificationAudioURL: 'some audio url',
2227
+ refreshURL: REFRESH_URL
2228
+ };
2229
+
2230
+ const FAKE_REFRESHED_CAPTCHA = {
2231
+ captchaID: 'some id',
2232
+ verificationImageURL: 'some image url',
2233
+ verificationAudioURL: 'some audio url'
2234
+ };
2235
+
2236
+ // setup the meeting so that a captcha is required
2237
+ meeting.attrs.meetingInfoProvider = {
2238
+ fetchMeetingInfo: sinon.stub().throws(new MeetingInfoV2CaptchaError(423005, FAKE_SDK_CAPTCHA_INFO))
2239
+ };
2240
+
2241
+ await assert.isRejected(meeting.fetchMeetingInfo({
2242
+ destination: 'something@somecompany.com', type: _SIP_URI_, password: ''
2243
+ }), CaptchaError);
2244
+
2245
+ assert.deepEqual(meeting.requiredCaptcha, FAKE_SDK_CAPTCHA_INFO);
2246
+ meeting.meetingRequest.refreshCaptcha = sinon.stub().returns(Promise.resolve({body: FAKE_REFRESHED_CAPTCHA}));
2247
+
2248
+ // test the captcha refresh
2249
+ await meeting.refreshCaptcha();
2250
+
2251
+ assert.calledWith(meeting.meetingRequest.refreshCaptcha,
2252
+ {
2253
+ captchaRefreshUrl: EXPECTED_REFRESH_URL,
2254
+ captchaId: FAKE_SDK_CAPTCHA_INFO.captchaId
2255
+ });
2256
+
2257
+ assert.deepEqual(meeting.requiredCaptcha, {
2258
+ captchaId: FAKE_REFRESHED_CAPTCHA.captchaID,
2259
+ verificationImageURL: FAKE_REFRESHED_CAPTCHA.verificationImageURL,
2260
+ verificationAudioURL: FAKE_REFRESHED_CAPTCHA.verificationAudioURL,
2261
+ refreshURL: FAKE_SDK_CAPTCHA_INFO.refreshURL // refresh url doesn't change
2262
+ });
2263
+ });
2264
+ });
2265
+
2266
+ describe('#verifyPassword', () => {
2267
+ it('calls fetchMeetingInfo() with the passed password and captcha code', async () => {
2268
+ // simulate successful case
2269
+ meeting.fetchMeetingInfo = sinon.stub().resolves();
2270
+ const result = await meeting.verifyPassword('password', 'captcha id');
2271
+
2272
+ assert.equal(result.isPasswordValid, true);
2273
+ assert.equal(result.requiredCaptcha, null);
2274
+ assert.equal(result.failureReason, MEETING_INFO_FAILURE_REASON.NONE);
2275
+ });
2276
+ it('handles PasswordError returned by fetchMeetingInfo', async () => {
2277
+ meeting.fetchMeetingInfo = sinon.stub().callsFake(() => {
2278
+ meeting.meetingInfoFailureReason = MEETING_INFO_FAILURE_REASON.WRONG_PASSWORD;
2279
+
2280
+ return Promise.reject(new PasswordError());
2281
+ });
2282
+ const result = await meeting.verifyPassword('password', 'captcha id');
2283
+
2284
+ assert.equal(result.isPasswordValid, false);
2285
+ assert.equal(result.requiredCaptcha, null);
2286
+ assert.equal(result.failureReason, MEETING_INFO_FAILURE_REASON.WRONG_PASSWORD);
2287
+ });
2288
+ it('handles CaptchaError returned by fetchMeetingInfo', async () => {
2289
+ const FAKE_CAPTCHA = {captchaId: 'some catcha id...'};
2290
+
2291
+ meeting.fetchMeetingInfo = sinon.stub().callsFake(() => {
2292
+ meeting.meetingInfoFailureReason = MEETING_INFO_FAILURE_REASON.WRONG_CAPTCHA;
2293
+ meeting.requiredCaptcha = FAKE_CAPTCHA;
2294
+
2295
+ return Promise.reject(new CaptchaError());
2296
+ });
2297
+ const result = await meeting.verifyPassword('password', 'captcha id');
2298
+
2299
+ assert.equal(result.isPasswordValid, false);
2300
+ assert.deepEqual(result.requiredCaptcha, FAKE_CAPTCHA);
2301
+ assert.equal(result.failureReason, MEETING_INFO_FAILURE_REASON.WRONG_CAPTCHA);
2302
+ });
2303
+ });
2013
2304
  describe('#mediaNegotiatedEvent', () => {
2014
2305
  it('should have #mediaNegotiatedEvent', () => {
2015
2306
  assert.exists(meeting.mediaNegotiatedEvent);
@@ -12,7 +12,7 @@ import {
12
12
  _MEETING_ID_,
13
13
  _PERSONAL_ROOM_
14
14
  } from '@webex/plugin-meetings/src/constants';
15
- import MeetingInfo from '@webex/plugin-meetings/src/meeting-info/meeting-info-v2';
15
+ import MeetingInfo, {MeetingInfoV2PasswordError, MeetingInfoV2CaptchaError} from '@webex/plugin-meetings/src/meeting-info/meeting-info-v2';
16
16
  import MeetingInfoUtil from '@webex/plugin-meetings/src/meeting-info/utilv2';
17
17
 
18
18
  describe('plugin-meetings', () => {
@@ -80,6 +80,79 @@ describe('plugin-meetings', () => {
80
80
  MeetingInfoUtil.getDestinationType.restore();
81
81
  MeetingInfoUtil.getRequestBody.restore();
82
82
  });
83
+
84
+ it('should fetch meeting info with provided password and captcha code', async () => {
85
+ await meetingInfo.fetchMeetingInfo('1234323', _MEETING_ID_, 'abc', {id: '999', code: 'aabbcc11'});
86
+
87
+ assert.calledWith(webex.request, {
88
+ method: 'POST',
89
+ service: 'webex-appapi-service',
90
+ resource: 'meetingInfo',
91
+ body: {
92
+ supportHostKey: true,
93
+ meetingKey: '1234323',
94
+ password: 'abc',
95
+ captchaID: '999',
96
+ captchaVerifyCode: 'aabbcc11'
97
+ }
98
+ });
99
+ });
100
+
101
+ it('should throw MeetingInfoV2PasswordError for 403 response', async () => {
102
+ const FAKE_MEETING_INFO = {blablabla: 'some_fake_meeting_info'};
103
+
104
+ webex.request = sinon.stub().rejects({statusCode: 403, body: {code: 403000, data: {meetingInfo: FAKE_MEETING_INFO}}});
105
+
106
+ try {
107
+ await meetingInfo.fetchMeetingInfo('1234323', _MEETING_ID_, 'abc', {id: '999', code: 'aabbcc11'});
108
+ assert.fail('fetchMeetingInfo should have thrown, but has not done that');
109
+ }
110
+ catch (err) {
111
+ assert.instanceOf(err, MeetingInfoV2PasswordError);
112
+ assert.deepEqual(err.meetingInfo, FAKE_MEETING_INFO);
113
+ assert.equal(err.wbxAppApiCode, 403000);
114
+ }
115
+ });
116
+
117
+ describe('should throw MeetingInfoV2CaptchaError for 423 response', () => {
118
+ const runTest = async (wbxAppApiCode, expectedIsPasswordRequired) => {
119
+ webex.request = sinon.stub().rejects(
120
+ {
121
+ statusCode: 423,
122
+ body: {
123
+ code: wbxAppApiCode,
124
+ captchaID: 'fake_captcha_id',
125
+ verificationImageURL: 'fake_image_url',
126
+ verificationAudioURL: 'fake_audio_url',
127
+ refreshURL: 'fake_refresh_url'
128
+ }
129
+ }
130
+ );
131
+ try {
132
+ await meetingInfo.fetchMeetingInfo('1234323', _MEETING_ID_, 'abc', {id: '999', code: 'aabbcc11'});
133
+ assert.fail('fetchMeetingInfo should have thrown, but has not done that');
134
+ }
135
+ catch (err) {
136
+ assert.instanceOf(err, MeetingInfoV2CaptchaError);
137
+ assert.deepEqual(err.captchaInfo, {
138
+ captchaId: 'fake_captcha_id',
139
+ verificationImageURL: 'fake_image_url',
140
+ verificationAudioURL: 'fake_audio_url',
141
+ refreshURL: 'fake_refresh_url'
142
+ });
143
+ assert.equal(err.wbxAppApiCode, wbxAppApiCode);
144
+ assert.equal(err.isPasswordRequired, expectedIsPasswordRequired);
145
+ }
146
+ };
147
+
148
+ it('should throw MeetingInfoV2CaptchaError for 423 response (wbxappapi code 423005)', async () => {
149
+ await runTest(423005, true);
150
+ });
151
+
152
+ it('should throw MeetingInfoV2CaptchaError for 423 response (wbxappapi code 423001)', async () => {
153
+ await runTest(423001, false);
154
+ });
155
+ });
83
156
  });
84
157
  });
85
158
  });