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