Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New command: viva engage community remove. Closes #6280 #6286

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions docs/docs/cmd/viva/engage/engage-community-remove.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
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`, `displayName` or `entraGroupId`, but not multiple.

`-n, --displayName [displayName]`
: 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.
```

<Global />

## 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'
```

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
5 changes: 5 additions & 0 deletions docs/src/config/sidebars.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
1 change: 1 addition & 0 deletions src/m365/viva/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`,
Expand Down
172 changes: 172 additions & 0 deletions src/m365/viva/commands/engage/engage-community-remove.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
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';
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();
sinon.stub(telemetry, 'trackEvent').returns();
sinon.stub(pid, 'getProcessName').returns('');
sinon.stub(session, 'getId').returns('');
auth.connection.active = true;
commandInfo = cli.getCommandInfo(command);
});

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,
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('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 } });

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, '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}`) {
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('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: {
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));
});
});
135 changes: 135 additions & 0 deletions src/m365/viva/commands/engage/engage-community-remove.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
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';

interface CommandArgs {
options: Options;
}

interface Options extends GlobalOptions {
id?: string;
displayName?: string;
entraGroupId?: 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.#initValidators();
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',
entraGroupId: args.options.entraGroupId !== 'undefined',
force: !!args.options.force
});
});
}

#initOptions(): void {
this.options.unshift(
{
option: '-i, --id [id]'
},
{
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', 'entraGroupId']
}
);
}

#initTypes(): void {
this.types.string.push('id', 'displayName', 'entraGroupId');
}

public async commandAction(logger: Logger, args: CommandArgs): Promise<void> {

const removeCommunity = async (): Promise<void> => {
try {
let communityId = args.options.id;

if (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) {
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 || args.options.entraGroupId }'?` });

if (result) {
await removeCommunity();
}
}
}
}

export default new VivaEngageCommunityRemoveCommand();
Loading