diff --git a/docs/samples/browser-plugin-meetings/app.js b/docs/samples/browser-plugin-meetings/app.js index 5a542fe9d3b..f9c7104edc4 100644 --- a/docs/samples/browser-plugin-meetings/app.js +++ b/docs/samples/browser-plugin-meetings/app.js @@ -1795,6 +1795,25 @@ function transferHostToMember(transferButton) { } } +function reclaimHost(reclaimHostBtn) { + const hostKey = reclaimHostBtn.previousElementSibling.value; + const meeting = getCurrentMeeting(); + const selfId = meeting.members.selfId; + const role = { + type: 'MODERATOR', + hasRole: true, + hostKey, + }; + + meeting.members.assignRoles(selfId, [role]) + .then(() => { + console.log('Host role reclaimed'); + }) + .catch((error) => { + console.log('Error reclaiming host role', error); + }); +} + function viewParticipants() { function createLabel(id, value = '') { const label = document.createElement('label'); @@ -1923,6 +1942,19 @@ function viewParticipants() { inviteDiv.appendChild(inviteBtn); participantButtons.appendChild(inviteDiv); + + const reclaimHostDiv = document.createElement('div'); + const reclaimHostInput = document.createElement('input'); + const reclaimHostBtn = createButton('Reclaim Host', reclaimHost); + + reclaimHostDiv.style.display = 'flex'; + reclaimHostInput.type = 'text'; + reclaimHostInput.placeholder = 'Host Key'; + + reclaimHostDiv.appendChild(reclaimHostInput); + reclaimHostDiv.appendChild(reclaimHostBtn); + + participantButtons.appendChild(reclaimHostDiv); } } diff --git a/packages/@webex/plugin-meetings/src/common/errors/reclaim-host-role-error.ts b/packages/@webex/plugin-meetings/src/common/errors/reclaim-host-role-error.ts new file mode 100644 index 00000000000..4b1a2a6b6a7 --- /dev/null +++ b/packages/@webex/plugin-meetings/src/common/errors/reclaim-host-role-error.ts @@ -0,0 +1,134 @@ +import * as MEETINGCONSTANTS from '../../constants'; + +/** + * Extended Error object for reclaim host role empty or wrong key + */ +export class ReclaimHostEmptyWrongKeyError extends Error { + sdkMessage: string; + error: null; + code: number; + + /** + * + * @constructor + * @param {String} [message] + * @param {Object} [error] + */ + constructor( + message: string = MEETINGCONSTANTS.ERROR_DICTIONARY.RECLAIM_HOST_ROLE_EMPTY_OR_WRONG_KEY + .MESSAGE, + error: any = null + ) { + super(message); + + if (Error.captureStackTrace) { + Error.captureStackTrace(this, ReclaimHostEmptyWrongKeyError); + } + + this.name = MEETINGCONSTANTS.ERROR_DICTIONARY.RECLAIM_HOST_ROLE_EMPTY_OR_WRONG_KEY.NAME; + this.sdkMessage = + message || MEETINGCONSTANTS.ERROR_DICTIONARY.RECLAIM_HOST_ROLE_EMPTY_OR_WRONG_KEY.MESSAGE; + this.error = error; + + this.code = MEETINGCONSTANTS.ERROR_DICTIONARY.RECLAIM_HOST_ROLE_EMPTY_OR_WRONG_KEY.CODE; + } +} + +/** + * Extended Error object for reclaim host role not supported + */ +export class ReclaimHostNotSupportedError extends Error { + sdkMessage: string; + error: null; + code: number; + + /** + * + * @constructor + * @param {String} [message] + * @param {Object} [error] + */ + constructor( + message: string = MEETINGCONSTANTS.ERROR_DICTIONARY.RECLAIM_HOST_ROLE_NOT_SUPPORTED.MESSAGE, + error: any = null + ) { + super(message); + + if (Error.captureStackTrace) { + Error.captureStackTrace(this, ReclaimHostNotSupportedError); + } + + this.name = MEETINGCONSTANTS.ERROR_DICTIONARY.RECLAIM_HOST_ROLE_NOT_SUPPORTED.NAME; + this.sdkMessage = + message || MEETINGCONSTANTS.ERROR_DICTIONARY.RECLAIM_HOST_ROLE_NOT_SUPPORTED.MESSAGE; + this.error = error; + + this.code = MEETINGCONSTANTS.ERROR_DICTIONARY.RECLAIM_HOST_ROLE_NOT_SUPPORTED.CODE; + } +} + +/** + * Extended Error object for reclaim host role not allowed for other participants + */ +export class ReclaimHostNotAllowedError extends Error { + sdkMessage: string; + error: null; + code: number; + + /** + * + * @constructor + * @param {String} [message] + * @param {Object} [error] + */ + constructor( + message: string = MEETINGCONSTANTS.ERROR_DICTIONARY.RECLAIM_HOST_ROLE_NOT_ALLOWED.MESSAGE, + error: any = null + ) { + super(message); + + if (Error.captureStackTrace) { + Error.captureStackTrace(this, ReclaimHostNotAllowedError); + } + + this.name = MEETINGCONSTANTS.ERROR_DICTIONARY.RECLAIM_HOST_ROLE_NOT_ALLOWED.NAME; + this.sdkMessage = + message || MEETINGCONSTANTS.ERROR_DICTIONARY.RECLAIM_HOST_ROLE_NOT_ALLOWED.MESSAGE; + this.error = error; + + this.code = MEETINGCONSTANTS.ERROR_DICTIONARY.RECLAIM_HOST_ROLE_NOT_ALLOWED.CODE; + } +} + +/** + * Extended Error object for reclaim host role when user is host already + */ +export class ReclaimHostIsHostAlreadyError extends Error { + sdkMessage: string; + error: null; + code: number; + + /** + * + * @constructor + * @param {String} [message] + * @param {Object} [error] + */ + constructor( + message: string = MEETINGCONSTANTS.ERROR_DICTIONARY.RECLAIM_HOST_ROLE_IS_ALREADY_HOST.MESSAGE, + error: any = null + ) { + super(message); + + if (Error.captureStackTrace) { + Error.captureStackTrace(this, ReclaimHostIsHostAlreadyError); + } + + this.name = MEETINGCONSTANTS.ERROR_DICTIONARY.RECLAIM_HOST_ROLE_IS_ALREADY_HOST.NAME; + this.sdkMessage = + message || MEETINGCONSTANTS.ERROR_DICTIONARY.RECLAIM_HOST_ROLE_IS_ALREADY_HOST.MESSAGE; + this.error = error; + + this.code = MEETINGCONSTANTS.ERROR_DICTIONARY.RECLAIM_HOST_ROLE_IS_ALREADY_HOST.CODE; + } +} diff --git a/packages/@webex/plugin-meetings/src/constants.ts b/packages/@webex/plugin-meetings/src/constants.ts index 5f5c2e26c9b..43d82ad2b7d 100644 --- a/packages/@webex/plugin-meetings/src/constants.ts +++ b/packages/@webex/plugin-meetings/src/constants.ts @@ -239,6 +239,13 @@ export const CALENDAR_EVENTS = { DELETE: 'event:calendar.meeting.delete', }; +export const ASSIGN_ROLES_ERROR_CODES = { + ReclaimHostNotSupportedErrorCode: 2400127, + ReclaimHostNotAllowedErrorCode: 2403135, + ReclaimHostEmptyWrongKeyErrorCode: 2403136, + ReclaimHostIsHostAlreadyErrorCode: 2409150, +}; + export const DEFAULT_GET_STATS_FILTER = { types: [ 'track', @@ -449,6 +456,30 @@ export const ERROR_DICTIONARY = { MESSAGE: 'Captcha is required.', CODE: 8, }, + RECLAIM_HOST_ROLE_NOT_SUPPORTED: { + NAME: 'ReclaimHostRoleNotSupported', + MESSAGE: + 'Non converged meetings, PSTN or SIP users in converged meetings are not supported currently.', + CODE: 9, + }, + RECLAIM_HOST_ROLE_NOT_ALLOWED: { + NAME: 'ReclaimHostRoleNotAllowed', + MESSAGE: + 'Reclaim Host Role Not Allowed For Other Participants. Participants cannot claim host role in PMR meeting, space instant meeting or escalated instant meeting. However, the original host still can reclaim host role when it manually makes another participant to be the host.', + CODE: 10, + }, + RECLAIM_HOST_ROLE_EMPTY_OR_WRONG_KEY: { + NAME: 'ReclaimHostRoleEmptyOrWrongKey', + MESSAGE: + 'Host Key Not Specified Or Matched. The original host can reclaim the host role without entering the host key. However, any other person who claims the host role must enter the host key to get it.', + CODE: 11, + }, + RECLAIM_HOST_ROLE_IS_ALREADY_HOST: { + NAME: 'ReclaimHostRoleIsAlreadyHost', + MESSAGE: + 'Participant Having Host Role Already. Participant who sends request to reclaim host role has already a host role.', + CODE: 12, + }, }; export const FLOOR_ACTION = { diff --git a/packages/@webex/plugin-meetings/src/member/index.ts b/packages/@webex/plugin-meetings/src/member/index.ts index 149ad0640d3..4d6653001a8 100644 --- a/packages/@webex/plugin-meetings/src/member/index.ts +++ b/packages/@webex/plugin-meetings/src/member/index.ts @@ -11,6 +11,7 @@ import MemberUtil from './util'; */ export default class Member { associatedUser: any; + canReclaimHost: boolean; id: any; isAudioMuted: any; isContentSharing: any; @@ -57,6 +58,13 @@ export default class Member { } | any = {} ) { + /** + * @instance + * @type {Boolean} + * @public + * @memberof Member + */ + this.canReclaimHost = false; /** * The server participant object * @instance @@ -250,6 +258,7 @@ export default class Member { private processParticipant(participant: object) { this.participant = participant; if (participant) { + this.canReclaimHost = MemberUtil.canReclaimHost(participant); this.id = MemberUtil.extractId(participant); this.name = MemberUtil.extractName(participant); this.isAudioMuted = MemberUtil.isAudioMuted(participant); diff --git a/packages/@webex/plugin-meetings/src/member/util.ts b/packages/@webex/plugin-meetings/src/member/util.ts index 815165c7a96..5b40d7638b9 100644 --- a/packages/@webex/plugin-meetings/src/member/util.ts +++ b/packages/@webex/plugin-meetings/src/member/util.ts @@ -20,6 +20,20 @@ import {IMediaStatus} from './member.types'; const MemberUtil: any = {}; +/** + * @param {Object} participant the locus participant + * @returns {Boolean} + */ +MemberUtil.canReclaimHost = (participant) => { + if (!participant) { + throw new ParameterError( + 'canReclaimHostRole could not be processed, participant is undefined.' + ); + } + + return participant.canReclaimHostRole || false; +}; + /** * @param {Object} participant the locus participant * @returns {Boolean} diff --git a/packages/@webex/plugin-meetings/src/members/index.ts b/packages/@webex/plugin-meetings/src/members/index.ts index 7c024aa7656..7b502477c77 100644 --- a/packages/@webex/plugin-meetings/src/members/index.ts +++ b/packages/@webex/plugin-meetings/src/members/index.ts @@ -5,7 +5,14 @@ import {isEmpty} from 'lodash'; // @ts-ignore import {StatelessWebexPlugin} from '@webex/webex-core'; -import {MEETINGS, EVENT_TRIGGERS, FLOOR_ACTION, CONTENT, WHITEBOARD} from '../constants'; +import { + MEETINGS, + EVENT_TRIGGERS, + FLOOR_ACTION, + CONTENT, + WHITEBOARD, + ASSIGN_ROLES_ERROR_CODES, +} from '../constants'; import Trigger from '../common/events/trigger-proxy'; import Member from '../member'; import LoggerProxy from '../common/logs/logger-proxy'; @@ -14,6 +21,13 @@ import ParameterError from '../common/errors/parameter'; import MembersCollection from './collection'; import MembersRequest from './request'; import MembersUtil from './util'; +import {ServerRoleShape} from './types'; +import { + ReclaimHostEmptyWrongKeyError, + ReclaimHostIsHostAlreadyError, + ReclaimHostNotAllowedError, + ReclaimHostNotSupportedError, +} from '../common/errors/reclaim-host-role-error'; /** * Members Update Event @@ -686,6 +700,46 @@ export default class Members extends StatelessWebexPlugin { return this.membersRequest.addMembers(options); } + /** + * Assign role(s) to a member in the meeting + * @param {String} memberId + * @param {[ServerRoleShape]} roles - to assign an array of roles + * @returns {Promise} + * @public + * @memberof Members + */ + public assignRoles(memberId: string, roles: Array) { + if (!this.locusUrl) { + return Promise.reject( + new ParameterError( + 'The associated locus url for this meetings members object must be defined.' + ) + ); + } + if (!memberId) { + return Promise.reject( + new ParameterError('The member id must be defined to assign the roles to a member.') + ); + } + const options = MembersUtil.generateRoleAssignmentMemberOptions(memberId, roles, this.locusUrl); + + return this.membersRequest.assignRolesMember(options).catch((error: any) => { + const errorCode = error.body?.errorCode; + switch (errorCode) { + case ASSIGN_ROLES_ERROR_CODES.ReclaimHostNotSupportedErrorCode: + return Promise.reject(new ReclaimHostNotSupportedError()); + case ASSIGN_ROLES_ERROR_CODES.ReclaimHostNotAllowedErrorCode: + return Promise.reject(new ReclaimHostNotAllowedError()); + case ASSIGN_ROLES_ERROR_CODES.ReclaimHostEmptyWrongKeyErrorCode: + return Promise.reject(new ReclaimHostEmptyWrongKeyError()); + case ASSIGN_ROLES_ERROR_CODES.ReclaimHostIsHostAlreadyErrorCode: + return Promise.reject(new ReclaimHostIsHostAlreadyError()); + default: + return Promise.reject(error); + } + }); + } + /** * Cancels an outgoing PSTN call to the associated meeting * @param {String} invitee diff --git a/packages/@webex/plugin-meetings/src/members/request.ts b/packages/@webex/plugin-meetings/src/members/request.ts index 747905db7ca..3bb0d8fcbeb 100644 --- a/packages/@webex/plugin-meetings/src/members/request.ts +++ b/packages/@webex/plugin-meetings/src/members/request.ts @@ -59,6 +59,26 @@ export default class MembersRequest extends StatelessWebexPlugin { return this.request(requestParams); } + /** + * Sends a request to assign roles to a member + * @param {Object} options + * @param {String} options.locusUrl + * @param {String} options.memberId ID of PSTN user + * @returns {Promise} + */ + assignRolesMember(options: any) { + if (!options || !options.locusUrl || !options.memberId) { + throw new ParameterError( + 'memberId must be defined, and the associated locus url for this meeting object must be defined.' + ); + } + + const requestParams = MembersUtil.getRoleAssignmentMemberRequestParams(options); + + // @ts-ignore + return this.request(requestParams); + } + removeMember(options) { if (!options || !options.locusUrl || !options.memberId) { throw new ParameterError( diff --git a/packages/@webex/plugin-meetings/src/members/types.ts b/packages/@webex/plugin-meetings/src/members/types.ts new file mode 100644 index 00000000000..1562b65fc99 --- /dev/null +++ b/packages/@webex/plugin-meetings/src/members/types.ts @@ -0,0 +1,29 @@ +export enum ServerRoles { + Cohost = 'COHOST', + Moderator = 'MODERATOR', + Presenter = 'PRESENTER', +} + +export type ServerRoleShape = { + type: ServerRoles; + hasRole: boolean; + hostKey?: string; +}; + +export type RoleAssignmentOptions = { + roles: Array; + locusUrl: string; + memberId: string; +}; + +export type RoleAssignmentBody = { + role: { + roles: Array; + }; +}; + +export type RoleAssignmentRequest = { + method: string; + uri: string; + body: RoleAssignmentBody; +}; diff --git a/packages/@webex/plugin-meetings/src/members/util.ts b/packages/@webex/plugin-meetings/src/members/util.ts index 3813d53adff..2ae2fa1d1ec 100644 --- a/packages/@webex/plugin-meetings/src/members/util.ts +++ b/packages/@webex/plugin-meetings/src/members/util.ts @@ -11,6 +11,7 @@ import { SEND_DTMF_ENDPOINT, _REMOVE_, } from '../constants'; +import {RoleAssignmentOptions, RoleAssignmentRequest, ServerRoleShape} from './types'; const MembersUtil: any = {}; @@ -91,6 +92,20 @@ MembersUtil.getAddMemberRequestParams = (format: any) => { return requestParams; }; +/** + * @param {ServerRoleShape} role + * @returns {ServerRoleShape} the role shape to be added to the body + */ +MembersUtil.getAddedRoleShape = (role: ServerRoleShape) => { + const roleShape: ServerRoleShape = {type: role.type, hasRole: role.hasRole}; + + if (role.hostKey) { + roleShape.hostKey = role.hostKey; + } + + return roleShape; +}; + MembersUtil.isInvalidInvitee = (invitee) => { if (!(invitee && (invitee.email || invitee.emailAddress || invitee.phoneNumber))) { return true; @@ -116,6 +131,43 @@ MembersUtil.getRemoveMemberRequestParams = (options) => { }; }; +/** + * @param {String} memberId + * @param {[ServerRoleShape]} roles + * @param {String} locusUrl + * @returns {RoleAssignmentOptions} + */ +MembersUtil.generateRoleAssignmentMemberOptions = ( + memberId: string, + roles: Array, + locusUrl: string +): RoleAssignmentOptions => ({ + memberId, + roles, + locusUrl, +}); + +/** + * @param {RoleAssignmentOptions} options + * @returns {RoleAssignmentRequest} the request parameters (method, uri, body) needed to make a addMember request + */ +MembersUtil.getRoleAssignmentMemberRequestParams = ( + options: RoleAssignmentOptions +): RoleAssignmentRequest => { + const body = {role: {roles: []}}; + options.roles.forEach((role) => { + body.role.roles.push(MembersUtil.getAddedRoleShape(role)); + }); + + const uri = `${options.locusUrl}/${PARTICIPANT}/${options.memberId}/${CONTROLS}`; + + return { + method: HTTP_VERBS.PATCH, + uri, + body, + }; +}; + MembersUtil.generateTransferHostMemberOptions = (transfer, moderator, locusUrl) => ({ moderator, locusUrl, diff --git a/packages/@webex/plugin-meetings/test/unit/spec/member/index.js b/packages/@webex/plugin-meetings/test/unit/spec/member/index.js index d06308bfafb..f2162e11950 100644 --- a/packages/@webex/plugin-meetings/test/unit/spec/member/index.js +++ b/packages/@webex/plugin-meetings/test/unit/spec/member/index.js @@ -20,6 +20,13 @@ describe('member', () => { assert.calledOnceWithExactly(MemberUtil.isHandRaised, participant); }); + + it('checks that processParticipant calls canReclaimHost', () => { + sinon.spy(MemberUtil, 'canReclaimHost'); + member.processParticipant(participant); + + assert.calledOnceWithExactly(MemberUtil.canReclaimHost, participant); + }); }) describe('#processMember', ()=>{ diff --git a/packages/@webex/plugin-meetings/test/unit/spec/member/util.js b/packages/@webex/plugin-meetings/test/unit/spec/member/util.js index 88c9ded5fb9..0dd2dbcad2a 100644 --- a/packages/@webex/plugin-meetings/test/unit/spec/member/util.js +++ b/packages/@webex/plugin-meetings/test/unit/spec/member/util.js @@ -78,3 +78,35 @@ describe('extractMediaStatus', () => { assert.deepEqual(mediaStatus, {audio: 'RECVONLY', video: 'SENDRECV'}); }); }); + +describe('MemberUtil.canReclaimHost', () => { + it('throws error when there is no participant', () => { + assert.throws(() => { + MemberUtil.canReclaimHost(); + }, 'canReclaimHostRole could not be processed, participant is undefined.'); + }); + + it('returns true when canReclaimHostRole is true', () => { + const participant = { + canReclaimHostRole: true, + }; + + assert.isTrue(MemberUtil.canReclaimHost(participant)); + }); + + it('returns false when canReclaimHostRole is false', () => { + const participant = { + canReclaimHostRole: false, + }; + + assert.isFalse(MemberUtil.canReclaimHost(participant)); + }); + + it('returns false when canReclaimHostRole is falsy', () => { + const participant = { + canReclaimHostRole: undefined, + }; + + assert.isFalse(MemberUtil.canReclaimHost(participant)); + }); +}); diff --git a/packages/@webex/plugin-meetings/test/unit/spec/members/index.js b/packages/@webex/plugin-meetings/test/unit/spec/members/index.js index 08f830b96ea..81109d07830 100644 --- a/packages/@webex/plugin-meetings/test/unit/spec/members/index.js +++ b/packages/@webex/plugin-meetings/test/unit/spec/members/index.js @@ -16,6 +16,13 @@ import ParameterError from '@webex/plugin-meetings/src/common/errors/parameter'; import Members from '@webex/plugin-meetings/src/members'; import MembersUtil from '@webex/plugin-meetings/src/members/util'; +import { + ReclaimHostEmptyWrongKeyError, + ReclaimHostIsHostAlreadyError, + ReclaimHostNotAllowedError, + ReclaimHostNotSupportedError, +} from '../../../../src/common/errors/reclaim-host-role-error'; + const {assert} = chai; chai.use(chaiAsPromised); @@ -192,6 +199,204 @@ describe('plugin-meetings', () => { }); }); + describe('#assignRoles', () => { + const fakeRoles = [ + {type: 'PRESENTER', hasRole: true}, + {type: 'MODERATOR', hasRole: false}, + {type: 'COHOST', hasRole: true}, + ]; + + const resolvedValue = "it worked"; + + const genericMessage = 'Generic error from the API'; + + const setup = (locusUrl, errorCode) => { + const members = createMembers({url: locusUrl}); + + const spies = { + generateRoleAssignmentMemberOptions: sandbox.spy( + MembersUtil, + 'generateRoleAssignmentMemberOptions' + ), + }; + + if (errorCode) { + spies.assignRolesMember = sandbox.stub(members.membersRequest, 'assignRolesMember').rejects({body: {errorCode}, message: genericMessage}); + } else { + spies.assignRolesMember = sandbox.stub(members.membersRequest, 'assignRolesMember').resolves(resolvedValue); + } + + return {members, spies}; + }; + + const checkInvalid = async (resultPromise, expectedMessage, spies) => { + await assert.isRejected(resultPromise, ParameterError, expectedMessage); + assert.notCalled(spies.generateRoleAssignmentMemberOptions); + assert.notCalled(spies.assignRolesMember); + }; + + const checkError = async (error, expectedMemberId, expectedRoles, expectedLocusUrl, resultPromise, expectedMessage, spies) => { + await assert.isRejected(resultPromise, error, expectedMessage); + assert.calledOnceWithExactly( + spies.generateRoleAssignmentMemberOptions, + expectedMemberId, + expectedRoles, + expectedLocusUrl + ); + assert.calledOnceWithExactly(spies.assignRolesMember, { + memberId: expectedMemberId, + roles: expectedRoles, + locusUrl: expectedLocusUrl, + }); + }; + + const checkValid = async ( + resultPromise, + spies, + expectedMemberId, + expectedRoles, + expectedLocusUrl + ) => { + const resolvedValue = await assert.isFulfilled(resultPromise); + assert.calledOnceWithExactly( + spies.generateRoleAssignmentMemberOptions, + expectedMemberId, + expectedRoles, + expectedLocusUrl + ); + assert.calledOnceWithExactly(spies.assignRolesMember, { + memberId: expectedMemberId, + roles: expectedRoles, + locusUrl: expectedLocusUrl, + }); + assert.strictEqual(resolvedValue, resolvedValue); + }; + + it('should not make a request if there is no member id', async () => { + const {members, spies} = setup(url1); + + const resultPromise = members.assignRoles(); + + await checkInvalid( + resultPromise, + 'The member id must be defined to assign the roles to a member.', + spies, + ); + }); + + it('should not make a request if there is no locus url', async () => { + const {members, spies} = setup(); + + const resultPromise = members.assignRoles(uuid.v4()); + + await checkInvalid( + resultPromise, + 'The associated locus url for this meetings members object must be defined.', + spies, + ); + }); + + it('should not make a request if locus throws ReclaimHostNotSupportedError', async () => { + const memberId = uuid.v4(); + const {members, spies} = setup(url1, 2400127); + + const resultPromise = members.assignRoles(memberId, fakeRoles); + + await checkError( + ReclaimHostNotSupportedError, + memberId, + fakeRoles, + url1, + resultPromise, + 'Non converged meetings, PSTN or SIP users in converged meetings are not supported currently.', + spies, + ); + }); + + it('should not make a request if locus throws ReclaimHostNotAllowedError', async () => { + const memberId = uuid.v4(); + const {members, spies} = setup(url1, 2403135); + + const resultPromise = members.assignRoles(memberId, fakeRoles); + + await checkError( + ReclaimHostNotAllowedError, + memberId, + fakeRoles, + url1, + resultPromise, + 'Reclaim Host Role Not Allowed For Other Participants. Participants cannot claim host role in PMR meeting, space instant meeting or escalated instant meeting. However, the original host still can reclaim host role when it manually makes another participant to be the host.', + spies, + ); + }); + + it('should not make a request if locus throws ReclaimHostEmptyWrongKeyError', async () => { + const memberId = uuid.v4(); + const {members, spies} = setup(url1, 2403136); + + const resultPromise = members.assignRoles(memberId, fakeRoles); + + await checkError( + ReclaimHostEmptyWrongKeyError, + memberId, + fakeRoles, + url1, + resultPromise, + 'Host Key Not Specified Or Matched. The original host can reclaim the host role without entering the host key. However, any other person who claims the host role must enter the host key to get it.', + spies, + ); + }); + + it('should not make a request if locus throws ReclaimHostIsHostAlreadyError', async () => { + const memberId = uuid.v4(); + const {members, spies} = setup(url1, 2409150); + + const resultPromise = members.assignRoles(memberId, fakeRoles); + + await checkError( + ReclaimHostIsHostAlreadyError, + memberId, + fakeRoles, + url1, + resultPromise, + 'Participant Having Host Role Already. Participant who sends request to reclaim host role has already a host role.', + spies, + ); + }); + + it('should not make a request if locus throws a different error', async () => { + const memberId = uuid.v4(); + const {members, spies} = setup(url1, 1234); + + const resultPromise = members.assignRoles(memberId, fakeRoles); + + await checkError( + {body: {errorCode: 1234}, message: genericMessage}, + memberId, + fakeRoles, + url1, + resultPromise, + genericMessage, + spies, + ); + }); + + it('should make the correct request when called with roles', async () => { + const memberId = uuid.v4(); + const {members, spies} = setup(url1); + + const resultPromise = members.assignRoles(memberId, fakeRoles); + + await checkValid( + resultPromise, + spies, + memberId, + fakeRoles, + url1, + ); + }); + }); + describe('#raiseOrLowerHand', () => { const setup = (locusUrl) => { const members = createMembers({url: locusUrl}); diff --git a/packages/@webex/plugin-meetings/test/unit/spec/members/utils.js b/packages/@webex/plugin-meetings/test/unit/spec/members/utils.js index 39514ea7627..a21e025b378 100644 --- a/packages/@webex/plugin-meetings/test/unit/spec/members/utils.js +++ b/packages/@webex/plugin-meetings/test/unit/spec/members/utils.js @@ -4,6 +4,8 @@ import chaiAsPromised from 'chai-as-promised'; import MembersUtil from '@webex/plugin-meetings/src/members/util'; +import {CONTROLS, PARTICIPANT} from '@webex/plugin-meetings/src/constants'; + const {assert} = chai; chai.use(chaiAsPromised); @@ -38,5 +40,78 @@ describe('plugin-meetings', () => { ); }); }); + + describe('#getAddedRoleShape', () => { + it('returns the correct shape with hostkey', () => { + const format = {type: 'PRESENTER', hasRole: true, hostKey: '123456'}; + assert.deepEqual(MembersUtil.getAddedRoleShape(format), { + type: 'PRESENTER', + hasRole: true, + hostKey: '123456', + }); + }); + + it('returns the correct shape without hostkey', () => { + const format = {type: 'PRESENTER', hasRole: true}; + assert.deepEqual(MembersUtil.getAddedRoleShape(format), { + type: 'PRESENTER', + hasRole: true, + }); + }); + }); + + + describe('#getRoleAssignmentMemberRequestParams', () => { + it('returns the correct request params', () => { + const format = { + locusUrl: 'locusUrl', + memberId: 'test', + roles: [ + {type: 'PRESENTER', hasRole: true}, + {type: 'MODERATOR', hasRole: false}, + {type: 'COHOST', hasRole: true}, + ], + }; + assert.deepEqual(MembersUtil.getRoleAssignmentMemberRequestParams(format), { + method: 'PATCH', + uri: `locusUrl/${PARTICIPANT}/test/${CONTROLS}`, + body: { + role: { + roles: [ + {type: 'PRESENTER', hasRole: true}, + {type: 'MODERATOR', hasRole: false}, + {type: 'COHOST', hasRole: true}, + ], + }, + }, + }); + }); + + it('returns the correct request params with a hostKey', () => { + const format = { + locusUrl: 'locusUrl', + memberId: 'test', + roles: [ + {type: 'PRESENTER', hasRole: true, hostKey: '123456'}, + {type: 'MODERATOR', hasRole: false, hostKey: '123456'}, + {type: 'COHOST', hasRole: true, hostKey: '123456'}, + ], + }; + + assert.deepEqual(MembersUtil.getRoleAssignmentMemberRequestParams(format), { + method: 'PATCH', + uri: `locusUrl/${PARTICIPANT}/test/${CONTROLS}`, + body: { + role: { + roles: [ + {type: 'PRESENTER', hasRole: true, hostKey: '123456'}, + {type: 'MODERATOR', hasRole: false, hostKey: '123456'}, + {type: 'COHOST', hasRole: true, hostKey: '123456'}, + ], + }, + }, + }); + }); + }); }); });