From dee9610b7d90d54dc2b13fbf483c3a9f16d92338 Mon Sep 17 00:00:00 2001 From: Martin Machacek Date: Fri, 25 Oct 2024 10:11:04 +0200 Subject: [PATCH] New command: viva engage community remove --- src/m365/viva/commands/engage/Community.ts | 12 +- .../engage/engage-community-remove.spec.ts | 35 ++++- .../engage/engage-community-remove.ts | 30 +++- src/utils/vivaEngage.spec.ts | 128 +++++++++++++++--- src/utils/vivaEngage.ts | 59 +++++++- 5 files changed, 221 insertions(+), 43 deletions(-) diff --git a/src/m365/viva/commands/engage/Community.ts b/src/m365/viva/commands/engage/Community.ts index 838f888ad13..bde8d4838fc 100644 --- a/src/m365/viva/commands/engage/Community.ts +++ b/src/m365/viva/commands/engage/Community.ts @@ -1,11 +1,7 @@ export interface Community { - id: string; - displayName: string; + id?: string; + displayName?: string; description?: string; - privacy: string; -}export interface Community { - 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-remove.spec.ts b/src/m365/viva/commands/engage/engage-community-remove.spec.ts index 5968308b04a..4b6e131743f 100644 --- a/src/m365/viva/commands/engage/engage-community-remove.spec.ts +++ b/src/m365/viva/commands/engage/engage-community-remove.spec.ts @@ -12,14 +12,17 @@ import { sinonUtil } from '../../../../utils/sinonUtil.js'; import { cli } from '../../../../cli/cli.js'; import command from './engage-community-remove.js'; import { vivaEngage } from '../../../../utils/vivaEngage.js'; +import { CommandInfo } from '../../../../cli/CommandInfo.js'; describe(commands.ENGAGE_COMMUNITY_REMOVE, () => { const communityId = 'eyJfdHlwZSI6Ikdyb3VwIiwiaWQiOiI0NzY5MTM1ODIwOSJ9'; const displayName = 'Software Engineers'; + const entraGroupId = '0bed8b86-5026-4a93-ac7d-56750cc099f1'; let log: string[]; let logger: Logger; let promptIssued: boolean; + let commandInfo: CommandInfo; before(() => { sinon.stub(auth, 'restoreAuth').resolves(); @@ -27,6 +30,7 @@ describe(commands.ENGAGE_COMMUNITY_REMOVE, () => { sinon.stub(pid, 'getProcessName').returns(''); sinon.stub(session, 'getId').returns(''); auth.connection.active = true; + commandInfo = cli.getCommandInfo(command); }); beforeEach(() => { @@ -53,7 +57,6 @@ describe(commands.ENGAGE_COMMUNITY_REMOVE, () => { afterEach(() => { sinonUtil.restore([ request.delete, - vivaEngage.getCommunityIdByDisplayName, cli.promptForConfirmation ]); }); @@ -71,6 +74,16 @@ describe(commands.ENGAGE_COMMUNITY_REMOVE, () => { assert.notStrictEqual(command.description, null); }); + it('passes validation when entraGroupId is specified', async () => { + const actual = await command.validate({ options: { entraGroupId: '0bed8b86-5026-4a93-ac7d-56750cc099f1' } }, commandInfo); + assert.strictEqual(actual, true); + }); + + it('fails validation when entraGroupId is not a valid GUID', async () => { + const actual = await command.validate({ options: { entraGroupId: 'foo' } }, commandInfo); + assert.notStrictEqual(actual, true); + }); + it('prompts before removing the community when confirm option not passed', async () => { await command.action(logger, { options: { id: communityId } }); @@ -98,7 +111,7 @@ describe(commands.ENGAGE_COMMUNITY_REMOVE, () => { }); it('removes the community specified by displayName while prompting for confirmation', async () => { - sinon.stub(vivaEngage, 'getCommunityIdByDisplayName').resolves(communityId); + sinon.stub(vivaEngage, 'getCommunityByDisplayName').resolves({ id: communityId }); const deleteRequestStub = sinon.stub(request, 'delete').callsFake(async (opts) => { if (opts.url === `https://graph.microsoft.com/v1.0/employeeExperience/communities/${communityId}`) { @@ -115,6 +128,24 @@ describe(commands.ENGAGE_COMMUNITY_REMOVE, () => { assert(deleteRequestStub.called); }); + it('removes the community specified by Entra group id while prompting for confirmation', async () => { + sinon.stub(vivaEngage, 'getCommunityByEntraGroupId').resolves({ id: communityId }); + + const deleteRequestStub = sinon.stub(request, 'delete').callsFake(async (opts) => { + if (opts.url === `https://graph.microsoft.com/v1.0/employeeExperience/communities/${communityId}`) { + return; + } + + throw 'Invalid request'; + }); + + sinonUtil.restore(cli.promptForConfirmation); + sinon.stub(cli, 'promptForConfirmation').resolves(true); + + await command.action(logger, { options: { entraGroupId: entraGroupId } }); + assert(deleteRequestStub.called); + }); + it('throws an error when the community specified by id cannot be found', async () => { const error = { error: { diff --git a/src/m365/viva/commands/engage/engage-community-remove.ts b/src/m365/viva/commands/engage/engage-community-remove.ts index 88a4d0f6532..25b48209ef2 100644 --- a/src/m365/viva/commands/engage/engage-community-remove.ts +++ b/src/m365/viva/commands/engage/engage-community-remove.ts @@ -2,6 +2,7 @@ import GlobalOptions from '../../../../GlobalOptions.js'; import { Logger } from '../../../../cli/Logger.js'; import { cli } from '../../../../cli/cli.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'; @@ -13,6 +14,7 @@ interface CommandArgs { interface Options extends GlobalOptions { id?: string; displayName?: string; + entraGroupId?: string; force?: boolean } @@ -29,6 +31,7 @@ class VivaEngageCommunityRemoveCommand extends GraphCommand { this.#initTelemetry(); this.#initOptions(); + this.#initValidators(); this.#initOptionSets(); this.#initTypes(); } @@ -38,6 +41,7 @@ class VivaEngageCommunityRemoveCommand extends GraphCommand { Object.assign(this.telemetryProperties, { id: args.options.id !== 'undefined', displayName: args.options.displayName !== 'undefined', + entraGroupId: args.options.entraGroupId !== 'undefined', force: !!args.options.force }); }); @@ -51,22 +55,37 @@ class VivaEngageCommunityRemoveCommand extends GraphCommand { { option: '-n, --displayName [displayName]' }, + { + option: '--entraGroupId [entraGroupId]' + }, { option: '-f, --force' } ); } + #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'.`; + } + + return true; + } + ); + } + #initOptionSets(): void { this.optionSets.push( { - options: ['id', 'displayName'] + options: ['id', 'displayName', 'entraGroupId'] } ); } #initTypes(): void { - this.types.string.push('id', 'displayName'); + this.types.string.push('id', 'displayName', 'entraGroupId'); } public async commandAction(logger: Logger, args: CommandArgs): Promise { @@ -76,7 +95,10 @@ class VivaEngageCommunityRemoveCommand extends GraphCommand { 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 (args.options.verbose) { @@ -101,7 +123,7 @@ class VivaEngageCommunityRemoveCommand extends GraphCommand { await removeCommunity(); } else { - const result = await cli.promptForConfirmation({ message: `Are you sure you want to remove Viva Engage community '${args.options.id || args.options.displayName}'?` }); + const result = await cli.promptForConfirmation({ message: `Are you sure you want to remove Viva Engage community '${args.options.id || args.options.displayName || args.options.entraGroupId }'?` }); if (result) { await removeCommunity(); 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..40ba84ddc2d 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