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

Adds command viva engage community user remove. Closes #6296 #6389

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
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
55 changes: 55 additions & 0 deletions docs/docs/cmd/viva/engage/engage-community-user-remove.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import Global from '/docs/cmd/_global.mdx';
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';

# viva engage community user remove

Removes a specified user from a Microsoft 365 Viva Engage community

## Usage

```sh
m365 viva engage community user remove [options]
```

## Options

```md definition-list
`-i, --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`.

`--id [id]`
: Microsoft Entra ID of the user. Specify either `id` or `userName` but not both.

`--userName [userName]`
: The user principal name of the user. Specify either `id` or `userName` but not both.

`-f, --force`
: Don't prompt for confirming removing the user from the specified Viva Engage community.
```

<Global />

## Examples

Remove a user specified by ID as a member from a community specified by display name.

```sh
m365 viva engage community user remove --communityDisplayName "All company" --id 098b9f52-f48c-4401-819f-29c33794c3f5
```

Remove a user specified by UPN from a community specified by its group ID without confirmation.

```sh
m365 viva engage community user remove --entraGroupId a03c0c35-ef9a-419b-8cab-f89e0a8d2d2a --userName [email protected] --force
```

## 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 @@ -4475,6 +4475,11 @@ const sidebars: SidebarsConfig = {
label: 'engage community list',
id: 'cmd/viva/engage/engage-community-list'
},
{
type: 'doc',
label: 'engage community user remove',
id: 'cmd/viva/engage/engage-community-user-remove'
},
{
type: 'doc',
label: 'engage group list',
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_USER_REMOVE: `${prefix} engage community user remove`,
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`,
Expand Down
1 change: 1 addition & 0 deletions src/m365/viva/commands/engage/Community.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ export interface Community {
displayName: string;
description?: string;
privacy: string;
groupId: string;
}
239 changes: 239 additions & 0 deletions src/m365/viva/commands/engage/engage-community-user-remove.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@

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-remove.js';
import { CommandInfo } from '../../../../cli/CommandInfo.js';
import { z } from 'zod';
import { cli } from '../../../../cli/cli.js';
import { vivaEngage } from '../../../../utils/vivaEngage.js';
import { entraUser } from '../../../../utils/entraUser.js';

describe(commands.ENGAGE_COMMUNITY_USER_REMOVE, () => {
const communityId = 'eyJfdHlwZSI6Ikdyb3VwIiwiaWQiOiIzNjAyMDAxMTAwOSJ9';
const communityDisplayName = 'All company';
const entraGroupId = 'b6c35b51-ebca-445c-885a-63a67d24cb53';
const userName = '[email protected]';
const userId = '3f2504e0-4f89-11d3-9a0c-0305e82c3301';

let log: string[];
let logger: Logger;
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()!;
sinon.stub(entraUser, 'getUserIdByUpn').resolves(userId);
sinon.stub(vivaEngage, 'getEntraGroupIdByCommunityDisplayName').resolves(entraGroupId);
sinon.stub(vivaEngage, 'getEntraGroupIdByCommunityId').resolves(entraGroupId);
});

beforeEach(() => {
log = [];
logger = {
log: async (msg: string) => {
log.push(msg);
},
logRaw: async (msg: string) => {
log.push(msg);
},
logToStderr: async (msg: string) => {
log.push(msg);
}
};
});

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_USER_REMOVE);
});

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',
userName: userName
});
assert.notStrictEqual(actual.success, true);
});

it('fails validation if id is not a valid GUID', () => {
const actual = commandOptionsSchema.safeParse({
entraGroupId: entraGroupId,
id: 'invalid'
});
assert.notStrictEqual(actual.success, true);
});

it('fails validation if userName is invalid user principal name', () => {
const actual = commandOptionsSchema.safeParse({
entraGroupId: entraGroupId,
userName: '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,
id: userId
});
assert.notStrictEqual(actual.success, true);
});

it('passes validation if communityId is specified', () => {
const actual = commandOptionsSchema.safeParse({
communityId: communityId,
userName: userName
});
assert.strictEqual(actual.success, true);
});

it('passes validation if entraGroupId is specified with a proper GUID', () => {
const actual = commandOptionsSchema.safeParse({
entraGroupId: entraGroupId,
userName: userName
});
assert.strictEqual(actual.success, true);
});

it('passes validation if communityDisplayName is specified', () => {
const actual = commandOptionsSchema.safeParse({
communityDisplayName: communityDisplayName,
userName: userName
});
assert.strictEqual(actual.success, true);
});

it('correctly removes user specified by id', async () => {
const deleteStub = sinon.stub(request, 'delete').callsFake(async (opts) => {
if (opts.url === `https://graph.microsoft.com/v1.0/groups/${entraGroupId}/owners/${userId}/$ref`) {
return;
}

