Skip to content

Commit 915a783

Browse files
MathijsVerbeeckmartinlingstuyl
authored andcommitted
Adds command 'viva engage community user remove'. Closes #6296
1 parent 3f16da4 commit 915a783

File tree

5 files changed

+427
-0
lines changed

5 files changed

+427
-0
lines changed
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import Global from '/docs/cmd/_global.mdx';
2+
import Tabs from '@theme/Tabs';
3+
import TabItem from '@theme/TabItem';
4+
5+
# viva engage community user remove
6+
7+
Removes a specified user from a Microsoft 365 Viva Engage community
8+
9+
## Usage
10+
11+
```sh
12+
m365 viva engage community user remove [options]
13+
```
14+
15+
## Options
16+
17+
```md definition-list
18+
`-i, --communityId [communityId]`
19+
: The ID of the Viva Engage community. Specify `communityId`, `communityDisplayName` or `entraGroupId`.
20+
21+
`-n, --communityDisplayName [communityDisplayName]`
22+
: The display name of the Viva Engage community. Specify `communityId`, `communityDisplayName` or `entraGroupId`.
23+
24+
`--entraGroupId [entraGroupId]`
25+
: The ID of the Microsoft 365 group. Specify `communityId`, `communityDisplayName` or `entraGroupId`.
26+
27+
`--id [id]`
28+
: Microsoft Entra ID of the user. Specify either `id` or `userName` but not both.
29+
30+
`--userName [userName]`
31+
: The user principal name of the user. Specify either `id` or `userName` but not both.
32+
33+
`-f, --force`
34+
: Don't prompt for confirming removing the user from the specified Viva Engage community.
35+
```
36+
37+
<Global />
38+
39+
## Examples
40+
41+
Remove a user specified by ID as a member from a community specified by display name.
42+
43+
```sh
44+
m365 viva engage community user remove --communityDisplayName "All company" --id 098b9f52-f48c-4401-819f-29c33794c3f5
45+
```
46+
47+
Remove a user specified by UPN from a community specified by its group ID without confirmation.
48+
49+
```sh
50+
m365 viva engage community user remove --entraGroupId a03c0c35-ef9a-419b-8cab-f89e0a8d2d2a --userName john.doe@contoso.com --force
51+
```
52+
53+
## Response
54+
55+
The command won't return a response on success.

