Skip to content
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
76 changes: 76 additions & 0 deletions docs/docs/cmd/viva/engage/engage-role-member-add.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import Global from '../../_global.mdx';
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';

# viva engage role member add

Assign a Viva Engage role to a user

## Usage

```sh
m365 viva engage role member add [options]
```

## Options

```md definition-list
`-i, --roleId [roleId]`
: The id of the Viva Engage role. Specify either `roleId` or `roleName`, but not both.

`-n, --roleName [roleName]`
: The name of the Viva Engage role. Specify either `roleId` or `roleName`, but not both.

`--userId [userId]`
: The id of the user to be assigned to the role. Specify either `userId` or `userName`, but not both.

`--userName [userName]`
: The UPN of the user to be assigned to the role. Specify either `userId` or `userName`, but not both.
```

<Global />

## Remarks

:::warning

This command is based on a Microsoft Graph API that is currently in preview and is subject to change once the API reached general availability.

:::

## Permissions

<Tabs>
<TabItem value="Delegated">

| Resource | Permissions |
|-----------------|------------------------------|
| Microsoft Graph | EngagementRole.ReadWrite.All |

</TabItem>
<TabItem value="Application">

| Resource | Permissions |
|-----------------|------------------------------|
| Microsoft Graph | EngagementRole.ReadWrite.All |

</TabItem>
</Tabs>

## Examples

Assign a user specified by id to the role specified by name.

```sh
m365 viva engage role member add --userId 7a2ca997-9461-402e-9882-58088a370889 --roleName 'Verified Admin'
```

Assign a user specified by UPN to the role specified by id.

```sh
m365 viva engage role member add --userName john.doe@contoso.com --roleId 77aa47ad-96fe-4ecc-8024-fd1ac5e28f17
```

## 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 @@ -5004,6 +5004,11 @@ const sidebars: SidebarsConfig = {
label: 'engage role list',
id: 'cmd/viva/engage/engage-role-list'
},
{
type: 'doc',
label: 'engage role member add',
id: 'cmd/viva/engage/engage-role-member-add'
},
{
type: 'doc',
label: 'engage role member 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 @@ -26,6 +26,7 @@ export default {
ENGAGE_REPORT_GROUPSACTIVITYDETAIL: `${prefix} engage report groupsactivitydetail`,
ENGAGE_REPORT_GROUPSACTIVITYGROUPCOUNTS: `${prefix} engage report groupsactivitygroupcounts`,
ENGAGE_ROLE_LIST: `${prefix} engage role list`,
ENGAGE_ROLE_MEMBER_ADD: `${prefix} engage role member add`,
ENGAGE_ROLE_MEMBER_LIST: `${prefix} engage role member list`,
ENGAGE_ROLE_MEMBER_REMOVE: `${prefix} engage role member remove`,
ENGAGE_SEARCH: `${prefix} engage search`,
Expand Down
213 changes: 213 additions & 0 deletions src/m365/viva/commands/engage/engage-role-member-add.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
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, { options } from './engage-role-member-add.js';
import { CommandInfo } from '../../../../cli/CommandInfo.js';
import { cli } from '../../../../cli/cli.js';
import { vivaEngage } from '../../../../utils/vivaEngage.js';
import { entraUser } from '../../../../utils/entraUser.js';


describe(commands.ENGAGE_ROLE_MEMBER_ADD, () => {
const roleId = 'ec759127-089f-4f91-8dfc-03a30b51cb38';
const roleName = 'Network Admin';
const userId = 'a1b2c3d4-e5f6-4789-9012-3456789abcde';
const userName = 'john.doe@contoso.com';

let log: string[];
let logger: Logger;
let commandInfo: CommandInfo;
let commandOptionsSchema: typeof options;

before(() => {
sinon.stub(auth, 'restoreAuth').resolves();
sinon.stub(telemetry, 'trackEvent').resolves();
sinon.stub(pid, 'getProcessName').returns('');
sinon.stub(session, 'getId').returns('');
auth.connection.active = true;
commandInfo = cli.getCommandInfo(command);
commandOptionsSchema = commandInfo.command.getSchemaToParse() as typeof options;
});

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.post,
entraUser.getUserIdByUpn,
vivaEngage.getRoleIdByName
]);
});

