From 756450fc5782582e48804d4ba09f8205fafcf2fa Mon Sep 17 00:00:00 2001 From: Martin Machacek Date: Fri, 25 Oct 2024 07:25:14 +0200 Subject: [PATCH] New command: viva engage community set --- .../cmd/viva/engage/engage-community-set.mdx | 13 +- src/m365/viva/commands/engage/Community.ts | 7 +- .../engage/engage-community-set.spec.ts | 27 +++- .../commands/engage/engage-community-set.ts | 17 ++- src/utils/vivaEngage.spec.ts | 128 +++++++++++++++--- src/utils/vivaEngage.ts | 61 +++++++-- 6 files changed, 214 insertions(+), 39 deletions(-) diff --git a/docs/docs/cmd/viva/engage/engage-community-set.mdx b/docs/docs/cmd/viva/engage/engage-community-set.mdx index 09ecf9bb73b..57bf76a5fff 100644 --- a/docs/docs/cmd/viva/engage/engage-community-set.mdx +++ b/docs/docs/cmd/viva/engage/engage-community-set.mdx @@ -16,10 +16,13 @@ m365 viva engage community set [options] ```md definition-list `-i, --id [id]` -: The id of the community. Specify either `id` or `displayName`, but not both. +: The id of the community. Specify either `id`, `displayName` or `entraGroupId`, but not multiple. `-d, --displayName [displayName]` -: The name of the community. Specify either `id` or `displayName`, but not both. +: The name of the community. Specify either `id`, `displayName` or `entraGroupId`, but not multiple. + +`--entraGroupId [entraGroupId]` +: The id of the Microsoft Entra group associated with the community. Specify either `id`, `displayName` or `entraGroupId`, but not multiple. `--newDisplayName [newDisplayName]` : New name for the community. The maximum length is 255 characters. @@ -47,6 +50,12 @@ Update info about the community specified by name m365 viva engage community set --displayName 'Developrs' --newDisplayName 'Developers' ``` +Update info about the community specified by Entra group id + +```sh +m365 viva engage community set --entraGroupId '0bed8b86-5026-4a93-ac7d-56750cc099f1' --newDisplayName 'Developers' +``` + ## Response The command won't return a response on success. \ No newline at end of file diff --git a/src/m365/viva/commands/engage/Community.ts b/src/m365/viva/commands/engage/Community.ts index ed5df596493..bde8d4838fc 100644 --- a/src/m365/viva/commands/engage/Community.ts +++ b/src/m365/viva/commands/engage/Community.ts @@ -1,6 +1,7 @@ export interface Community { - id: string; - displayName: string; + id?: string; + displayName?: string; description?: string; - privacy: string; + privacy?: string; + groupId?: string; } \ No newline at end of file diff --git a/src/m365/viva/commands/engage/engage-community-set.spec.ts b/src/m365/viva/commands/engage/engage-community-set.spec.ts index 4e65d675902..7f73364a19d 100644 --- a/src/m365/viva/commands/engage/engage-community-set.spec.ts +++ b/src/m365/viva/commands/engage/engage-community-set.spec.ts @@ -17,6 +17,7 @@ import { cli } from '../../../../cli/cli.js'; describe(commands.ENGAGE_COMMUNITY_SET, () => { const communityId = 'eyJfdHlwZSI6Ikdyb3VwIiwiaWQiOiI0NzY5MTM1ODIwOSJ9'; const displayName = 'Software Engineers'; + const entraGroupId = '0bed8b86-5026-4a93-ac7d-56750cc099f1'; let log: string[]; let logger: Logger; let commandInfo: CommandInfo; @@ -74,6 +75,11 @@ describe(commands.ENGAGE_COMMUNITY_SET, () => { assert.strictEqual(actual, true); }); + it('passes validation when entraGroupId is specified', async () => { + const actual = await command.validate({ options: { entraGroupId: '0bed8b86-5026-4a93-ac7d-56750cc099f1', description: 'Community for all devs' } }, commandInfo); + assert.strictEqual(actual, true); + }); + it('fails validation when newDisplayName, description or privacy is not specified', async () => { const actual = await command.validate({ options: { displayName: 'Software Engineers' } }, commandInfo); assert.notStrictEqual(actual, true); @@ -109,6 +115,11 @@ describe(commands.ENGAGE_COMMUNITY_SET, () => { assert.notStrictEqual(actual, true); }); + it('fails validation when entraGroupId is not a valid GUID', async () => { + const actual = await command.validate({ options: { entraGroupId: 'foo', description: 'Community for all devs' } }, commandInfo); + assert.notStrictEqual(actual, true); + }); + it('updates info about a community specified by id', async () => { const patchRequestStub = sinon.stub(request, 'patch').callsFake(async (opts) => { if (opts.url === `https://graph.microsoft.com/v1.0/employeeExperience/communities/${communityId}`) { @@ -123,7 +134,7 @@ describe(commands.ENGAGE_COMMUNITY_SET, () => { }); it('updates info about a community specified by displayName', async () => { - sinon.stub(vivaEngage, 'getCommunityIdByDisplayName').resolves(communityId); + sinon.stub(vivaEngage, 'getCommunityByDisplayName').resolves({ id: communityId }); const patchRequestStub = sinon.stub(request, 'patch').callsFake(async (opts) => { if (opts.url === `https://graph.microsoft.com/v1.0/employeeExperience/communities/${communityId}`) { return; @@ -136,6 +147,20 @@ describe(commands.ENGAGE_COMMUNITY_SET, () => { assert(patchRequestStub.called); }); + it('updates info about a community specified by entraGroupId', async () => { + sinon.stub(vivaEngage, 'getCommunityByEntraGroupId').resolves({ id: communityId }); + const patchRequestStub = sinon.stub(request, 'patch').callsFake(async (opts) => { + if (opts.url === `https://graph.microsoft.com/v1.0/employeeExperience/communities/${communityId}`) { + return; + } + + throw 'Invalid request'; + }); + + await command.action(logger, { options: { entraGroupId: entraGroupId, description: 'Community for all devs', privacy: 'Public', verbose: true } }); + assert(patchRequestStub.called); + }); + it('handles error when updating Viva Engage community failed', async () => { sinon.stub(request, 'patch').callsFake(async (opts) => { if (opts.url === `https://graph.microsoft.com/v1.0/employeeExperience/communities/${communityId}`) { diff --git a/src/m365/viva/commands/engage/engage-community-set.ts b/src/m365/viva/commands/engage/engage-community-set.ts index 9d858494cd3..c6061ae9a23 100644 --- a/src/m365/viva/commands/engage/engage-community-set.ts +++ b/src/m365/viva/commands/engage/engage-community-set.ts @@ -1,6 +1,7 @@ import GlobalOptions from '../../../../GlobalOptions.js'; import { Logger } from '../../../../cli/Logger.js'; import request, { CliRequestOptions } from '../../../../request.js'; +import { validation } from '../../../../utils/validation.js'; import { vivaEngage } from '../../../../utils/vivaEngage.js'; import GraphCommand from '../../../base/GraphCommand.js'; import commands from '../../commands.js'; @@ -12,6 +13,7 @@ interface CommandArgs { interface Options extends GlobalOptions { id?: string; displayName?: string; + entraGroupId?: string; newDisplayName?: string; description?: string; privacy?: string; @@ -58,6 +60,9 @@ class VivaEngageCommunitySetCommand extends GraphCommand { { option: '-d, --displayName [displayName]' }, + { + option: '--entraGroupId [entraGroupId]' + }, { option: '--newDisplayName [newDisplayName]' }, @@ -74,6 +79,10 @@ class VivaEngageCommunitySetCommand extends GraphCommand { #initValidators(): void { this.validators.push( async (args: CommandArgs) => { + if (args.options.entraGroupId && !validation.isValidGuid(args.options.entraGroupId)) { + return `${args.options.entraGroupId} is not a valid GUID for the option 'entraGroupId'.`; + } + if (args.options.newDisplayName && args.options.newDisplayName.length > 255) { return `The maximum amount of characters for 'newDisplayName' is 255.`; } @@ -100,15 +109,17 @@ class VivaEngageCommunitySetCommand extends GraphCommand { } #initOptionSets(): void { - this.optionSets.push({ options: ['id', 'displayName'] }); + this.optionSets.push({ options: ['id', 'displayName', 'entraGroupId'] }); } public async commandAction(logger: Logger, args: CommandArgs): Promise { - let communityId = args.options.id; if (args.options.displayName) { - communityId = await vivaEngage.getCommunityIdByDisplayName(args.options.displayName); + communityId = (await vivaEngage.getCommunityByDisplayName(args.options.displayName, ['id'])).id!; + } + else if (args.options.entraGroupId) { + communityId = (await vivaEngage.getCommunityByEntraGroupId(args.options.entraGroupId, ['id'])).id!; } if (this.verbose) { diff --git a/src/utils/vivaEngage.spec.ts b/src/utils/vivaEngage.spec.ts index e372d61f405..8df9f11a0d5 100644 --- a/src/utils/vivaEngage.spec.ts +++ b/src/utils/vivaEngage.spec.ts @@ -10,17 +10,21 @@ 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" + "privacy": "Public", + "groupId": "0bed8b86-5026-4a93-ac7d-56750cc099f1" }; const anotherCommunityResponse = { "id": "eyJfdHlwZ0NzY5SIwiIiSJ9IwO6IaWQiOIMTM1ODikdyb3Vw", "description": "Test only", "displayName": "All Company", - "privacy": "Private" + "privacy": "Private", + "groupId": "0bed8b86-5026-4a93-ac7d-56750cc099f1" }; afterEach(() => { @@ -31,12 +35,14 @@ describe('utils/vivaEngage', () => { ]); }); - it('correctly get single community id by name using getCommunityIdByDisplayName', async () => { + it('correctly get single community id by name using getCommunityByDisplayName', 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)}'`) { + if (opts.url === `https://graph.microsoft.com/v1.0/employeeExperience/communities?$filter=displayName eq '${formatting.encodeQueryParameter(displayName)}'&$select=id`) { return { value: [ - communityResponse + { + id: communityId + } ] }; } @@ -44,17 +50,17 @@ describe('utils/vivaEngage', () => { return 'Invalid Request'; }); - const actual = await vivaEngage.getCommunityIdByDisplayName(displayName); - assert.deepStrictEqual(actual, 'eyJfdHlwZSI6Ikdyb3VwIiwiaWQiOiI0NzY5MTM1ODIwOSJ9'); + const actual = await vivaEngage.getCommunityByDisplayName(displayName, ['id']); + assert.deepStrictEqual(actual, { id: communityId }); }); - it('handles selecting single community when multiple communities with the specified name found using getCommunityIdByDisplayName and cli is set to prompt', async () => { + it('handles selecting single community when multiple communities with the specified name found using getCommunityByDisplayName 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)}'`) { + if (opts.url === `https://graph.microsoft.com/v1.0/employeeExperience/communities?$filter=displayName eq '${formatting.encodeQueryParameter(displayName)}'&$select=id`) { return { value: [ - communityResponse, - anotherCommunityResponse + { id: communityId }, + { id: "eyJfdHlwZ0NzY5SIwiIiSJ9IwO6IaWQiOIMTM1ODikdyb3Vw" } ] }; } @@ -62,25 +68,25 @@ describe('utils/vivaEngage', () => { return 'Invalid Request'; }); - sinon.stub(cli, 'handleMultipleResultsFound').resolves(communityResponse); + sinon.stub(cli, 'handleMultipleResultsFound').resolves({ id: communityId }); - const actual = await vivaEngage.getCommunityIdByDisplayName(displayName); - assert.deepStrictEqual(actual, 'eyJfdHlwZSI6Ikdyb3VwIiwiaWQiOiI0NzY5MTM1ODIwOSJ9'); + const actual = await vivaEngage.getCommunityByDisplayName(displayName, ['id']); + assert.deepStrictEqual(actual, { id: 'eyJfdHlwZSI6Ikdyb3VwIiwiaWQiOiI0NzY5MTM1ODIwOSJ9' }); }); - it('throws error message when no community was found using getCommunityIdByDisplayName', async () => { + it('throws error message when no community was found using getCommunityByDisplayName', 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)}'`) { + if (opts.url === `https://graph.microsoft.com/v1.0/employeeExperience/communities?$filter=displayName eq '${formatting.encodeQueryParameter(invalidDisplayName)}'&$select=id`) { return { value: [] }; } throw 'Invalid Request'; }); - await assert.rejects(vivaEngage.getCommunityIdByDisplayName(invalidDisplayName)), Error(`The specified Viva Engage community '${invalidDisplayName}' does not exist.`); + await assert.rejects(vivaEngage.getCommunityByDisplayName(invalidDisplayName, ['id'])), Error(`The specified Viva Engage community '${invalidDisplayName}' does not exist.`); }); - it('throws error message when multiple communities were found using getCommunityIdByDisplayName', async () => { + it('throws error message when multiple communities were found using getCommunityByDisplayName', async () => { sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => { if (settingName === settingsNames.prompt) { return false; @@ -90,11 +96,11 @@ describe('utils/vivaEngage', () => { }); sinon.stub(request, 'get').callsFake(async opts => { - if (opts.url === `https://graph.microsoft.com/v1.0/employeeExperience/communities?$filter=displayName eq '${formatting.encodeQueryParameter(displayName)}'`) { + if (opts.url === `https://graph.microsoft.com/v1.0/employeeExperience/communities?$filter=displayName eq '${formatting.encodeQueryParameter(displayName)}'&$select=id`) { return { value: [ - communityResponse, - anotherCommunityResponse + { id: communityId }, + { id: "eyJfdHlwZ0NzY5SIwiIiSJ9IwO6IaWQiOIMTM1ODikdyb3Vw" } ] }; } @@ -102,7 +108,85 @@ describe('utils/vivaEngage', () => { return 'Invalid Request'; }); - await assert.rejects(vivaEngage.getCommunityIdByDisplayName(displayName), + await assert.rejects(vivaEngage.getCommunityByDisplayName(displayName, ['id']), Error(`Multiple Viva Engage communities with name '${displayName}' found. Found: ${communityResponse.id}, ${anotherCommunityResponse.id}.`)); }); + + it('correctly get single community by group id using getCommunityByEntraGroupId', async () => { + sinon.stub(request, 'get').callsFake(async opts => { + if (opts.url === 'https://graph.microsoft.com/v1.0/employeeExperience/communities?$select=id,groupId') { + return { + value: [ + { + id: communityId, + groupId: entraGroupId + } + ] + }; + } + + return 'Invalid Request'; + }); + + const actual = await vivaEngage.getCommunityByEntraGroupId(entraGroupId, ['id']); + assert.deepStrictEqual(actual, { id: communityId, groupId: entraGroupId }); + }); + + it('correctly get single community by group id using getCommunityByEntraGroupId and multiple properties', async () => { + sinon.stub(request, 'get').callsFake(async opts => { + if (opts.url === 'https://graph.microsoft.com/v1.0/employeeExperience/communities?$select=id,groupId,displayName') { + return { + value: [ + { + id: communityId, + groupId: entraGroupId, + displayName: displayName + } + ] + }; + } + + return 'Invalid Request'; + }); + + const actual = await vivaEngage.getCommunityByEntraGroupId(entraGroupId, ['id', 'groupId', 'displayName']); + assert.deepStrictEqual(actual, { id: communityId, groupId: entraGroupId, displayName: displayName }); + }); + + it('throws error message when no community was found using getCommunityByEntraGroupId', 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.getCommunityByEntraGroupId(entraGroupId, ['id'])), 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 { groupId: entraGroupId }; + } + + throw 'Invalid Request'; + }); + + const actual = await vivaEngage.getCommunityById(communityId, ['groupId']); + assert.deepStrictEqual(actual, { groupId: '0bed8b86-5026-4a93-ac7d-56750cc099f1' }); + }); + + it('throws error message when no Entra group ID was found using getCommunityById', 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.getCommunityById(communityId, ['groupId'])), Error(`The specified Viva Engage community with ID '${communityId}' does not exist.`); + }); }); \ No newline at end of file diff --git a/src/utils/vivaEngage.ts b/src/utils/vivaEngage.ts index 35bc9461529..0efcbfe39a1 100644 --- a/src/utils/vivaEngage.ts +++ b/src/utils/vivaEngage.ts @@ -1,17 +1,42 @@ 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. + * 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. + * @returns The Viva Engage community. + */ + async getCommunityById(communityId: string, selectProperties: string[]): Promise { + const requestOptions: CliRequestOptions = { + url: `https://graph.microsoft.com/v1.0/employeeExperience/communities/${communityId}?$select=${selectProperties.join(',')}`, + 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; + }, + + /** + * Get Viva Engage community by display name. * @param displayName Community display name. - * @returns The ID of the Viva Engage community. - * @throws Error when the community was not found. + * @param selectProperties Properties to select. + * @returns 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)}'`); + async getCommunityByDisplayName(displayName: string, selectProperties: string[]): Promise { + const communities = await odata.getAllItems(`https://graph.microsoft.com/v1.0/employeeExperience/communities?$filter=displayName eq '${formatting.encodeQueryParameter(displayName)}'&$select=${selectProperties.join(',')}`); if (communities.length === 0) { throw `The specified Viva Engage community '${displayName}' does not exist.`; @@ -20,9 +45,29 @@ export const vivaEngage = { 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 selectedCommunity; + } + + return communities[0]; + }, + + /** + * Get Viva Engage community 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. + * @param selectProperties Properties to select. + * @returns The Viva Engage community. + */ + async getCommunityByEntraGroupId(entraGroupId: string, selectProperties: string[]): Promise { + const properties = selectProperties.includes('groupId') ? selectProperties : [...selectProperties, 'groupId']; + const communities = await odata.getAllItems(`https://graph.microsoft.com/v1.0/employeeExperience/communities?$select=${properties.join(',')}`); + + const filteredCommunity = communities.find(c => c.groupId === entraGroupId); + + if (!filteredCommunity) { + throw `The Microsoft Entra group with id '${entraGroupId}' is not associated with any Viva Engage community.`; } - return communities[0].id; + return filteredCommunity; } -}; \ No newline at end of file +};