docs/src/config/sidebars.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4715,6 +4715,11 @@ const sidebars: SidebarsConfig = {
47154715
label: 'engage community user list',
47164716
id: 'cmd/viva/engage/engage-community-user-list'
47174717
},
4718+
{
4719+
type: 'doc',
4720+
label: 'engage community user remove',
4721+
id: 'cmd/viva/engage/engage-community-user-remove'
4722+
},
47184723
{
47194724
type: 'doc',
47204725
label: 'engage group list',

src/m365/viva/commands.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export default {
88
ENGAGE_COMMUNITY_SET: `${prefix} engage community set`,
99
ENGAGE_COMMUNITY_USER_ADD: `${prefix} engage community user add`,
1010
ENGAGE_COMMUNITY_USER_LIST: `${prefix} engage community user list`,
11+
ENGAGE_COMMUNITY_USER_REMOVE: `${prefix} engage community user remove`,
1112
ENGAGE_GROUP_LIST: `${prefix} engage group list`,
1213
ENGAGE_GROUP_USER_ADD: `${prefix} engage group user add`,
1314
ENGAGE_GROUP_USER_REMOVE: `${prefix} engage group user remove`,
Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,239 @@
1+
2+
import assert from 'assert';
3+
import sinon from 'sinon';
4+
import auth from '../../../../Auth.js';
5+
import { Logger } from '../../../../cli/Logger.js';
6+
import { CommandError } from '../../../../Command.js';
7+
import request from '../../../../request.js';
8+
import { telemetry } from '../../../../telemetry.js';
9+
import { pid } from '../../../../utils/pid.js';
10+
import { session } from '../../../../utils/session.js';
11+
import { sinonUtil } from '../../../../utils/sinonUtil.js';
12+
import commands from '../../commands.js';
13+
import command from './engage-community-user-remove.js';
14+
import { CommandInfo } from '../../../../cli/CommandInfo.js';
15+
import { z } from 'zod';
16+
import { cli } from '../../../../cli/cli.js';
17+
import { vivaEngage } from '../../../../utils/vivaEngage.js';
18+
import { entraUser } from '../../../../utils/entraUser.js';
19+
20+
describe(commands.ENGAGE_COMMUNITY_USER_REMOVE, () => {
21+
const communityId = 'eyJfdHlwZSI6Ikdyb3VwIiwiaWQiOiIzNjAyMDAxMTAwOSJ9';
22+
const communityDisplayName = 'All company';
23+
const entraGroupId = 'b6c35b51-ebca-445c-885a-63a67d24cb53';
24+
const userName = 'john@contoso.com';
25+
const userId = '3f2504e0-4f89-11d3-9a0c-0305e82c3301';
26+
27+
let log: string[];
28+
let logger: Logger;
29+
let commandInfo: CommandInfo;
30+
let commandOptionsSchema: z.ZodTypeAny;
31+
32+
before(() => {
33+
sinon.stub(auth, 'restoreAuth').resolves();
34+
sinon.stub(telemetry, 'trackEvent').resolves();
35+
sinon.stub(pid, 'getProcessName').returns('');
36+
sinon.stub(session, 'getId').returns('');
37+
auth.connection.active = true;
38+
commandInfo = cli.getCommandInfo(command);
39+
commandOptionsSchema = commandInfo.command.getSchemaToParse()!;
40+
sinon.stub(entraUser, 'getUserIdByUpn').resolves(userId);
41+
sinon.stub(vivaEngage, 'getCommunityByDisplayName').resolves({ groupId: entraGroupId });
42+
sinon.stub(vivaEngage, 'getCommunityById').resolves({ groupId: entraGroupId });
43+
});
44+
45+
beforeEach(() => {
46+
log = [];
47+
logger = {
48+
log: async (msg: string) => {
49+
log.push(msg);
50+
},
51+
logRaw: async (msg: string) => {
52+
log.push(msg);
53+
},
54+
logToStderr: async (msg: string) => {
55+
log.push(msg);
56+
}
57+
};
58+
});
59+
60+
afterEach(() => {
61+
sinonUtil.restore([
62+
request.delete,
63+
cli.promptForConfirmation
64+
]);
65+
});
66+
67+
after(() => {
68+
sinon.restore();
69+
auth.connection.active = false;
70+
});
71+
72+
it('has correct name', () => {
73+
assert.strictEqual(command.name, commands.ENGAGE_COMMUNITY_USER_REMOVE);
74+
});
75+
76+
it('has a description', () => {
77+
assert.notStrictEqual(command.description, null);
78+
});
79+
80+
it('fails validation if entraGroupId is not a valid GUID', () => {
81+
const actual = commandOptionsSchema.safeParse({
82+
entraGroupId: 'invalid',
83+
userName: userName
84+
});
85+
assert.notStrictEqual(actual.success, true);
86+
});
87+
88+
it('fails validation if id is not a valid GUID', () => {
89+
const actual = commandOptionsSchema.safeParse({
90+
entraGroupId: entraGroupId,
91+
id: 'invalid'
92+
});
93+
assert.notStrictEqual(actual.success, true);
94+
});
95+
96+
it('fails validation if userName is invalid user principal name', () => {
97+
const actual = commandOptionsSchema.safeParse({
98+
entraGroupId: entraGroupId,
99+
userName: 'invalid'
100+
});
101+
assert.notStrictEqual(actual.success, true);
102+
});
103+
104+
it('fails validation if communityId, communityDisplayName or entraGroupId are not specified', () => {
105+
const actual = commandOptionsSchema.safeParse({});
106+
assert.notStrictEqual(actual.success, true);
107+
});
108+
109+
it('fails validation if communityId, communityDisplayName and entraGroupId are specified', () => {
110+
const actual = commandOptionsSchema.safeParse({
111+
communityId: communityId,
112+
communityDisplayName: communityDisplayName,
113+
entraGroupId: entraGroupId,
114+
id: userId
115+
});
116+
assert.notStrictEqual(actual.success, true);
117+
});
118+
119+
it('passes validation if communityId is specified', () => {
120+
const actual = commandOptionsSchema.safeParse({
121+
communityId: communityId,
122+
userName: userName
123+
});
124+
assert.strictEqual(actual.success, true);
125+
});
126+
127+
it('passes validation if entraGroupId is specified with a proper GUID', () => {
128+
const actual = commandOptionsSchema.safeParse({
129+
entraGroupId: entraGroupId,
130+
userName: userName
131+
});
132+
assert.strictEqual(actual.success, true);
133+
});
134+
135+
it('passes validation if communityDisplayName is specified', () => {
136+
const actual = commandOptionsSchema.safeParse({
137+
communityDisplayName: communityDisplayName,
138+
userName: userName
139+
});
140+
assert.strictEqual(actual.success, true);
141+
});
142+
143+
it('correctly removes user specified by id', async () => {
144+
const deleteStub = sinon.stub(request, 'delete').callsFake(async (opts) => {
145+
if (opts.url === `https://graph.microsoft.com/v1.0/groups/${entraGroupId}/owners/${userId}/$ref`) {
146+
return;
147+
}
148+
149+
if (opts.url === `https://graph.microsoft.com/v1.0/groups/${entraGroupId}/members/${userId}/$ref`) {
150+
return;
151+
}
152+
153+
throw 'Invalid request';
154+
});
155+
156+
await command.action(logger, { options: { communityDisplayName: communityDisplayName, id: userId, force: true, verbose: true } });
157+
assert(deleteStub.calledTwice);
158+
});
159+
160+
it('correctly removes user by userName', async () => {
161+
const deleteStub = sinon.stub(request, 'delete').callsFake(async (opts) => {
162+
if (opts.url === `https://graph.microsoft.com/v1.0/groups/${entraGroupId}/owners/${userId}/$ref`) {
163+
return;
164+
}
165+
166+
if (opts.url === `https://graph.microsoft.com/v1.0/groups/${entraGroupId}/members/${userId}/$ref`) {
167+
return;
168+
}
169+
throw 'Invalid request';
170+
});
171+
172+
await command.action(logger, { options: { communityId: communityId, verbose: true, userName: userName, force: true } });
173+
assert(deleteStub.calledTwice);
174+
});
175+
176+
it('correctly removes user as member by userName', async () => {
177+
const deleteStub = sinon.stub(request, 'delete').callsFake(async (opts) => {
178+
if (opts.url === `https://graph.microsoft.com/v1.0/groups/${entraGroupId}/owners/${userId}/$ref`) {
179+
throw {
180+
response: {
181+
status: 404,
182+
data: {
183+
message: 'Object does not exist...'
184+
}
185+
}
186+
};
187+
}
188+
189+
if (opts.url === `https://graph.microsoft.com/v1.0/groups/${entraGroupId}/members/${userId}/$ref`) {
190+
return;
191+
}
192+
throw 'Invalid request';
193+
});
194+
195+
sinonUtil.restore(cli.promptForConfirmation);
196+
sinon.stub(cli, 'promptForConfirmation').resolves(true);
197+
198+
await command.action(logger, { options: { communityId: communityId, verbose: true, userName: userName } });
199+
assert(deleteStub.calledTwice);
200+
});
201+
202+
it('handles API error when removing user', async () => {
203+
const errorMessage = 'Invalid object identifier';
204+
sinon.stub(request, 'delete').callsFake(async opts => {
205+
if (opts.url === `https://graph.microsoft.com/v1.0/groups/${entraGroupId}/owners/${userId}/$ref`) {
206+
throw {
207+
response: {
208+
status: 400,
209+
data: { error: { 'odata.error': { message: { value: errorMessage } } } }
210+
}
211+
};
212+
}
213+
214+
throw 'Invalid request';
215+
});
216+
217+
sinonUtil.restore(cli.promptForConfirmation);
218+
sinon.stub(cli, 'promptForConfirmation').resolves(true);
219+
220+
await assert.rejects(command.action(logger, { options: { entraGroupId: entraGroupId, id: userId } }),
221+
new CommandError(errorMessage));
222+
});
223+
224+
it('prompts before removal when confirmation argument not passed', async () => {
225+
const promptStub: sinon.SinonStub = sinon.stub(cli, 'promptForConfirmation').resolves(false);
226+
227+
await command.action(logger, { options: { entraGroupId: entraGroupId, id: userId } });
228+
229+
assert(promptStub.called);
230+
});
231+
232+
it('aborts execution when prompt not confirmed', async () => {
233+
const deleteStub = sinon.stub(request, 'delete');
234+
sinon.stub(cli, 'promptForConfirmation').resolves(false);
235+
236+
await command.action(logger, { options: { entraGroupId: entraGroupId, id: userId } });
237+
assert(deleteStub.notCalled);
238+
});
239+
});

0 commit comments

Comments
 (0)