after(() => {
sinon.restore();
auth.connection.active = false;
});

it('has correct name', () => {
assert.strictEqual(command.name, commands.ENGAGE_ROLE_MEMBER_ADD);
});

it('has a description', () => {
assert.notStrictEqual(command.description, null);
});

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

it('passes validation if roleId is a valid GUID', () => {
const actual = commandOptionsSchema.safeParse({
roleId: roleId,
userId: userId
});
assert.strictEqual(actual.success, true);
});

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

it('fails validation if both roleId and roleName are specified', () => {
const actual = commandOptionsSchema.safeParse({
roleId: roleId,
roleName: roleName,
userId: userId
});
assert.notStrictEqual(actual.success, true);
});

it('fails validation if neither roleId nor roleName is specified', () => {
const actual = commandOptionsSchema.safeParse({
userId: userId
});
assert.notStrictEqual(actual.success, true);
});

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

it('passes validation if userId is a valid GUID', () => {
const actual = commandOptionsSchema.safeParse({
roleId: roleId,
userId: userId
});
assert.strictEqual(actual.success, true);
});

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

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

it('fails validation if both userId and userName are specified', () => {
const actual = commandOptionsSchema.safeParse({
roleId: roleId,
userId: userId,
userName: userName
});
assert.notStrictEqual(actual.success, true);
});

it('fails validation if neither userId nor userName is specified', () => {
const actual = commandOptionsSchema.safeParse({
roleId: roleId
});
assert.notStrictEqual(actual.success, true);
});

it('adds the user specified by id to Viva Engage role specified by id', async () => {
const postStub = sinon.stub(request, 'post').callsFake(async (opts) => {
if (opts.url === `https://graph.microsoft.com/beta/employeeExperience/roles/${roleId}/members`) {
return {
"@odata.type": "#microsoft.graph.engagementRoleMember",
"id": "a40473a5-0fb4-a250-e029-f6fe33d07733",
"userId": userId,
"createdDateTime": "2026-04-15T14:03:00Z"
};
}

throw 'Invalid request';
});

await command.action(logger, { options: commandOptionsSchema.parse({ debug: true, roleId: roleId, userId: userId }) });
assert(postStub.called);
assert.deepStrictEqual(postStub.lastCall.args[0].data, { "user@odata.bind": `https://graph.microsoft.com/beta/users('${userId}')` });
});

it('adds the user specified by name to Viva Engage role specified by name', async () => {
sinon.stub(entraUser, 'getUserIdByUpn').withArgs(userName).resolves(userId);
sinon.stub(vivaEngage, 'getRoleIdByName').withArgs(roleName).resolves(roleId);

const postStub = sinon.stub(request, 'post').callsFake(async (opts) => {
if (opts.url === `https://graph.microsoft.com/beta/employeeExperience/roles/${roleId}/members`) {
return {
"@odata.type": "#microsoft.graph.engagementRoleMember",
"id": "a40473a5-0fb4-a250-e029-f6fe33d07733",
"userId": userId,
"createdDateTime": "2026-04-15T14:03:00Z"
};
}

throw 'Invalid request';
});

await command.action(logger, { options: commandOptionsSchema.parse({ debug: true, roleName: roleName, userName: userName }) });
assert(postStub.called);
assert.deepStrictEqual(postStub.lastCall.args[0].data, { "user@odata.bind": `https://graph.microsoft.com/beta/users('${userId}')` });
});

it('handles error when adding a user to a Viva Engage role failed', async () => {
sinon.stub(request, 'post').rejects({ error: { message: 'An error has occurred' } });

await assert.rejects(
command.action(logger, { options: commandOptionsSchema.parse({ roleId: roleId, userId: userId }) }),
new CommandError('An error has occurred')
);
});
});
Loading
Loading