From 9fb7665ef80ccb541726cb64be7c53f374890d35 Mon Sep 17 00:00:00 2001 From: Martin Machacek Date: Fri, 30 Aug 2024 08:42:29 +0200 Subject: [PATCH 1/4] New command: viva engage community remove --- .../viva/engage/engage-community-remove.mdx | 52 +++++++ docs/src/config/sidebars.ts | 5 + src/m365/viva/commands.ts | 1 + src/m365/viva/commands/engage/Community.ts | 5 + .../engage/engage-community-remove.spec.ts | 141 ++++++++++++++++++ .../engage/engage-community-remove.ts | 113 ++++++++++++++ 6 files changed, 317 insertions(+) create mode 100644 docs/docs/cmd/viva/engage/engage-community-remove.mdx create mode 100644 src/m365/viva/commands/engage/engage-community-remove.spec.ts create mode 100644 src/m365/viva/commands/engage/engage-community-remove.ts diff --git a/docs/docs/cmd/viva/engage/engage-community-remove.mdx b/docs/docs/cmd/viva/engage/engage-community-remove.mdx new file mode 100644 index 00000000000..e4f0aeb5b05 --- /dev/null +++ b/docs/docs/cmd/viva/engage/engage-community-remove.mdx @@ -0,0 +1,52 @@ +import Global from '/docs/cmd/_global.mdx'; + +# viva engage community remove + +Removes a Viva Engage community + +## Usage + +```sh +m365 viva engage community remove [options] +``` + +## Options + +```md definition-list +`-i, --id [id]` +: The id of the community. Specify either `id` or `displayName` but not both. + +`-n, --displayName [displayName]` +: The name of the community. Specify either `id` or `displayName` but not both. + +`-f, --force` +: Don't prompt for confirmation. +``` + + + +## Remarks + +:::info + +When the Viva Engage community is removed, all the associated Microsoft 365 content, including the M365 group, the document library, OneNote notebook, and Planner plans is deleted. + +::: + +## Examples + +Remove a community specified by id without prompting + +```sh +m365 viva engage community remove --id eyJfdHlwZSI6Ikdyb3VwIiwiaWQiOiI0NzY5MTM1ODIwOSJ9 --force +``` + +Remove a community specified by name and prompt for confirmation + +```sh +m365 viva engage community remove --displayName 'Software Engineers' +``` + +## Response + +The command won't return a response on success \ No newline at end of file diff --git a/docs/src/config/sidebars.ts b/docs/src/config/sidebars.ts index 68fa5ec4249..64029223903 100644 --- a/docs/src/config/sidebars.ts +++ b/docs/src/config/sidebars.ts @@ -4562,6 +4562,11 @@ const sidebars: SidebarsConfig = { label: 'engage community list', id: 'cmd/viva/engage/engage-community-list' }, + { + type: 'doc', + label: 'engage community remove', + id: 'cmd/viva/engage/engage-community-remove' + }, { type: 'doc', label: 'engage community set', diff --git a/src/m365/viva/commands.ts b/src/m365/viva/commands.ts index 3061b0d2b6d..de5b0556a58 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_REMOVE: `${prefix} engage community remove`, ENGAGE_COMMUNITY_SET: `${prefix} engage community set`, ENGAGE_COMMUNITY_USER_LIST: `${prefix} engage community user list`, ENGAGE_GROUP_LIST: `${prefix} engage group list`, diff --git a/src/m365/viva/commands/engage/Community.ts b/src/m365/viva/commands/engage/Community.ts index bde8d4838fc..021b8fc0017 100644 --- a/src/m365/viva/commands/engage/Community.ts +++ b/src/m365/viva/commands/engage/Community.ts @@ -4,4 +4,9 @@ export interface Community { description?: string; privacy?: string; groupId?: string; +}export interface Community { + id: string; + displayName: string; + description: string; + privacy: 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 new file mode 100644 index 00000000000..5968308b04a --- /dev/null +++ b/src/m365/viva/commands/engage/engage-community-remove.spec.ts @@ -0,0 +1,141 @@ +import assert from 'assert'; +import sinon from 'sinon'; +import auth from '../../../../Auth.js'; +import commands from '../../commands.js'; +import request from '../../../../request.js'; +import { telemetry } from '../../../../telemetry.js'; +import { Logger } from '../../../../cli/Logger.js'; +import { CommandError } from '../../../../Command.js'; +import { pid } from '../../../../utils/pid.js'; +import { session } from '../../../../utils/session.js'; +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'; + +describe(commands.ENGAGE_COMMUNITY_REMOVE, () => { + const communityId = 'eyJfdHlwZSI6Ikdyb3VwIiwiaWQiOiI0NzY5MTM1ODIwOSJ9'; + const displayName = 'Software Engineers'; + + let log: string[]; + let logger: Logger; + let promptIssued: boolean; + + 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; + }); + + beforeEach(() => { + log = []; + logger = { + log: async (msg: string) => { + log.push(msg); + }, + logRaw: async (msg: string) => { + log.push(msg); + }, + logToStderr: async (msg: string) => { + log.push(msg); + } + }; + sinon.stub(cli, 'promptForConfirmation').callsFake(() => { + promptIssued = true; + return Promise.resolve(false); + }); + + promptIssued = false; + }); + + afterEach(() => { + sinonUtil.restore([ + request.delete, + vivaEngage.getCommunityIdByDisplayName, + cli.promptForConfirmation + ]); + }); + + after(() => { + sinon.restore(); + auth.connection.active = false; + }); + + it('has correct name', () => { + assert.strictEqual(command.name, commands.ENGAGE_COMMUNITY_REMOVE); + }); + + it('has a description', () => { + assert.notStrictEqual(command.description, null); + }); + + it('prompts before removing the community when confirm option not passed', async () => { + await command.action(logger, { options: { id: communityId } }); + + assert(promptIssued); + }); + + it('aborts removing the community when prompt not confirmed', async () => { + const deleteSpy = sinon.stub(request, 'delete').resolves(); + + await command.action(logger, { options: { id: communityId } }); + assert(deleteSpy.notCalled); + }); + + it('removes the community specified by id without prompting for confirmation', async () => { + 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'; + }); + + await command.action(logger, { options: { id: communityId, force: true, verbose: true } }); + assert(deleteRequestStub.called); + }); + + it('removes the community specified by displayName while prompting for confirmation', async () => { + sinon.stub(vivaEngage, 'getCommunityIdByDisplayName').resolves(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: { displayName: displayName } }); + assert(deleteRequestStub.called); + }); + + it('throws an error when the community specified by id cannot be found', async () => { + const error = { + error: { + code: 'notFound', + message: 'Not found.', + innerError: { + date: '2024-08-30T06:25:04', + 'request-id': '186480bb-73a7-4164-8a10-b05f45a94a4f', + 'client-request-id': '186480bb-73a7-4164-8a10-b05f45a94a4f' + } + } + }; + sinon.stub(request, 'delete').callsFake(async (opts) => { + if (opts.url === `https://graph.microsoft.com/v1.0/employeeExperience/communities/${communityId}`) { + throw error; + } + + throw 'Invalid request'; + }); + + await assert.rejects(command.action(logger, { options: { id: communityId, force: true } }), + new CommandError(error.error.message)); + }); +}); \ No newline at end of file diff --git a/src/m365/viva/commands/engage/engage-community-remove.ts b/src/m365/viva/commands/engage/engage-community-remove.ts new file mode 100644 index 00000000000..88a4d0f6532 --- /dev/null +++ b/src/m365/viva/commands/engage/engage-community-remove.ts @@ -0,0 +1,113 @@ +import GlobalOptions from '../../../../GlobalOptions.js'; +import { Logger } from '../../../../cli/Logger.js'; +import { cli } from '../../../../cli/cli.js'; +import request, { CliRequestOptions } from '../../../../request.js'; +import { vivaEngage } from '../../../../utils/vivaEngage.js'; +import GraphCommand from '../../../base/GraphCommand.js'; +import commands from '../../commands.js'; + +interface CommandArgs { + options: Options; +} + +interface Options extends GlobalOptions { + id?: string; + displayName?: string; + force?: boolean +} + +class VivaEngageCommunityRemoveCommand extends GraphCommand { + public get name(): string { + return commands.ENGAGE_COMMUNITY_REMOVE; + } + public get description(): string { + return 'Removes a community'; + } + + constructor() { + super(); + + this.#initTelemetry(); + this.#initOptions(); + this.#initOptionSets(); + this.#initTypes(); + } + + #initTelemetry(): void { + this.telemetry.push((args: CommandArgs) => { + Object.assign(this.telemetryProperties, { + id: args.options.id !== 'undefined', + displayName: args.options.displayName !== 'undefined', + force: !!args.options.force + }); + }); + } + + #initOptions(): void { + this.options.unshift( + { + option: '-i, --id [id]' + }, + { + option: '-n, --displayName [displayName]' + }, + { + option: '-f, --force' + } + ); + } + + #initOptionSets(): void { + this.optionSets.push( + { + options: ['id', 'displayName'] + } + ); + } + + #initTypes(): void { + this.types.string.push('id', 'displayName'); + } + + public async commandAction(logger: Logger, args: CommandArgs): Promise { + + const removeCommunity = async (): Promise => { + try { + let communityId = args.options.id; + + if (args.options.displayName) { + communityId = await vivaEngage.getCommunityIdByDisplayName(args.options.displayName); + } + + if (args.options.verbose) { + await logger.logToStderr(`Removing Viva Engage community with ID ${communityId}...`); + } + + const requestOptions: CliRequestOptions = { + url: `${this.resource}/v1.0/employeeExperience/communities/${communityId}`, + headers: { + accept: 'application/json;odata.metadata=none' + } + }; + + await request.delete(requestOptions); + } + catch (err: any) { + this.handleRejectedODataJsonPromise(err); + } + }; + + if (args.options.force) { + 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}'?` }); + + if (result) { + await removeCommunity(); + } + } + } +} + +export default new VivaEngageCommunityRemoveCommand(); \ No newline at end of file From 5783377251f2988023dae5490c060451d0695929 Mon Sep 17 00:00:00 2001 From: Martin Machacek Date: Fri, 25 Oct 2024 10:11:04 +0200 Subject: [PATCH 2/4] New command: viva engage community remove --- .../engage/engage-community-remove.spec.ts | 35 +++++++++++++++++-- .../engage/engage-community-remove.ts | 30 +++++++++++++--- 2 files changed, 59 insertions(+), 6 deletions(-) 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(); From 5e56abd8ef46579a4c3e8ea3002631d94349c88e Mon Sep 17 00:00:00 2001 From: Martin Machacek Date: Fri, 25 Oct 2024 10:13:32 +0200 Subject: [PATCH 3/4] New command: viva engage community remove --- .../cmd/viva/engage/engage-community-remove.mdx | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/docs/docs/cmd/viva/engage/engage-community-remove.mdx b/docs/docs/cmd/viva/engage/engage-community-remove.mdx index e4f0aeb5b05..80ca38f962e 100644 --- a/docs/docs/cmd/viva/engage/engage-community-remove.mdx +++ b/docs/docs/cmd/viva/engage/engage-community-remove.mdx @@ -14,10 +14,13 @@ m365 viva engage community remove [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. `-n, --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 365 group associated with the community. Specify either `id`, `displayName` or `entraGroupId`, but not multiple. `-f, --force` : Don't prompt for confirmation. @@ -47,6 +50,12 @@ Remove a community specified by name and prompt for confirmation m365 viva engage community remove --displayName 'Software Engineers' ``` +Remove a community specified by Entra group id and prompt for confirmation + +```sh +m365 viva engage community remove --entraGroupId '0bed8b86-5026-4a93-ac7d-56750cc099f1' +``` + ## Response The command won't return a response on success \ No newline at end of file From 324175ecf4c91019b0a86a889bcc785660877784 Mon Sep 17 00:00:00 2001 From: Martin Machacek Date: Sun, 27 Oct 2024 11:43:08 +0100 Subject: [PATCH 4/4] New command: viva engage community remove --- src/m365/viva/commands/engage/Community.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/m365/viva/commands/engage/Community.ts b/src/m365/viva/commands/engage/Community.ts index 021b8fc0017..bde8d4838fc 100644 --- a/src/m365/viva/commands/engage/Community.ts +++ b/src/m365/viva/commands/engage/Community.ts @@ -4,9 +4,4 @@ export interface Community { description?: string; privacy?: string; groupId?: string; -}export interface Community { - id: string; - displayName: string; - description: string; - privacy: string; } \ No newline at end of file