From 4504b4e9d4072f56bdbcf35e16b2af241b2a3a47 Mon Sep 17 00:00:00 2001 From: Jwaegebaert <38426621+Jwaegebaert@users.noreply.github.com> Date: Fri, 20 Sep 2024 16:21:33 +0200 Subject: [PATCH] Adds command 'viva engage community user list'. Closes #3908 --- .../engage/engage-community-user-list.mdx | 123 +++++++ docs/src/config/sidebars.ts | 5 + src/m365/viva/commands.ts | 1 + src/m365/viva/commands/engage/Community.ts | 1 + .../engage/engage-community-user-list.spec.ts | 310 ++++++++++++++++++ .../engage/engage-community-user-list.ts | 125 +++++++ src/utils/vivaEngage.spec.ts | 187 +++++++++++ src/utils/vivaEngage.ts | 79 +++++ 8 files changed, 831 insertions(+) create mode 100644 docs/docs/cmd/viva/engage/engage-community-user-list.mdx create mode 100644 src/m365/viva/commands/engage/engage-community-user-list.spec.ts create mode 100644 src/m365/viva/commands/engage/engage-community-user-list.ts create mode 100644 src/utils/vivaEngage.spec.ts create mode 100644 src/utils/vivaEngage.ts diff --git a/docs/docs/cmd/viva/engage/engage-community-user-list.mdx b/docs/docs/cmd/viva/engage/engage-community-user-list.mdx new file mode 100644 index 0000000000..689d26a048 --- /dev/null +++ b/docs/docs/cmd/viva/engage/engage-community-user-list.mdx @@ -0,0 +1,123 @@ +import Global from '/docs/cmd/_global.mdx'; +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +# viva engage community user list + +Lists all users within a specified Microsoft 365 Viva Engage community + +## Usage + +```sh +m365 viva engage community user list [options] +``` + +## Options + +```md definition-list +`--communityId [communityId]` +: The ID of the Viva Engage community. Specify `communityId`, `communityDisplayName` or `entraGroupId`. + +`-n, --communityDisplayName [communityDisplayName]` +: The display name of the Viva Engage community. Specify `communityId`, `communityDisplayName` or `entraGroupId`. + +`--entraGroupId [entraGroupId]` +: The ID of the Microsoft 365 group. Specify `communityId`, `communityDisplayName` or `entraGroupId`. + +`-r, --role [role]` +: Filter the results to only users with the given role: `Admin`, `Member`. +``` + + + +## Examples + +List all users from a community specified by ID. + +```sh +m365 viva engage community user list --communityId eyJfdHlwZSI6Ikdyb3VwIiwiaWQiOiIzNjAyMDAxMTAwOSJ9 +``` + +List all admins from a community specified by display name. + +```sh +m365 viva engage community user list --communityDisplayName "All company" --role Admin +``` + +List all members from a community specified by group ID. + +```sh +m365 viva engage community user list --entraGroupId b6c35b51-ebca-445c-885a-63a67d24cb53 --role Member +``` + +## Response + +### Standard response + + + + + ```json + [ + { + "id": "da634de7-d23c-4419-ab83-fcd395b4ebd0", + "businessPhones": [ + "123-555-1215" + ], + "displayName": "Anton Johansen", + "givenName": "Anton", + "jobTitle": "IT Manager", + "mail": null, + "mobilePhone": "123-555-6645", + "officeLocation": "123455", + "preferredLanguage": null, + "surname": "Johansen", + "userPrincipalName": "Anton.Johansen@contoso.onmicrosoft.com", + "roles": [ + "Admin" + ] + } + ] + ``` + + + + + ```text + id displayName userPrincipalName roles + ------------------------------------ ---------------- ----------------------------------------- ------ + da634de7-d23c-4419-ab83-fcd395b4ebd0 Anton Johansen Anton.Johansen@contoso.onmicrosoft.com Admin + ``` + + + + + ```csv + id,displayName,givenName,jobTitle,mail,mobilePhone,officeLocation,preferredLanguage,surname,userPrincipalName + da634de7-d23c-4419-ab83-fcd395b4ebd0,Anton Johansen,Anton,IT Manager,,123-555-6645,123455,,Johansen,Anton.Johansen@contoso.onmicrosoft.com + ``` + + + + + ```md + # viva engage community user list --entraGroupId "b6c35b51-ebca-445c-885a-63a67d24cb53" + + Date: 19/9/2024 + + ## Anton Johansen (da634de7-d23c-4419-ab83-fcd395b4ebd0) + + Property | Value + ---------|------- + id | da634de7-d23c-4419-ab83-fcd395b4ebd0 + displayName | Anton Johansen + givenName | Anton + jobTitle | IT Manager + mobilePhone | 123-555-6645 + officeLocation | 123455 + surname | Johansen + userPrincipalName | Anton.Johansen@contoso.onmicrosoft.com + ``` + + + diff --git a/docs/src/config/sidebars.ts b/docs/src/config/sidebars.ts index ed0cd101d3..6edcefdd3a 100644 --- a/docs/src/config/sidebars.ts +++ b/docs/src/config/sidebars.ts @@ -4474,6 +4474,11 @@ const sidebars: SidebarsConfig = { label: 'engage community list', id: 'cmd/viva/engage/engage-community-list' }, + { + type: 'doc', + label: 'engage community user list', + id: 'cmd/viva/engage/engage-community-user-list' + }, { type: 'doc', label: 'engage group list', diff --git a/src/m365/viva/commands.ts b/src/m365/viva/commands.ts index f273274983..4c43173691 100644 --- a/src/m365/viva/commands.ts +++ b/src/m365/viva/commands.ts @@ -5,6 +5,7 @@ export default { ENGAGE_COMMUNITY_ADD: `${prefix} engage community add`, ENGAGE_COMMUNITY_GET: `${prefix} engage community get`, ENGAGE_COMMUNITY_LIST: `${prefix} engage community list`, + ENGAGE_COMMUNITY_USER_LIST: `${prefix} engage community user list`, ENGAGE_GROUP_LIST: `${prefix} engage group list`, ENGAGE_GROUP_USER_ADD: `${prefix} engage group user add`, ENGAGE_GROUP_USER_REMOVE: `${prefix} engage group user remove`, diff --git a/src/m365/viva/commands/engage/Community.ts b/src/m365/viva/commands/engage/Community.ts index ed5df59649..75f5405227 100644 --- a/src/m365/viva/commands/engage/Community.ts +++ b/src/m365/viva/commands/engage/Community.ts @@ -3,4 +3,5 @@ export interface Community { displayName: string; description?: string; privacy: string; + groupId: string; } \ No newline at end of file diff --git a/src/m365/viva/commands/engage/engage-community-user-list.spec.ts b/src/m365/viva/commands/engage/engage-community-user-list.spec.ts new file mode 100644 index 0000000000..a422217a36 --- /dev/null +++ b/src/m365/viva/commands/engage/engage-community-user-list.spec.ts @@ -0,0 +1,310 @@ + +import assert from 'assert'; +import sinon from 'sinon'; +import auth from '../../../../Auth.js'; +import { Logger } from '../../../../cli/Logger.js'; +import { CommandError } from '../../../../Command.js'; +import request from '../../../../request.js'; +import { telemetry } from '../../../../telemetry.js'; +import { pid } from '../../../../utils/pid.js'; +import { session } from '../../../../utils/session.js'; +import { sinonUtil } from '../../../../utils/sinonUtil.js'; +import commands from '../../commands.js'; +import command from './engage-community-user-list.js'; +import { CommandInfo } from '../../../../cli/CommandInfo.js'; +import { z } from 'zod'; +import { cli } from '../../../../cli/cli.js'; +import { vivaEngage } from '../../../../utils/vivaEngage.js'; + +describe(commands.ENGAGE_COMMUNITY_USER_LIST, () => { + const communityId = 'eyJfdHlwZSI6Ikdyb3VwIiwiaWQiOiIzNjAyMDAxMTAwOSJ9'; + const communityDisplayName = 'All company'; + const entraGroupId = 'b6c35b51-ebca-445c-885a-63a67d24cb53'; + const membersAPIResult = [ + { + "id": "1deb8814-8130-451d-8fcb-849dc7ed47e5", + "businessPhones": [ + "123-555-1215" + ], + "displayName": "Samu Tolonen", + "givenName": "Samu", + "jobTitle": "IT Manager", + "mail": null, + "mobilePhone": "123-555-6645", + "officeLocation": "123455", + "preferredLanguage": null, + "surname": "Tolonen", + "userPrincipalName": "Samu.Tolonen@contoso.onmicrosoft.com" + } + ]; + const membersResult = [ + { + "id": "1deb8814-8130-451d-8fcb-849dc7ed47e5", + "businessPhones": [ + "123-555-1215" + ], + "displayName": "Samu Tolonen", + "givenName": "Samu", + "jobTitle": "IT Manager", + "mail": null, + "mobilePhone": "123-555-6645", + "officeLocation": "123455", + "preferredLanguage": null, + "surname": "Tolonen", + "userPrincipalName": "Samu.Tolonen@contoso.onmicrosoft.com", + "roles": [ + "Member" + ] + } + ]; + const adminsAPIResult = [ + { + "id": "da634de7-d23c-4419-ab83-fcd395b4ebd0", + "businessPhones": [ + "123-555-1215" + ], + "displayName": "Anton Johansen", + "givenName": "Anton", + "jobTitle": "IT Manager", + "mail": null, + "mobilePhone": "123-555-6645", + "officeLocation": "123455", + "preferredLanguage": null, + "surname": "Johansen", + "userPrincipalName": "Anton.Johansen@contoso.onmicrosoft.com" + } + ]; + const adminsResult = [ + { + "id": "da634de7-d23c-4419-ab83-fcd395b4ebd0", + "businessPhones": [ + "123-555-1215" + ], + "displayName": "Anton Johansen", + "givenName": "Anton", + "jobTitle": "IT Manager", + "mail": null, + "mobilePhone": "123-555-6645", + "officeLocation": "123455", + "preferredLanguage": null, + "surname": "Johansen", + "userPrincipalName": "Anton.Johansen@contoso.onmicrosoft.com", + "roles": [ + "Admin" + ] + } + ]; + + let log: string[]; + let logger: Logger; + let loggerLogSpy: sinon.SinonSpy; + let commandInfo: CommandInfo; + let commandOptionsSchema: z.ZodTypeAny; + + before(() => { + sinon.stub(auth, 'restoreAuth').resolves(); + sinon.stub(telemetry, 'trackEvent').returns(); + sinon.stub(pid, 'getProcessName').returns(''); + sinon.stub(session, 'getId').returns(''); + auth.connection.active = true; + commandInfo = cli.getCommandInfo(command); + commandOptionsSchema = commandInfo.command.getSchemaToParse()!; + }); + + beforeEach(() => { + log = []; + logger = { + log: async (msg: string) => { + log.push(msg); + }, + logRaw: async (msg: string) => { + log.push(msg); + }, + logToStderr: async (msg: string) => { + log.push(msg); + } + }; + loggerLogSpy = sinon.spy(logger, 'log'); + }); + + afterEach(() => { + sinonUtil.restore([ + request.get, + vivaEngage.getEntraGroupIdByCommunityId + ]); + }); + + after(() => { + sinon.restore(); + auth.connection.active = false; + }); + + it('has correct name', () => { + assert.strictEqual(command.name, commands.ENGAGE_COMMUNITY_USER_LIST); + }); + + it('has a description', () => { + assert.notStrictEqual(command.description, null); + }); + + it('fails validation if entraGroupId is not a valid GUID', () => { + const actual = commandOptionsSchema.safeParse({ + entraGroupId: 'invalid' + }); + assert.notStrictEqual(actual.success, true); + }); + + it('fails validation if communityId, communityDisplayName or entraGroupId are not specified', () => { + const actual = commandOptionsSchema.safeParse({}); + assert.notStrictEqual(actual.success, true); + }); + + it('fails validation if communityId, communityDisplayName and entraGroupId are specified', () => { + const actual = commandOptionsSchema.safeParse({ + communityId: communityId, + communityDisplayName: communityDisplayName, + entraGroupId: entraGroupId + }); + assert.notStrictEqual(actual.success, true); + }); + + it('fails validation if incorrect role value is specified', () => { + const actual = commandOptionsSchema.safeParse({ + communityId: communityId, + role: 'invalid' + }); + assert.notStrictEqual(actual.success, true); + }); + + it('passes validation if communityId is specified', () => { + const actual = commandOptionsSchema.safeParse({ + communityId: communityId + }); + assert.strictEqual(actual.success, true); + }); + + it('passes validation if entraGroupId is specified with a proper GUID', () => { + const actual = commandOptionsSchema.safeParse({ + entraGroupId: entraGroupId + }); + assert.strictEqual(actual.success, true); + }); + + it('passes validation if communityDisplayName is specified', () => { + const actual = commandOptionsSchema.safeParse({ + communityDisplayName: communityDisplayName + }); + assert.strictEqual(actual.success, true); + }); + + it('passes validation if role is specified with a proper value', () => { + const actual = commandOptionsSchema.safeParse({ + communityId: communityId, + role: 'Admin' + }); + assert.strictEqual(actual.success, true); + }); + + it('correctly gets the list of users in the community by entraGroupId', async () => { + sinon.stub(request, 'get').callsFake(async (opts) => { + if (opts.url === `https://graph.microsoft.com/v1.0/groups/${entraGroupId}/owners`) { + return { value: adminsAPIResult }; + } + + if (opts.url === `https://graph.microsoft.com/v1.0/groups/${entraGroupId}/members`) { + return { value: membersAPIResult }; + } + + throw 'Invalid request'; + }); + + await command.action(logger, { options: { entraGroupId: entraGroupId, verbose: true } }); + assert(loggerLogSpy.calledWith([...adminsResult, ...membersResult])); + }); + + it('correctly gets the list of users in the community by communityId', async () => { + sinon.stub(request, 'get').callsFake(async (opts) => { + if (opts.url === `https://graph.microsoft.com/v1.0/groups/${entraGroupId}/owners`) { + return { value: adminsAPIResult }; + } + + if (opts.url === `https://graph.microsoft.com/v1.0/groups/${entraGroupId}/members`) { + return { value: membersAPIResult }; + } + + throw 'Invalid request'; + }); + + sinon.stub(vivaEngage, 'getEntraGroupIdByCommunityId').resolves(entraGroupId); + + await command.action(logger, { options: { communityId: communityId, verbose: true } }); + assert(loggerLogSpy.calledWith([...adminsResult, ...membersResult])); + }); + + it('correctly gets the list of users in the community by communityName', async () => { + sinon.stub(request, 'get').callsFake(async (opts) => { + if (opts.url === `https://graph.microsoft.com/v1.0/groups/${entraGroupId}/owners`) { + return { value: adminsAPIResult }; + } + + if (opts.url === `https://graph.microsoft.com/v1.0/groups/${entraGroupId}/members`) { + return { value: membersAPIResult }; + } + + throw 'Invalid request'; + }); + + sinon.stub(vivaEngage, 'getEntraGroupIdByCommunityDisplayName').resolves(entraGroupId); + + await command.action(logger, { options: { communityDisplayName: communityDisplayName, verbose: true } }); + assert(loggerLogSpy.calledWith([...adminsResult, ...membersResult])); + }); + + it('correctly gets the list of members in the community by entraGroupId', async () => { + sinon.stub(request, 'get').callsFake(async (opts) => { + if (opts.url === `https://graph.microsoft.com/v1.0/groups/${entraGroupId}/owners`) { + return { value: adminsAPIResult }; + } + + if (opts.url === `https://graph.microsoft.com/v1.0/groups/${entraGroupId}/members`) { + return { value: membersAPIResult }; + } + + throw 'Invalid request'; + }); + + await command.action(logger, { options: { entraGroupId: entraGroupId, role: 'Member' } }); + assert(loggerLogSpy.calledWith(membersResult)); + }); + + it('correctly gets the list of admins in the community by communityId', async () => { + sinon.stub(request, 'get').callsFake(async (opts) => { + if (opts.url === `https://graph.microsoft.com/v1.0/groups/${entraGroupId}/owners`) { + return { value: adminsAPIResult }; + } + + if (opts.url === `https://graph.microsoft.com/v1.0/groups/${entraGroupId}/members`) { + return { value: membersAPIResult }; + } + + throw 'Invalid request'; + }); + + sinon.stub(vivaEngage, 'getEntraGroupIdByCommunityId').resolves(entraGroupId); + + await command.action(logger, { options: { communityId: communityId, role: 'Admin' } }); + assert(loggerLogSpy.calledWith(adminsResult)); + }); + + it('correctly handles error', async () => { + const errorMessage = 'Bad request.'; + sinon.stub(request, 'get').rejects({ + error: { + message: errorMessage + } + }); + + await assert.rejects(command.action(logger, { options: { id: 'invalid', verbose: true } }), + new CommandError(errorMessage)); + }); +}); \ No newline at end of file diff --git a/src/m365/viva/commands/engage/engage-community-user-list.ts b/src/m365/viva/commands/engage/engage-community-user-list.ts new file mode 100644 index 0000000000..af559b7c99 --- /dev/null +++ b/src/m365/viva/commands/engage/engage-community-user-list.ts @@ -0,0 +1,125 @@ +import { z } from 'zod'; +import { Logger } from '../../../../cli/Logger.js'; +import { globalOptionsZod } from '../../../../Command.js'; +import { zod } from '../../../../utils/zod.js'; +import GraphCommand from '../../../base/GraphCommand.js'; +import commands from '../../commands.js'; +import { validation } from '../../../../utils/validation.js'; +import { vivaEngage } from '../../../../utils/vivaEngage.js'; +import { CliRequestOptions } from '../../../../request.js'; +import { User } from '@microsoft/microsoft-graph-types'; +import { odata } from '../../../../utils/odata.js'; + +const options = globalOptionsZod + .extend({ + communityId: z.string().optional(), + communityDisplayName: zod.alias('n', z.string().optional()), + entraGroupId: z.string() + .refine(name => validation.isValidGuid(name), name => ({ + message: `'${name}' is not a valid GUID.` + })).optional(), + role: zod.alias('r', z.enum(['Admin', 'Member']).optional()) + }) + .strict(); +declare type Options = z.infer; + +interface CommandArgs { + options: Options; +} + +interface ExtendedUser extends User { + roles: string[]; +} + +class VivaEngageCommunityUserListCommand extends GraphCommand { + + public get name(): string { + return commands.ENGAGE_COMMUNITY_USER_LIST; + } + + public get description(): string { + return 'Lists all users within a specified Microsoft 365 Viva Engage community'; + } + + public get schema(): z.ZodTypeAny { + return options; + } + + public getRefinedSchema(schema: typeof options): z.ZodEffects | undefined { + return schema + .refine(options => [options.communityId, options.communityDisplayName, options.entraGroupId].filter(x => x !== undefined).length === 1, { + message: 'Specify either communityId, communityDisplayName, or entraGroupId, but not multiple.' + }) + .refine(options => options.communityId || options.communityDisplayName || options.entraGroupId, { + message: 'Specify at least one of communityId, communityDisplayName, or entraGroupId.' + }); + } + + public defaultProperties(): string[] | undefined { + return ['id', 'displayName', 'userPrincipalName', 'roles']; + } + + public async commandAction(logger: Logger, args: CommandArgs): Promise { + try { + if (this.verbose) { + await logger.logToStderr('Getting list of users in community...'); + } + + let entraGroupId = args.options.entraGroupId; + + if (args.options.communityDisplayName) { + entraGroupId = await vivaEngage.getEntraGroupIdByCommunityDisplayName(args.options.communityDisplayName); + } + + if (args.options.communityId) { + entraGroupId = await vivaEngage.getEntraGroupIdByCommunityId(args.options.communityId); + } + + const requestOptions: CliRequestOptions = { + url: `${this.resource}/v1.0/groups/${entraGroupId}/members`, + headers: { + accept: 'application/json;odata.metadata=none' + }, + responseType: 'json' + }; + const members = await odata.getAllItems(requestOptions); + + requestOptions.url = `${this.resource}/v1.0/groups/${entraGroupId}/owners`; + const owners = await odata.getAllItems(requestOptions); + + const extendedMembers: ExtendedUser[] = members.map(m => { + return { + ...m, + roles: ['Member'] + }; + }); + + const extendedOwners: ExtendedUser[] = owners.map(o => { + return { + ...o, + roles: ['Admin'] + }; + }); + + let users: ExtendedUser[] = []; + if (args.options.role) { + if (args.options.role === 'Member') { + users = users.concat(extendedMembers); + } + if (args.options.role === 'Admin') { + users = users.concat(extendedOwners); + } + } + else { + users = extendedOwners.concat(extendedMembers); + } + + await logger.log(users); + } + catch (err: any) { + this.handleRejectedODataJsonPromise(err); + } + } +} + +export default new VivaEngageCommunityUserListCommand(); \ No newline at end of file diff --git a/src/utils/vivaEngage.spec.ts b/src/utils/vivaEngage.spec.ts new file mode 100644 index 0000000000..baf42f2aed --- /dev/null +++ b/src/utils/vivaEngage.spec.ts @@ -0,0 +1,187 @@ +import assert from 'assert'; +import sinon from 'sinon'; +import { cli } from '../cli/cli.js'; +import request from '../request.js'; +import { sinonUtil } from './sinonUtil.js'; +import { vivaEngage } from './vivaEngage.js'; +import { formatting } from './formatting.js'; +import { settingsNames } from '../settingsNames.js'; + +describe('utils/vivaEngage', () => { + const displayName = 'All Company'; + const invalidDisplayName = 'All Compayn'; + const entraGroupId = '0bed8b86-5026-4a93-ac7d-56750cc099f1'; + const communityId = 'eyJfdHlwZSI6Ikdyb3VwIiwiaWQiOiI0NzY5MTM1ODIwOSJ9'; + const communityResponse = { + "id": "eyJfdHlwZSI6Ikdyb3VwIiwiaWQiOiI0NzY5MTM1ODIwOSJ9", + "description": "This is the default group for everyone in the network", + "displayName": "All Company", + "privacy": "Public", + "groupId": "0bed8b86-5026-4a93-ac7d-56750cc099f1" + }; + const anotherCommunityResponse = { + "id": "eyJfdHlwZ0NzY5SIwiIiSJ9IwO6IaWQiOIMTM1ODikdyb3Vw", + "description": "Test only", + "displayName": "All Company", + "privacy": "Private", + "groupId": "0bed8b86-5026-4a93-ac7d-56750cc099f1" + }; + + afterEach(() => { + sinonUtil.restore([ + request.get, + cli.getSettingWithDefaultValue, + cli.handleMultipleResultsFound + ]); + }); + + it('correctly get single community id by name using getCommunityIdByDisplayName', async () => { + sinon.stub(request, 'get').callsFake(async opts => { + if (opts.url === `https://graph.microsoft.com/v1.0/employeeExperience/communities?$filter=displayName eq '${formatting.encodeQueryParameter(displayName)}'`) { + return { + value: [ + communityResponse + ] + }; + } + + return 'Invalid Request'; + }); + + const actual = await vivaEngage.getCommunityIdByDisplayName(displayName); + assert.deepStrictEqual(actual, 'eyJfdHlwZSI6Ikdyb3VwIiwiaWQiOiI0NzY5MTM1ODIwOSJ9'); + }); + + it('handles selecting single community when multiple communities with the specified name found using getCommunityIdByDisplayName and cli is set to prompt', async () => { + sinon.stub(request, 'get').callsFake(async opts => { + if (opts.url === `https://graph.microsoft.com/v1.0/employeeExperience/communities?$filter=displayName eq '${formatting.encodeQueryParameter(displayName)}'`) { + return { + value: [ + communityResponse, + anotherCommunityResponse + ] + }; + } + + return 'Invalid Request'; + }); + + sinon.stub(cli, 'handleMultipleResultsFound').resolves(communityResponse); + + const actual = await vivaEngage.getCommunityIdByDisplayName(displayName); + assert.deepStrictEqual(actual, 'eyJfdHlwZSI6Ikdyb3VwIiwiaWQiOiI0NzY5MTM1ODIwOSJ9'); + }); + + it('throws error message when no community was found using getCommunityIdByDisplayName', async () => { + sinon.stub(request, 'get').callsFake(async (opts) => { + if (opts.url === `https://graph.microsoft.com/v1.0/employeeExperience/communities?$filter=displayName eq '${formatting.encodeQueryParameter(invalidDisplayName)}'`) { + return { value: [] }; + } + + throw 'Invalid Request'; + }); + + await assert.rejects(vivaEngage.getCommunityIdByDisplayName(invalidDisplayName)), Error(`The specified Viva Engage community '${invalidDisplayName}' does not exist.`); + }); + + it('throws error message when multiple communities were found using getCommunityIdByDisplayName', async () => { + sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => { + if (settingName === settingsNames.prompt) { + return false; + } + + return defaultValue; + }); + + sinon.stub(request, 'get').callsFake(async opts => { + if (opts.url === `https://graph.microsoft.com/v1.0/employeeExperience/communities?$filter=displayName eq '${formatting.encodeQueryParameter(displayName)}'`) { + return { + value: [ + communityResponse, + anotherCommunityResponse + ] + }; + } + + return 'Invalid Request'; + }); + + await assert.rejects(vivaEngage.getCommunityIdByDisplayName(displayName), + Error(`Multiple Viva Engage communities with name '${displayName}' found. Found: ${communityResponse.id}, ${anotherCommunityResponse.id}.`)); + }); + + it('correctly get single community id by group id using getCommunityIdByEntraGroupId', async () => { + sinon.stub(request, 'get').callsFake(async opts => { + if (opts.url === 'https://graph.microsoft.com/v1.0/employeeExperience/communities?$select=id,groupId') { + return { + value: [ + communityResponse + ] + }; + } + + return 'Invalid Request'; + }); + + const actual = await vivaEngage.getCommunityIdByEntraGroupId(entraGroupId); + assert.deepStrictEqual(actual, 'eyJfdHlwZSI6Ikdyb3VwIiwiaWQiOiI0NzY5MTM1ODIwOSJ9'); + }); + + it('throws error message when no community was found using getCommunityIdByEntraGroupId', async () => { + sinon.stub(request, 'get').callsFake(async (opts) => { + if (opts.url === 'https://graph.microsoft.com/v1.0/employeeExperience/communities?$select=id,groupId') { + return { value: [] }; + } + + throw 'Invalid Request'; + }); + + await assert.rejects(vivaEngage.getCommunityIdByEntraGroupId(entraGroupId)), Error(`The Microsoft Entra group with id '${entraGroupId}' is not associated with any Viva Engage community.`); + }); + + it('correctly gets Entra group ID by community ID using getEntraGroupIdByCommunityId', async () => { + sinon.stub(request, 'get').callsFake(async (opts) => { + if (opts.url === `https://graph.microsoft.com/v1.0/employeeExperience/communities/${communityId}?$select=groupId`) { + return communityResponse; + } + + throw 'Invalid Request'; + }); + + const actual = await vivaEngage.getEntraGroupIdByCommunityId(communityId); + assert.deepStrictEqual(actual, '0bed8b86-5026-4a93-ac7d-56750cc099f1'); + }); + + it('throws error message when no Entra group ID was found using getEntraGroupIdByCommunityId', async () => { + sinon.stub(request, 'get').callsFake(async (opts) => { + if (opts.url === `https://graph.microsoft.com/v1.0/employeeExperience/communities/${communityId}?$select=groupId`) { + return null; + } + + throw 'Invalid Request'; + }); + + await assert.rejects(vivaEngage.getEntraGroupIdByCommunityId(communityId)), Error(`The specified Viva Engage community with ID '${communityId}' does not exist.`); + }); + + it('correctly gets Entra group ID by community display name using getEntraGroupIdByCommunityDisplayName', async () => { + sinon.stub(request, 'get').callsFake(async (opts) => { + if (opts.url === `https://graph.microsoft.com/v1.0/employeeExperience/communities/${communityId}?$select=groupId`) { + return communityResponse; + } + + if (opts.url === `https://graph.microsoft.com/v1.0/employeeExperience/communities?$filter=displayName eq '${formatting.encodeQueryParameter(displayName)}'`) { + return { + value: [ + communityResponse + ] + }; + } + + throw 'Invalid Request'; + }); + + const actual = await vivaEngage.getEntraGroupIdByCommunityDisplayName(displayName); + assert.deepStrictEqual(actual, entraGroupId); + }); +}); \ No newline at end of file diff --git a/src/utils/vivaEngage.ts b/src/utils/vivaEngage.ts new file mode 100644 index 0000000000..9fc4403a01 --- /dev/null +++ b/src/utils/vivaEngage.ts @@ -0,0 +1,79 @@ +import { cli } from '../cli/cli.js'; +import { Community } from '../m365/viva/commands/engage/Community.js'; +import request, { CliRequestOptions } from '../request.js'; +import { formatting } from './formatting.js'; +import { odata } from './odata.js'; + +export const vivaEngage = { + /** + * Get Viva Engage community ID by display name. + * @param displayName Community display name. + * @returns The ID of the Viva Engage community. + */ + async getCommunityIdByDisplayName(displayName: string): Promise { + const communities = await odata.getAllItems(`https://graph.microsoft.com/v1.0/employeeExperience/communities?$filter=displayName eq '${formatting.encodeQueryParameter(displayName)}'`); + + if (communities.length === 0) { + throw `The specified Viva Engage community '${displayName}' does not exist.`; + } + + if (communities.length > 1) { + const resultAsKeyValuePair = formatting.convertArrayToHashTable('id', communities); + const selectedCommunity = await cli.handleMultipleResultsFound(`Multiple Viva Engage communities with name '${displayName}' found.`, resultAsKeyValuePair); + return selectedCommunity.id; + } + + return communities[0].id; + }, + + /** + * Get Viva Engage community ID by Microsoft Entra group ID. + * Note: The Graph API doesn't support filtering by groupId, so we need to retrieve all communities and filter them in memory. + * @param entraGroupId The ID of the Microsoft Entra group. + * @returns The ID of the Viva Engage community. + */ + async getCommunityIdByEntraGroupId(entraGroupId: string): Promise { + const communities = await odata.getAllItems('https://graph.microsoft.com/v1.0/employeeExperience/communities?$select=id,groupId'); + + const filtereCommunities = communities.filter(c => c.groupId === entraGroupId); + + if (filtereCommunities.length === 0) { + throw `The Microsoft Entra group with id '${entraGroupId}' is not associated with any Viva Engage community.`; + } + + return filtereCommunities[0].id; + }, + + /** + * Get Viva Engage group ID by community ID. + * @param communityId The ID of the Viva Engage community. + * @returns The ID of the Viva Engage group. + */ + async getEntraGroupIdByCommunityId(communityId: string): Promise { + const requestOptions: CliRequestOptions = { + url: `https://graph.microsoft.com/v1.0/employeeExperience/communities/${communityId}?$select=groupId`, + headers: { + accept: 'application/json;odata.metadata=none' + }, + responseType: 'json' + }; + + const community = await request.get(requestOptions); + + if (!community) { + throw `The specified Viva Engage community with ID '${communityId}' does not exist.`; + } + + return community.groupId; + }, + + /** + * Get Viva Engage group ID by community display name. + * @param displayName Community display name. + * @returns The ID of the Viva Engage group. + */ + async getEntraGroupIdByCommunityDisplayName(displayName: string): Promise { + const communityId = await this.getCommunityIdByDisplayName(displayName); + return await this.getEntraGroupIdByCommunityId(communityId); + } +}; \ No newline at end of file