@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.
- package/dist/common/errors/captcha-error.js +64 -0
- package/dist/common/errors/captcha-error.js.map +1 -0
- package/dist/common/errors/password-error.js +64 -0
- package/dist/common/errors/password-error.js.map +1 -0
- package/dist/constants.js +33 -1
- package/dist/constants.js.map +1 -1
- package/dist/meeting/index.js +665 -429
- package/dist/meeting/index.js.map +1 -1
- package/dist/meeting/request.js +59 -32
- package/dist/meeting/request.js.map +1 -1
- package/dist/meeting/util.js +12 -0
- package/dist/meeting/util.js.map +1 -1
- package/dist/meeting-info/meeting-info-v2.js +142 -8
- package/dist/meeting-info/meeting-info-v2.js.map +1 -1
- package/dist/meeting-info/utilv2.js +12 -1
- package/dist/meeting-info/utilv2.js.map +1 -1
- package/dist/meetings/index.js +25 -19
- package/dist/meetings/index.js.map +1 -1
- package/package.json +5 -5
- package/src/common/errors/captcha-error.js +21 -0
- package/src/common/errors/password-error.js +21 -0
- package/src/constants.js +24 -0
- package/src/meeting/index.js +170 -0
- package/src/meeting/request.js +27 -0
- package/src/meeting/util.js +15 -4
- package/src/meeting-info/meeting-info-v2.js +67 -3
- package/src/meeting-info/utilv2.js +12 -1
- package/src/meetings/index.js +10 -8
- package/test/unit/spec/meeting/index.js +293 -2
- package/test/unit/spec/meeting-info/meetinginfov2.js +74 -1
|
@@ -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 {
|
|
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
|
|
package/src/meetings/index.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
736
|
-
|
|
737
|
-
|
|
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
|
});
|