@webex/plugin-meetings 3.12.0-next.11 → 3.12.0-next.13
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/aiEnableRequest/index.js +1 -1
- package/dist/breakouts/breakout.js +1 -1
- package/dist/breakouts/index.js +1 -1
- package/dist/controls-options-manager/constants.js +11 -1
- package/dist/controls-options-manager/constants.js.map +1 -1
- package/dist/controls-options-manager/index.js +23 -21
- package/dist/controls-options-manager/index.js.map +1 -1
- package/dist/controls-options-manager/util.js +91 -0
- package/dist/controls-options-manager/util.js.map +1 -1
- package/dist/interpretation/index.js +1 -1
- package/dist/interpretation/siLanguage.js +1 -1
- package/dist/meetings/index.js +5 -1
- package/dist/meetings/index.js.map +1 -1
- package/dist/types/controls-options-manager/constants.d.ts +6 -1
- package/dist/webinar/index.js +1 -1
- package/package.json +1 -1
- package/src/controls-options-manager/constants.ts +14 -1
- package/src/controls-options-manager/index.ts +26 -19
- package/src/controls-options-manager/util.ts +81 -1
- package/src/meetings/index.ts +5 -1
- package/test/unit/spec/controls-options-manager/index.js +114 -6
- package/test/unit/spec/controls-options-manager/util.js +165 -0
- package/test/unit/spec/meetings/index.js +33 -0
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
declare const ENABLED = "enabled";
|
|
2
2
|
declare const CAN_SET = "canSet";
|
|
3
3
|
declare const CAN_UNSET = "canUnset";
|
|
4
|
-
|
|
4
|
+
/**
|
|
5
|
+
* Body keys that represent audio controls. These do not support cross-locus
|
|
6
|
+
* authorization and must be sent directly to the current locus URL.
|
|
7
|
+
*/
|
|
8
|
+
declare const AUDIO_CONTROL_BODY_KEYS: ReadonlySet<string>;
|
|
9
|
+
export { ENABLED, CAN_SET, CAN_UNSET, AUDIO_CONTROL_BODY_KEYS };
|
package/dist/webinar/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
|
+
import {camelCase} from 'lodash';
|
|
2
|
+
import {Control, Setting} from './enums';
|
|
3
|
+
|
|
1
4
|
const ENABLED = 'enabled';
|
|
2
5
|
const CAN_SET = 'canSet';
|
|
3
6
|
const CAN_UNSET = 'canUnset';
|
|
4
7
|
|
|
5
|
-
|
|
8
|
+
/**
|
|
9
|
+
* Body keys that represent audio controls. These do not support cross-locus
|
|
10
|
+
* authorization and must be sent directly to the current locus URL.
|
|
11
|
+
*/
|
|
12
|
+
const AUDIO_CONTROL_BODY_KEYS: ReadonlySet<string> = new Set([
|
|
13
|
+
Control.audio,
|
|
14
|
+
camelCase(Setting.muteOnEntry),
|
|
15
|
+
camelCase(Setting.disallowUnmute),
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
export {ENABLED, CAN_SET, CAN_UNSET, AUDIO_CONTROL_BODY_KEYS};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {camelCase} from 'lodash';
|
|
2
|
+
import ParameterError from '../common/errors/parameter';
|
|
2
3
|
import PermissionError from '../common/errors/permission';
|
|
3
|
-
import {CONTROLS, HTTP_VERBS} from '../constants';
|
|
4
4
|
import MeetingRequest from '../meeting/request';
|
|
5
5
|
import LoggerProxy from '../common/logs/logger-proxy';
|
|
6
6
|
import {Control, Setting} from './enums';
|
|
@@ -153,6 +153,12 @@ export default class ControlsOptionsManager {
|
|
|
153
153
|
* @returns {Promise<Array<any>>}- Promise resolving if the request was successful.
|
|
154
154
|
*/
|
|
155
155
|
public update(...controls: Array<ControlConfig>) {
|
|
156
|
+
if (!this.locusUrl) {
|
|
157
|
+
return Promise.reject(
|
|
158
|
+
new ParameterError('The associated locusUrl for update() controls must be defined.')
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
156
162
|
const payloads = controls.map((control) => {
|
|
157
163
|
if (!Object.keys(Control).includes(control.scope)) {
|
|
158
164
|
throw new Error(
|
|
@@ -172,18 +178,15 @@ export default class ControlsOptionsManager {
|
|
|
172
178
|
});
|
|
173
179
|
|
|
174
180
|
return payloads.reduce((previous, payload) => {
|
|
175
|
-
const
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
181
|
+
const requestParams = Util.getControlsRequestParams({
|
|
182
|
+
body: payload,
|
|
183
|
+
locusUrl: this.locusUrl,
|
|
184
|
+
mainLocusUrl: this.mainLocusUrl,
|
|
185
|
+
});
|
|
179
186
|
|
|
180
187
|
return previous.then(() =>
|
|
181
188
|
// @ts-ignore
|
|
182
|
-
this.request.request(
|
|
183
|
-
uri: `${this.mainLocusUrl || this.locusUrl}/${CONTROLS}`,
|
|
184
|
-
body: {...payload, ...extraBody},
|
|
185
|
-
method: HTTP_VERBS.PATCH,
|
|
186
|
-
})
|
|
189
|
+
this.request.request(requestParams)
|
|
187
190
|
);
|
|
188
191
|
}, Promise.resolve());
|
|
189
192
|
}
|
|
@@ -200,6 +203,12 @@ export default class ControlsOptionsManager {
|
|
|
200
203
|
[Setting.muteOnEntry]?: boolean;
|
|
201
204
|
[Setting.roles]?: Array<string>;
|
|
202
205
|
}): Promise<any> {
|
|
206
|
+
if (!this.locusUrl) {
|
|
207
|
+
return Promise.reject(
|
|
208
|
+
new ParameterError('The associated locusUrl for setControls() must be defined.')
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
|
|
203
212
|
LoggerProxy.logger.log(
|
|
204
213
|
`ControlsOptionsManager:index#setControls --> ${JSON.stringify(setting)}`
|
|
205
214
|
);
|
|
@@ -258,17 +267,15 @@ export default class ControlsOptionsManager {
|
|
|
258
267
|
if (error) {
|
|
259
268
|
return Promise.reject(error);
|
|
260
269
|
}
|
|
261
|
-
const extraBody =
|
|
262
|
-
this.mainLocusUrl && this.mainLocusUrl !== this.locusUrl
|
|
263
|
-
? {authorizingLocusUrl: this.locusUrl}
|
|
264
|
-
: {};
|
|
265
270
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
method: HTTP_VERBS.PATCH,
|
|
271
|
+
const requestParams = Util.getControlsRequestParams({
|
|
272
|
+
body,
|
|
273
|
+
locusUrl: this.locusUrl,
|
|
274
|
+
mainLocusUrl: this.mainLocusUrl,
|
|
271
275
|
});
|
|
276
|
+
|
|
277
|
+
// @ts-ignore
|
|
278
|
+
return this.request.request(requestParams);
|
|
272
279
|
}
|
|
273
280
|
|
|
274
281
|
/**
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import {DISPLAY_HINTS} from '../constants';
|
|
1
|
+
import {CONTROLS, DISPLAY_HINTS, HTTP_VERBS} from '../constants';
|
|
2
2
|
import {Control} from './enums';
|
|
3
|
+
import {AUDIO_CONTROL_BODY_KEYS} from './constants';
|
|
3
4
|
import {
|
|
4
5
|
ControlConfig,
|
|
5
6
|
AudioProperties,
|
|
@@ -400,6 +401,85 @@ class Utils {
|
|
|
400
401
|
|
|
401
402
|
return determinant;
|
|
402
403
|
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Check if all body keys represent audio controls.
|
|
407
|
+
*
|
|
408
|
+
* @param {Record<string, any>} body - The request body to inspect.
|
|
409
|
+
* @returns {boolean} - True if every key in the body is an audio control key.
|
|
410
|
+
*/
|
|
411
|
+
public static isAudioControl(body: Record<string, any>): boolean {
|
|
412
|
+
return Object.keys(body).every((key) => AUDIO_CONTROL_BODY_KEYS.has(key));
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Check if the current locus URL differs from the main locus URL,
|
|
417
|
+
* indicating a breakout session.
|
|
418
|
+
*
|
|
419
|
+
* @param {string} locusUrl - The current locus URL.
|
|
420
|
+
* @param {string} [mainLocusUrl] - The main locus URL.
|
|
421
|
+
* @returns {boolean} - True if in a breakout session.
|
|
422
|
+
*/
|
|
423
|
+
public static isBreakoutLocusUrl(locusUrl: string, mainLocusUrl?: string): boolean {
|
|
424
|
+
return Boolean(mainLocusUrl) && mainLocusUrl !== locusUrl;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Resolve the target URL and extra body fields for a controls request,
|
|
429
|
+
* handling breakout session routing. Note: This is a pure computation function.
|
|
430
|
+
* It does not validate that locusUrl is
|
|
431
|
+
* defined. Callers must guard against falsy locusUrl before
|
|
432
|
+
* invoking this function.
|
|
433
|
+
* Mixed audio and non-audio keys in a single body (e.g., {audio: {...},
|
|
434
|
+
* raiseHand: {...}}) are treated as non-audio and routed to mainLocusUrl with
|
|
435
|
+
* authorizingLocusUrl. This means the audio portion would go through unsupported
|
|
436
|
+
* cross-locus authorization. Callers must not produce mixed payloads — update()
|
|
437
|
+
* sends each control scope as a separate request, and setControls() only handles
|
|
438
|
+
* audio-related settings.
|
|
439
|
+
*
|
|
440
|
+
* The authorizingLocusUrl mechanism on PATCH /loci/{lid}/controls is not supported
|
|
441
|
+
* for audio control updates (mute/unmute, muteOnEntry, disallowUnmute).
|
|
442
|
+
* Audio controls are not wired into the cross-locus GraphQL authorization path that
|
|
443
|
+
* other control types (raiseHand, viewParticipantList, admit, reactions, etc.) use.
|
|
444
|
+
* Specifically, the GraphQL authorization layer does not recognize audio as a control
|
|
445
|
+
* type eligible for remote locus authorization.
|
|
446
|
+
* This means authorizingLocusUrl is effectively ignored for audio controls and the
|
|
447
|
+
* server evaluates the request against the target locus only, where the host may not
|
|
448
|
+
* be currently joined.
|
|
449
|
+
* Audio control updates must be sent directly to the locus the user is currently in.
|
|
450
|
+
* If the host is in a breakout and wants to mute participants in that breakout, the
|
|
451
|
+
* request should target the breakout locus URL directly, not the main session locus
|
|
452
|
+
* with authorizingLocusUrl.
|
|
453
|
+
* Meeting-wide audio control actions (e.g., muting panelists across all breakouts
|
|
454
|
+
* from a single request) are not currently supported through this mechanism.
|
|
455
|
+
*
|
|
456
|
+
* @param {object} options
|
|
457
|
+
* @param {Record<string, any>} options.body - The request body.
|
|
458
|
+
* @param {string} options.locusUrl - The current locus URL. Must be defined (callers must validate).
|
|
459
|
+
* @param {string} [options.mainLocusUrl] - The main locus URL.
|
|
460
|
+
* @returns {{ uri: string, body: Record<string, any>, method: string }}
|
|
461
|
+
*/
|
|
462
|
+
public static getControlsRequestParams(options: {
|
|
463
|
+
body: Record<string, any>;
|
|
464
|
+
locusUrl: string;
|
|
465
|
+
mainLocusUrl?: string;
|
|
466
|
+
}): {
|
|
467
|
+
uri: string;
|
|
468
|
+
body: Record<string, any>;
|
|
469
|
+
method: string;
|
|
470
|
+
} {
|
|
471
|
+
const {body, locusUrl, mainLocusUrl} = options;
|
|
472
|
+
|
|
473
|
+
const isAudio = Utils.isAudioControl(body);
|
|
474
|
+
const inBreakout = Utils.isBreakoutLocusUrl(locusUrl, mainLocusUrl);
|
|
475
|
+
const targetUrl = inBreakout && !isAudio ? mainLocusUrl : locusUrl;
|
|
476
|
+
|
|
477
|
+
return {
|
|
478
|
+
uri: `${targetUrl}/${CONTROLS}`,
|
|
479
|
+
body: inBreakout && !isAudio ? {...body, authorizingLocusUrl: locusUrl} : body,
|
|
480
|
+
method: HTTP_VERBS.PATCH,
|
|
481
|
+
};
|
|
482
|
+
}
|
|
403
483
|
}
|
|
404
484
|
|
|
405
485
|
export default Utils;
|
package/src/meetings/index.ts
CHANGED
|
@@ -1811,7 +1811,11 @@ export default class Meetings extends WebexPlugin {
|
|
|
1811
1811
|
// For type LOCUS_ID we need to parse the locus object to get the information
|
|
1812
1812
|
// about the caller and callee
|
|
1813
1813
|
// Meeting Added event will be created in `handleLocusEvent`
|
|
1814
|
-
if
|
|
1814
|
+
// Only emit MEETING_ADDED if the meeting still exists in the collection.
|
|
1815
|
+
// If fetchMeetingInfo failed and the meeting was destroyed in the catch block,
|
|
1816
|
+
// skip emitting to prevent orphaned meeting references on the consumer side.
|
|
1817
|
+
// @ts-ignore - getMeetingByType types value as object but accepts strings (same as handleLocusEvent)
|
|
1818
|
+
if (type !== DESTINATION_TYPE.LOCUS_ID && this.getMeetingByType(_ID_, meeting.id)) {
|
|
1815
1819
|
if (!meeting.sipUri) {
|
|
1816
1820
|
meeting.setSipUri(destination);
|
|
1817
1821
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import ControlsOptionsManager from '@webex/plugin-meetings/src/controls-options-manager';
|
|
2
2
|
import Util from '@webex/plugin-meetings/src/controls-options-manager/util';
|
|
3
|
+
import ParameterError from '@webex/plugin-meetings/src/common/errors/parameter';
|
|
3
4
|
import sinon from 'sinon';
|
|
4
5
|
import {assert} from '@webex/test-helper-chai';
|
|
5
6
|
import { HTTP_VERBS } from '@webex/plugin-meetings/src/constants';
|
|
@@ -76,6 +77,19 @@ describe('plugin-meetings', () => {
|
|
|
76
77
|
|
|
77
78
|
assert.deepEqual(result, request.request.firstCall.returnValue);
|
|
78
79
|
});
|
|
80
|
+
|
|
81
|
+
it('should send setMuteOnEntry to locusUrl without authorizingLocusUrl when in breakout', () => {
|
|
82
|
+
manager.setDisplayHints(['ENABLE_MUTE_ON_ENTRY']);
|
|
83
|
+
manager.mainLocusUrl = 'test/main';
|
|
84
|
+
|
|
85
|
+
const result = manager.setMuteOnEntry(true);
|
|
86
|
+
|
|
87
|
+
assert.calledWith(request.request, { uri: 'test/id/controls',
|
|
88
|
+
body: { muteOnEntry: { enabled: true } },
|
|
89
|
+
method: HTTP_VERBS.PATCH});
|
|
90
|
+
|
|
91
|
+
assert.deepEqual(result, request.request.firstCall.returnValue);
|
|
92
|
+
});
|
|
79
93
|
});
|
|
80
94
|
|
|
81
95
|
describe('setDisallowUnmute', () => {
|
|
@@ -118,6 +132,19 @@ describe('plugin-meetings', () => {
|
|
|
118
132
|
|
|
119
133
|
assert.deepEqual(result, request.request.firstCall.returnValue);
|
|
120
134
|
});
|
|
135
|
+
|
|
136
|
+
it('should send setDisallowUnmute to locusUrl without authorizingLocusUrl when in breakout', () => {
|
|
137
|
+
manager.setDisplayHints(['ENABLE_HARD_MUTE']);
|
|
138
|
+
manager.mainLocusUrl = 'test/main';
|
|
139
|
+
|
|
140
|
+
const result = manager.setDisallowUnmute(true);
|
|
141
|
+
|
|
142
|
+
assert.calledWith(request.request, { uri: 'test/id/controls',
|
|
143
|
+
body: { disallowUnmute: { enabled: true } },
|
|
144
|
+
method: HTTP_VERBS.PATCH});
|
|
145
|
+
|
|
146
|
+
assert.deepEqual(result, request.request.firstCall.returnValue);
|
|
147
|
+
});
|
|
121
148
|
});
|
|
122
149
|
});
|
|
123
150
|
|
|
@@ -138,6 +165,18 @@ describe('plugin-meetings', () => {
|
|
|
138
165
|
});
|
|
139
166
|
});
|
|
140
167
|
|
|
168
|
+
it('should reject with ParameterError when locusUrl is not set', () => {
|
|
169
|
+
const noLocusManager = new ControlsOptionsManager(request);
|
|
170
|
+
|
|
171
|
+
const result = noLocusManager.update({scope: 'audio', properties: {muted: true}});
|
|
172
|
+
|
|
173
|
+
assert.notCalled(request.request);
|
|
174
|
+
return assert.isRejected(result).then((err) => {
|
|
175
|
+
assert.instanceOf(err, ParameterError);
|
|
176
|
+
assert.match(err.message, /locusUrl.*must be defined/);
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
141
180
|
it('should throw an error if the scope is not supported', () => {
|
|
142
181
|
const scope = 'invalid';
|
|
143
182
|
|
|
@@ -203,7 +242,7 @@ describe('plugin-meetings', () => {
|
|
|
203
242
|
});
|
|
204
243
|
});
|
|
205
244
|
|
|
206
|
-
it('should
|
|
245
|
+
it('should send audio controls to locusUrl without authorizingLocusUrl and non-audio to mainLocusUrl with authorizingLocusUrl when in breakout', () => {
|
|
207
246
|
const restorable = Util.canUpdate;
|
|
208
247
|
Util.canUpdate = sinon.stub().returns(true);
|
|
209
248
|
manager.mainLocusUrl = 'test/main';
|
|
@@ -213,15 +252,16 @@ describe('plugin-meetings', () => {
|
|
|
213
252
|
|
|
214
253
|
return manager.update(audio, reactions)
|
|
215
254
|
.then(() => {
|
|
255
|
+
// Audio controls go directly to current locusUrl (no cross-locus authorization)
|
|
216
256
|
assert.calledWith(request.request, {
|
|
217
|
-
uri: 'test/
|
|
257
|
+
uri: 'test/id/controls',
|
|
218
258
|
body: {
|
|
219
259
|
audio: audio.properties,
|
|
220
|
-
authorizingLocusUrl: 'test/id'
|
|
221
260
|
},
|
|
222
261
|
method: HTTP_VERBS.PATCH,
|
|
223
262
|
});
|
|
224
263
|
|
|
264
|
+
// Non-audio controls go to mainLocusUrl with authorizingLocusUrl
|
|
225
265
|
assert.calledWith(request.request, {
|
|
226
266
|
uri: 'test/main/controls',
|
|
227
267
|
body: {
|
|
@@ -234,6 +274,49 @@ describe('plugin-meetings', () => {
|
|
|
234
274
|
Util.canUpdate = restorable;
|
|
235
275
|
});
|
|
236
276
|
});
|
|
277
|
+
|
|
278
|
+
it('should send audio controls to locusUrl without authorizingLocusUrl when in breakout', () => {
|
|
279
|
+
const restorable = Util.canUpdate;
|
|
280
|
+
Util.canUpdate = sinon.stub().returns(true);
|
|
281
|
+
manager.mainLocusUrl = 'test/main';
|
|
282
|
+
|
|
283
|
+
const audio = {scope: 'audio', properties: {muted: true, disallowUnmute: false}};
|
|
284
|
+
|
|
285
|
+
return manager.update(audio)
|
|
286
|
+
.then(() => {
|
|
287
|
+
assert.calledWith(request.request, {
|
|
288
|
+
uri: 'test/id/controls',
|
|
289
|
+
body: {
|
|
290
|
+
audio: audio.properties,
|
|
291
|
+
},
|
|
292
|
+
method: HTTP_VERBS.PATCH,
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
Util.canUpdate = restorable;
|
|
296
|
+
});
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
it('should send non-audio controls to mainLocusUrl with authorizingLocusUrl when in breakout', () => {
|
|
300
|
+
const restorable = Util.canUpdate;
|
|
301
|
+
Util.canUpdate = sinon.stub().returns(true);
|
|
302
|
+
manager.mainLocusUrl = 'test/main';
|
|
303
|
+
|
|
304
|
+
const reactions = {scope: 'reactions', properties: {enabled: true}};
|
|
305
|
+
|
|
306
|
+
return manager.update(reactions)
|
|
307
|
+
.then(() => {
|
|
308
|
+
assert.calledWith(request.request, {
|
|
309
|
+
uri: 'test/main/controls',
|
|
310
|
+
body: {
|
|
311
|
+
reactions: reactions.properties,
|
|
312
|
+
authorizingLocusUrl: 'test/id',
|
|
313
|
+
},
|
|
314
|
+
method: HTTP_VERBS.PATCH,
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
Util.canUpdate = restorable;
|
|
318
|
+
});
|
|
319
|
+
});
|
|
237
320
|
});
|
|
238
321
|
|
|
239
322
|
describe('Mute/Unmute All', () => {
|
|
@@ -252,6 +335,18 @@ describe('plugin-meetings', () => {
|
|
|
252
335
|
})
|
|
253
336
|
});
|
|
254
337
|
|
|
338
|
+
it('should reject with ParameterError when locusUrl is not set', () => {
|
|
339
|
+
const noLocusManager = new ControlsOptionsManager(request);
|
|
340
|
+
|
|
341
|
+
const result = noLocusManager.setMuteAll(true, true, true);
|
|
342
|
+
|
|
343
|
+
assert.notCalled(request.request);
|
|
344
|
+
return assert.isRejected(result).then((err) => {
|
|
345
|
+
assert.instanceOf(err, ParameterError);
|
|
346
|
+
assert.match(err.message, /locusUrl.*must be defined/);
|
|
347
|
+
});
|
|
348
|
+
});
|
|
349
|
+
|
|
255
350
|
it('rejects when correct display hint is not present mutedEnabled=false', () => {
|
|
256
351
|
const result = manager.setMuteAll(false, false, false);
|
|
257
352
|
|
|
@@ -340,14 +435,27 @@ describe('plugin-meetings', () => {
|
|
|
340
435
|
assert.deepEqual(result, request.request.firstCall.returnValue);
|
|
341
436
|
});
|
|
342
437
|
|
|
343
|
-
it('
|
|
438
|
+
it('should send setMuteAll to locusUrl without authorizingLocusUrl when in breakout', () => {
|
|
344
439
|
manager.setDisplayHints(['MUTE_ALL', 'DISABLE_HARD_MUTE', 'DISABLE_MUTE_ON_ENTRY']);
|
|
345
440
|
manager.mainLocusUrl = `test/main`;
|
|
346
441
|
|
|
347
442
|
const result = manager.setMuteAll(true, true, true, ['attendee']);
|
|
348
443
|
|
|
349
|
-
assert.calledWith(request.request, { uri: 'test/
|
|
350
|
-
body: { audio: { muted: true, disallowUnmute: true, muteOnEntry: true, roles: ['attendee'] }
|
|
444
|
+
assert.calledWith(request.request, { uri: 'test/id/controls',
|
|
445
|
+
body: { audio: { muted: true, disallowUnmute: true, muteOnEntry: true, roles: ['attendee'] } },
|
|
446
|
+
method: HTTP_VERBS.PATCH});
|
|
447
|
+
|
|
448
|
+
assert.deepEqual(result, request.request.firstCall.returnValue);
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
it('should send setMuteAll with PANELIST role to locusUrl without authorizingLocusUrl when in breakout', () => {
|
|
452
|
+
manager.setDisplayHints(['MUTE_ALL', 'ENABLE_HARD_MUTE', 'ENABLE_MUTE_ON_ENTRY']);
|
|
453
|
+
manager.mainLocusUrl = `test/main`;
|
|
454
|
+
|
|
455
|
+
const result = manager.setMuteAll(true, true, true, ['PANELIST']);
|
|
456
|
+
|
|
457
|
+
assert.calledWith(request.request, { uri: 'test/id/controls',
|
|
458
|
+
body: { audio: { muted: true, disallowUnmute: true, muteOnEntry: true, roles: ['PANELIST'] } },
|
|
351
459
|
method: HTTP_VERBS.PATCH});
|
|
352
460
|
|
|
353
461
|
assert.deepEqual(result, request.request.firstCall.returnValue);
|
|
@@ -799,6 +799,171 @@ describe('plugin-meetings', () => {
|
|
|
799
799
|
);
|
|
800
800
|
});
|
|
801
801
|
});
|
|
802
|
+
|
|
803
|
+
describe('isAudioControl()', () => {
|
|
804
|
+
it('should return true when all body keys are audio control keys', () => {
|
|
805
|
+
assert.isTrue(ControlsOptionsUtil.isAudioControl({audio: {muted: true}}));
|
|
806
|
+
});
|
|
807
|
+
|
|
808
|
+
it('should return true when body has muteOnEntry key', () => {
|
|
809
|
+
assert.isTrue(ControlsOptionsUtil.isAudioControl({muteOnEntry: {enabled: true}}));
|
|
810
|
+
});
|
|
811
|
+
|
|
812
|
+
it('should return true when body has disallowUnmute key', () => {
|
|
813
|
+
assert.isTrue(ControlsOptionsUtil.isAudioControl({disallowUnmute: {enabled: true}}));
|
|
814
|
+
});
|
|
815
|
+
|
|
816
|
+
it('should return true when body has multiple audio control keys', () => {
|
|
817
|
+
assert.isTrue(ControlsOptionsUtil.isAudioControl({audio: {muted: true}, muteOnEntry: {enabled: true}, disallowUnmute: {enabled: true}}));
|
|
818
|
+
});
|
|
819
|
+
|
|
820
|
+
it('should return false when body has a non-audio control key', () => {
|
|
821
|
+
assert.isFalse(ControlsOptionsUtil.isAudioControl({raiseHand: {enabled: true}}));
|
|
822
|
+
});
|
|
823
|
+
|
|
824
|
+
it('should return false when body has a mix of audio and non-audio keys', () => {
|
|
825
|
+
assert.isFalse(ControlsOptionsUtil.isAudioControl({audio: {muted: true}, raiseHand: {enabled: true}}));
|
|
826
|
+
});
|
|
827
|
+
|
|
828
|
+
it('should return true for an empty body', () => {
|
|
829
|
+
assert.isTrue(ControlsOptionsUtil.isAudioControl({}));
|
|
830
|
+
});
|
|
831
|
+
});
|
|
832
|
+
|
|
833
|
+
describe('isBreakoutLocusUrl()', () => {
|
|
834
|
+
it('should return true when mainLocusUrl differs from locusUrl', () => {
|
|
835
|
+
assert.isTrue(ControlsOptionsUtil.isBreakoutLocusUrl('locus/breakout', 'locus/main'));
|
|
836
|
+
});
|
|
837
|
+
|
|
838
|
+
it('should return false when mainLocusUrl equals locusUrl', () => {
|
|
839
|
+
assert.isFalse(ControlsOptionsUtil.isBreakoutLocusUrl('locus/main', 'locus/main'));
|
|
840
|
+
});
|
|
841
|
+
|
|
842
|
+
it('should return false when mainLocusUrl is undefined', () => {
|
|
843
|
+
assert.isFalse(ControlsOptionsUtil.isBreakoutLocusUrl('locus/breakout', undefined));
|
|
844
|
+
});
|
|
845
|
+
|
|
846
|
+
it('should return false when mainLocusUrl is null', () => {
|
|
847
|
+
assert.isFalse(ControlsOptionsUtil.isBreakoutLocusUrl('locus/breakout', null));
|
|
848
|
+
});
|
|
849
|
+
|
|
850
|
+
it('should return false when mainLocusUrl is empty string', () => {
|
|
851
|
+
assert.isFalse(ControlsOptionsUtil.isBreakoutLocusUrl('locus/breakout', ''));
|
|
852
|
+
});
|
|
853
|
+
});
|
|
854
|
+
|
|
855
|
+
describe('getControlsRequestParams()', () => {
|
|
856
|
+
const locusUrl = 'locus/breakout';
|
|
857
|
+
const mainLocusUrl = 'locus/main';
|
|
858
|
+
|
|
859
|
+
it('should return full request params targeting locusUrl when not in a breakout', () => {
|
|
860
|
+
const result = ControlsOptionsUtil.getControlsRequestParams({
|
|
861
|
+
body: {raiseHand: {enabled: true}},
|
|
862
|
+
locusUrl: 'locus/main',
|
|
863
|
+
mainLocusUrl: 'locus/main',
|
|
864
|
+
});
|
|
865
|
+
|
|
866
|
+
assert.equal(result.uri, 'locus/main/controls');
|
|
867
|
+
assert.deepEqual(result.body, {raiseHand: {enabled: true}});
|
|
868
|
+
assert.equal(result.method, 'PATCH');
|
|
869
|
+
});
|
|
870
|
+
|
|
871
|
+
it('should return mainLocusUrl with authorizingLocusUrl in body for non-audio controls in a breakout', () => {
|
|
872
|
+
const result = ControlsOptionsUtil.getControlsRequestParams({
|
|
873
|
+
body: {raiseHand: {enabled: true}},
|
|
874
|
+
locusUrl,
|
|
875
|
+
mainLocusUrl,
|
|
876
|
+
});
|
|
877
|
+
|
|
878
|
+
assert.equal(result.uri, 'locus/main/controls');
|
|
879
|
+
assert.deepEqual(result.body, {raiseHand: {enabled: true}, authorizingLocusUrl: locusUrl});
|
|
880
|
+
assert.equal(result.method, 'PATCH');
|
|
881
|
+
});
|
|
882
|
+
|
|
883
|
+
it('should return locusUrl without authorizingLocusUrl for audio controls in a breakout', () => {
|
|
884
|
+
const result = ControlsOptionsUtil.getControlsRequestParams({
|
|
885
|
+
body: {audio: {muted: true}},
|
|
886
|
+
locusUrl,
|
|
887
|
+
mainLocusUrl,
|
|
888
|
+
});
|
|
889
|
+
|
|
890
|
+
assert.equal(result.uri, 'locus/breakout/controls');
|
|
891
|
+
assert.deepEqual(result.body, {audio: {muted: true}});
|
|
892
|
+
assert.equal(result.method, 'PATCH');
|
|
893
|
+
});
|
|
894
|
+
|
|
895
|
+
it('should return locusUrl without authorizingLocusUrl for muteOnEntry in a breakout', () => {
|
|
896
|
+
const result = ControlsOptionsUtil.getControlsRequestParams({
|
|
897
|
+
body: {muteOnEntry: {enabled: true}},
|
|
898
|
+
locusUrl,
|
|
899
|
+
mainLocusUrl,
|
|
900
|
+
});
|
|
901
|
+
|
|
902
|
+
assert.equal(result.uri, 'locus/breakout/controls');
|
|
903
|
+
assert.deepEqual(result.body, {muteOnEntry: {enabled: true}});
|
|
904
|
+
assert.equal(result.method, 'PATCH');
|
|
905
|
+
});
|
|
906
|
+
|
|
907
|
+
it('should return locusUrl without authorizingLocusUrl for disallowUnmute in a breakout', () => {
|
|
908
|
+
const result = ControlsOptionsUtil.getControlsRequestParams({
|
|
909
|
+
body: {disallowUnmute: {enabled: true}},
|
|
910
|
+
locusUrl,
|
|
911
|
+
mainLocusUrl,
|
|
912
|
+
});
|
|
913
|
+
|
|
914
|
+
assert.equal(result.uri, 'locus/breakout/controls');
|
|
915
|
+
assert.deepEqual(result.body, {disallowUnmute: {enabled: true}});
|
|
916
|
+
assert.equal(result.method, 'PATCH');
|
|
917
|
+
});
|
|
918
|
+
|
|
919
|
+
it('should return locusUrl when mainLocusUrl is undefined', () => {
|
|
920
|
+
const result = ControlsOptionsUtil.getControlsRequestParams({
|
|
921
|
+
body: {raiseHand: {enabled: true}},
|
|
922
|
+
locusUrl,
|
|
923
|
+
mainLocusUrl: undefined,
|
|
924
|
+
});
|
|
925
|
+
|
|
926
|
+
assert.equal(result.uri, 'locus/breakout/controls');
|
|
927
|
+
assert.deepEqual(result.body, {raiseHand: {enabled: true}});
|
|
928
|
+
assert.equal(result.method, 'PATCH');
|
|
929
|
+
});
|
|
930
|
+
|
|
931
|
+
it('should return locusUrl when mainLocusUrl is null', () => {
|
|
932
|
+
const result = ControlsOptionsUtil.getControlsRequestParams({
|
|
933
|
+
body: {raiseHand: {enabled: true}},
|
|
934
|
+
locusUrl,
|
|
935
|
+
mainLocusUrl: null,
|
|
936
|
+
});
|
|
937
|
+
|
|
938
|
+
assert.equal(result.uri, 'locus/breakout/controls');
|
|
939
|
+
assert.deepEqual(result.body, {raiseHand: {enabled: true}});
|
|
940
|
+
assert.equal(result.method, 'PATCH');
|
|
941
|
+
});
|
|
942
|
+
|
|
943
|
+
it('should return locusUrl when mainLocusUrl is empty string', () => {
|
|
944
|
+
const result = ControlsOptionsUtil.getControlsRequestParams({
|
|
945
|
+
body: {raiseHand: {enabled: true}},
|
|
946
|
+
locusUrl,
|
|
947
|
+
mainLocusUrl: '',
|
|
948
|
+
});
|
|
949
|
+
|
|
950
|
+
assert.equal(result.uri, 'locus/breakout/controls');
|
|
951
|
+
assert.deepEqual(result.body, {raiseHand: {enabled: true}});
|
|
952
|
+
assert.equal(result.method, 'PATCH');
|
|
953
|
+
});
|
|
954
|
+
|
|
955
|
+
it('should return locusUrl for audio controls when not in a breakout', () => {
|
|
956
|
+
const result = ControlsOptionsUtil.getControlsRequestParams({
|
|
957
|
+
body: {audio: {muted: true}},
|
|
958
|
+
locusUrl: 'locus/main',
|
|
959
|
+
mainLocusUrl: 'locus/main',
|
|
960
|
+
});
|
|
961
|
+
|
|
962
|
+
assert.equal(result.uri, 'locus/main/controls');
|
|
963
|
+
assert.deepEqual(result.body, {audio: {muted: true}});
|
|
964
|
+
assert.equal(result.method, 'PATCH');
|
|
965
|
+
});
|
|
966
|
+
});
|
|
802
967
|
});
|
|
803
968
|
});
|
|
804
969
|
});
|
|
@@ -2833,6 +2833,39 @@ describe('plugin-meetings', () => {
|
|
|
2833
2833
|
checkCreateMeetingWithNoMeetingInfo(true, true);
|
|
2834
2834
|
});
|
|
2835
2835
|
|
|
2836
|
+
it('does not emit meeting:added when meeting is destroyed due to missing meeting info', async () => {
|
|
2837
|
+
// Make destroy actually remove the meeting from the collection
|
|
2838
|
+
// so that getMeetingByType returns null in the finally block
|
|
2839
|
+
webex.meetings.destroy = sinon.stub().callsFake((meeting) => {
|
|
2840
|
+
webex.meetings.meetingCollection.delete(meeting.id);
|
|
2841
|
+
});
|
|
2842
|
+
|
|
2843
|
+
try {
|
|
2844
|
+
await webex.meetings.createMeeting(
|
|
2845
|
+
'test destination',
|
|
2846
|
+
'test type',
|
|
2847
|
+
undefined,
|
|
2848
|
+
undefined,
|
|
2849
|
+
undefined,
|
|
2850
|
+
true
|
|
2851
|
+
);
|
|
2852
|
+
assert.fail('should have thrown NoMeetingInfoError');
|
|
2853
|
+
} catch (err) {
|
|
2854
|
+
assert.instanceOf(err, NoMeetingInfoError);
|
|
2855
|
+
}
|
|
2856
|
+
|
|
2857
|
+
assert.calledOnce(webex.meetings.destroy);
|
|
2858
|
+
|
|
2859
|
+
// meeting:added should NOT have been triggered since the meeting was destroyed
|
|
2860
|
+
assert.neverCalledWith(
|
|
2861
|
+
TriggerProxy.trigger,
|
|
2862
|
+
sinon.match.any,
|
|
2863
|
+
sinon.match({function: 'createMeeting'}),
|
|
2864
|
+
'meeting:added',
|
|
2865
|
+
sinon.match.any
|
|
2866
|
+
);
|
|
2867
|
+
});
|
|
2868
|
+
|
|
2836
2869
|
it('creates the meeting avoiding meeting info fetch by passing type as DESTINATION_TYPE.ONE_ON_ONE_CALL', async () => {
|
|
2837
2870
|
const meeting = await webex.meetings.createMeeting(
|
|
2838
2871
|
'test destination',
|