if (opts.url === `https://graph.microsoft.com/v1.0/groups/${entraGroupId}/members/${userId}/$ref`) {
return;
}

throw 'Invalid request';
});

await command.action(logger, { options: { communityDisplayName: communityDisplayName, id: userId, force: true, verbose: true } });
assert(deleteStub.calledTwice);
});

it('correctly removes user by userName', async () => {
const deleteStub = sinon.stub(request, 'delete').callsFake(async (opts) => {
if (opts.url === `https://graph.microsoft.com/v1.0/groups/${entraGroupId}/owners/${userId}/$ref`) {
return;
}

if (opts.url === `https://graph.microsoft.com/v1.0/groups/${entraGroupId}/members/${userId}/$ref`) {
return;
}
throw 'Invalid request';
});

await command.action(logger, { options: { communityId: communityId, verbose: true, userName: userName, force: true } });
assert(deleteStub.calledTwice);
});

it('correctly removes user as member by userName', async () => {
const deleteStub = sinon.stub(request, 'delete').callsFake(async (opts) => {
if (opts.url === `https://graph.microsoft.com/v1.0/groups/${entraGroupId}/owners/${userId}/$ref`) {
throw {
response: {
status: 404,
data: {
message: 'Object does not exist...'
}
}
};
}

if (opts.url === `https://graph.microsoft.com/v1.0/groups/${entraGroupId}/members/${userId}/$ref`) {
return;
}
throw 'Invalid request';
});

sinonUtil.restore(cli.promptForConfirmation);
sinon.stub(cli, 'promptForConfirmation').resolves(true);

await command.action(logger, { options: { communityId: communityId, verbose: true, userName: userName } });
assert(deleteStub.calledTwice);
});

it('handles API error when removing user', async () => {
const errorMessage = 'Invalid object identifier';
sinon.stub(request, 'delete').callsFake(async opts => {
if (opts.url === `https://graph.microsoft.com/v1.0/groups/${entraGroupId}/owners/${userId}/$ref`) {
throw {
response: {
status: 400,
data: { error: { 'odata.error': { message: { value: errorMessage } } } }
}
};
}

throw 'Invalid request';
});

sinonUtil.restore(cli.promptForConfirmation);
sinon.stub(cli, 'promptForConfirmation').resolves(true);

await assert.rejects(command.action(logger, { options: { entraGroupId: entraGroupId, id: userId } }),
new CommandError(errorMessage));
});

it('prompts before removal when confirmation argument not passed', async () => {
const promptStub: sinon.SinonStub = sinon.stub(cli, 'promptForConfirmation').resolves(false);

await command.action(logger, { options: { entraGroupId: entraGroupId, id: userId } });

assert(promptStub.called);
});

it('aborts execution when prompt not confirmed', async () => {
const deleteStub = sinon.stub(request, 'delete');
sinon.stub(cli, 'promptForConfirmation').resolves(false);

await command.action(logger, { options: { entraGroupId: entraGroupId, id: userId } });
assert(deleteStub.notCalled);
});
});
Loading