From babb4d6e54e8e6473a663095a5dca8e6e6e172f1 Mon Sep 17 00:00:00 2001 From: Sreekanth Narayanan Date: Fri, 5 Jan 2024 15:28:39 +0530 Subject: [PATCH 1/7] feat(meetings): add reclaim host functionality --- docs/samples/browser-plugin-meetings/app.js | 26 ++++++++++ .../plugin-meetings/src/member/index.ts | 9 ++++ .../@webex/plugin-meetings/src/member/util.ts | 14 ++++++ .../plugin-meetings/src/members/index.ts | 27 ++++++++++ .../plugin-meetings/src/members/request.ts | 20 ++++++++ .../plugin-meetings/src/members/types.ts | 29 +++++++++++ .../plugin-meetings/src/members/util.ts | 50 +++++++++++++++++++ 7 files changed, 175 insertions(+) create mode 100644 packages/@webex/plugin-meetings/src/members/types.ts diff --git a/docs/samples/browser-plugin-meetings/app.js b/docs/samples/browser-plugin-meetings/app.js index 5a542fe9d3b..e9bf55ee13c 100644 --- a/docs/samples/browser-plugin-meetings/app.js +++ b/docs/samples/browser-plugin-meetings/app.js @@ -1795,6 +1795,19 @@ 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]); +} + function viewParticipants() { function createLabel(id, value = '') { const label = document.createElement('label'); @@ -1923,6 +1936,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/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..fd56abe2a75 100644 --- a/packages/@webex/plugin-meetings/src/members/index.ts +++ b/packages/@webex/plugin-meetings/src/members/index.ts @@ -14,6 +14,7 @@ import ParameterError from '../common/errors/parameter'; import MembersCollection from './collection'; import MembersRequest from './request'; import MembersUtil from './util'; +import {ServerRoleShape} from './types'; /** * Members Update Event @@ -686,6 +687,32 @@ 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); + } + /** * 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..20a9b526b96 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, 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,41 @@ MembersUtil.getRemoveMemberRequestParams = (options) => { }; }; +/** + * @param {String} memberId + * @param {[ServerRoleShape]} roles + * @param {String} locusUrl + * @returns {RoleAssignmentOptions} + */ +MembersUtil.generateRoleAssignmentMemberOptions = ( + memberId: string, + roles: Array, + locusUrl: string +) => ({ + memberId, + roles, + locusUrl, +}); + +/** + * @param {RoleAssignmentOptions} options + * @returns {RoleAssignmentRequest} the request parameters (method, uri, body) needed to make a addMember request + */ +MembersUtil.getRoleAssignmentMemberRequestParams = (options: RoleAssignmentOptions) => { + 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, From 399e695826661a7760a50ec1fb1188a60d511c4b Mon Sep 17 00:00:00 2001 From: Sreekanth Narayanan Date: Wed, 10 Jan 2024 19:51:24 +0530 Subject: [PATCH 2/7] fix(ut): add tests for the new reclaim host param --- .../test/unit/spec/member/index.js | 7 +++ .../test/unit/spec/member/util.js | 32 +++++++++++ .../test/unit/spec/members/utils.js | 53 +++++++++++++++++++ 3 files changed, 92 insertions(+) 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/utils.js b/packages/@webex/plugin-meetings/test/unit/spec/members/utils.js index 39514ea7627..14ff674a7b9 100644 --- a/packages/@webex/plugin-meetings/test/unit/spec/members/utils.js +++ b/packages/@webex/plugin-meetings/test/unit/spec/members/utils.js @@ -38,5 +38,58 @@ describe('plugin-meetings', () => { ); }); }); + + 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'}, + ], + }, + }, + }); + }); + }); }); }); From c44d977c486a0b0c8905935dc0cb1ea18378ea8a Mon Sep 17 00:00:00 2001 From: Sreekanth Narayanan Date: Thu, 11 Jan 2024 12:09:57 +0530 Subject: [PATCH 3/7] feat(reclaim-host): add errors for reclaim host --- .../common/errors/reclaim-host-role-error.ts | 134 ++++++++++++++++++ .../@webex/plugin-meetings/src/constants.ts | 31 ++++ .../plugin-meetings/src/members/index.ts | 31 +++- 3 files changed, 194 insertions(+), 2 deletions(-) create mode 100644 packages/@webex/plugin-meetings/src/common/errors/reclaim-host-role-error.ts 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/members/index.ts b/packages/@webex/plugin-meetings/src/members/index.ts index fd56abe2a75..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'; @@ -15,6 +22,12 @@ 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 @@ -710,7 +723,21 @@ export default class Members extends StatelessWebexPlugin { } const options = MembersUtil.generateRoleAssignmentMemberOptions(memberId, roles, this.locusUrl); - return this.membersRequest.assignRolesMember(options); + 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); + } + }); } /** From bf029ee510837b7b9735c55772b805362ad85c5c Mon Sep 17 00:00:00 2001 From: Sreekanth Narayanan Date: Sun, 14 Jan 2024 23:09:35 +0530 Subject: [PATCH 4/7] fix(ut): add ut for assignRoles --- docs/samples/browser-plugin-meetings/app.js | 8 +- .../test/unit/spec/members/index.js | 198 ++++++++++++++++++ .../test/unit/spec/members/utils.js | 20 ++ 3 files changed, 225 insertions(+), 1 deletion(-) diff --git a/docs/samples/browser-plugin-meetings/app.js b/docs/samples/browser-plugin-meetings/app.js index e9bf55ee13c..f9c7104edc4 100644 --- a/docs/samples/browser-plugin-meetings/app.js +++ b/docs/samples/browser-plugin-meetings/app.js @@ -1805,7 +1805,13 @@ function reclaimHost(reclaimHostBtn) { hostKey, }; - meeting.members.assignRoles(selfId, [role]); + meeting.members.assignRoles(selfId, [role]) + .then(() => { + console.log('Host role reclaimed'); + }) + .catch((error) => { + console.log('Error reclaiming host role', error); + }); } function viewParticipants() { 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..f0dc111f005 100644 --- a/packages/@webex/plugin-meetings/test/unit/spec/members/index.js +++ b/packages/@webex/plugin-meetings/test/unit/spec/members/index.js @@ -192,6 +192,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 14ff674a7b9..14a3499aa49 100644 --- a/packages/@webex/plugin-meetings/test/unit/spec/members/utils.js +++ b/packages/@webex/plugin-meetings/test/unit/spec/members/utils.js @@ -39,6 +39,26 @@ 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 = { From 6323f9930aa8dc9e0552373710139bd86674b714 Mon Sep 17 00:00:00 2001 From: Sreekanth Narayanan Date: Mon, 15 Jan 2024 14:20:40 +0530 Subject: [PATCH 5/7] fix(ut): fix failures in UT due to imports --- .../@webex/plugin-meetings/test/unit/spec/members/index.js | 7 +++++++ .../@webex/plugin-meetings/test/unit/spec/members/utils.js | 2 ++ 2 files changed, 9 insertions(+) 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 f0dc111f005..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); 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 14a3499aa49..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); From be775944309a0146e0e9ec26d199ec3e5f778e08 Mon Sep 17 00:00:00 2001 From: Sreekanth Narayanan Date: Wed, 17 Jan 2024 15:02:23 +0530 Subject: [PATCH 6/7] fix(type): updated the return type for the functions --- packages/@webex/plugin-meetings/src/members/util.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/@webex/plugin-meetings/src/members/util.ts b/packages/@webex/plugin-meetings/src/members/util.ts index 20a9b526b96..909ced24a76 100644 --- a/packages/@webex/plugin-meetings/src/members/util.ts +++ b/packages/@webex/plugin-meetings/src/members/util.ts @@ -11,7 +11,7 @@ import { SEND_DTMF_ENDPOINT, _REMOVE_, } from '../constants'; -import {RoleAssignmentOptions, ServerRoleShape} from './types'; +import {RoleAssignmentOptions, RoleAssignmentRequest, ServerRoleShape} from './types'; const MembersUtil: any = {}; @@ -151,7 +151,9 @@ MembersUtil.generateRoleAssignmentMemberOptions = ( * @param {RoleAssignmentOptions} options * @returns {RoleAssignmentRequest} the request parameters (method, uri, body) needed to make a addMember request */ -MembersUtil.getRoleAssignmentMemberRequestParams = (options: RoleAssignmentOptions) => { +MembersUtil.getRoleAssignmentMemberRequestParams = ( + options: RoleAssignmentOptions +): RoleAssignmentRequest => { const body = {role: {roles: []}}; options.roles.forEach((role) => { body.role.roles.push(MembersUtil.getAddedRoleShape(role)); From 0da5ce3cff7e9658ad3e0ce6115cd5b8fe52729d Mon Sep 17 00:00:00 2001 From: Sreekanth Narayanan Date: Wed, 17 Jan 2024 15:02:52 +0530 Subject: [PATCH 7/7] fix(type): updated the return type for the functions --- packages/@webex/plugin-meetings/src/members/util.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@webex/plugin-meetings/src/members/util.ts b/packages/@webex/plugin-meetings/src/members/util.ts index 909ced24a76..2ae2fa1d1ec 100644 --- a/packages/@webex/plugin-meetings/src/members/util.ts +++ b/packages/@webex/plugin-meetings/src/members/util.ts @@ -141,7 +141,7 @@ MembersUtil.generateRoleAssignmentMemberOptions = ( memberId: string, roles: Array, locusUrl: string -) => ({ +): RoleAssignmentOptions => ({ memberId, roles, locusUrl,