Skip to content

Commit

Permalink
Adds spo group set command. Closes #3485
Browse files Browse the repository at this point in the history
  • Loading branch information
milanholemans authored and Adam-it committed Jul 9, 2022
1 parent 64e5635 commit 7851c9e
Show file tree
Hide file tree
Showing 5 changed files with 587 additions and 0 deletions.
69 changes: 69 additions & 0 deletions docs/docs/cmd/spo/group/group-set.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# spo group set

Updates a group in the specified site

## Usage

```sh
m365 spo group set [options]
```

## Options

`-u, --webUrl <webUrl>`
: URL of the site where the group is located.

`-i, --id [id]`
: ID of the group to update. Specify either `id` or `name` but not both.

`-n, --name [name]`
: Name of the group. Specify either `id` or `name` but not both.

`--newName [newName]`
: New name for the group.

`--description [description]`
: The description for the group.

`--allowMembersEditMembership [allowMembersEditMembership]`
: Who can edit the membership of the group? When `true` members can edit membership, otherwise only owners can do this.

`--onlyAllowMembersViewMembership [onlyAllowMembersViewMembership]`
: Who can view the membership of the group? When `true` only group members can view the membership, otherwise everyone can.

`--allowRequestToJoinLeave [allowRequestToJoinLeave]`
: Specify whether to allow users to request membership in this group and allow users to request to leave the group.

`--autoAcceptRequestToJoinLeave [autoAcceptRequestToJoinLeave]`
: If auto-accept is enabled, users will automatically be added or removed when they make a request.

`--requestToJoinLeaveEmailSetting [requestToJoinLeaveEmailSetting]`
: All membership requests will be sent to the email address specified.

`--ownerEmail [ownerEmail]`
: Set user with this email as owner of the group. Specify either `ownerEmail` or `ownerUserName` but not both.

`--ownerUserName [ownerUserName]`
: Set user with this login name as owner of the group. Specify either `ownerEmail` or `ownerUserName` but not both.

--8<-- "docs/cmd/_global.md"

## Examples

Update group title and description

```sh
m365 spo group set --webUrl https://contoso.sharepoint.com/sites/project-x --id 18 --newTitle "Project leaders" --description "This group contains all project leaders"
```

Update group with membership requests

```sh
m365 spo group set --webUrl https://contoso.sharepoint.com/sites/project-x --title "Project leaders" --allowRequestToJoinLeave true --requestToJoinLeaveEmailSetting [email protected]
```

Sets a specified user as group owner

```sh
m365 spo group set --webUrl https://contoso.sharepoint.com/sites/project-x --id 18 --ownerEmail [email protected]
```
1 change: 1 addition & 0 deletions docs/mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,7 @@ nav:
- group get: 'cmd/spo/group/group-get.md'
- group list: 'cmd/spo/group/group-list.md'
- group remove: 'cmd/spo/group/group-remove.md'
- group set: 'cmd/spo/group/group-set.md'
- group user add: 'cmd/spo/group/group-user-add.md'
- group user list: 'cmd/spo/group/group-user-list.md'
- group user remove: 'cmd/spo/group/group-user-remove.md'
Expand Down
1 change: 1 addition & 0 deletions src/m365/spo/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ export default {
GROUP_GET: `${prefix} group get`,
GROUP_LIST: `${prefix} group list`,
GROUP_REMOVE: `${prefix} group remove`,
GROUP_SET: `${prefix} group set`,
GROUP_USER_ADD: `${prefix} group user add`,
GROUP_USER_LIST: `${prefix} group user list`,
GROUP_USER_REMOVE: `${prefix} group user remove`,
Expand Down
308 changes: 308 additions & 0 deletions src/m365/spo/commands/group/group-set.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,308 @@
import * as assert from 'assert';
import * as sinon from 'sinon';
import appInsights from '../../../../appInsights';
import auth from '../../../../Auth';
import { Cli, Logger } from '../../../../cli';
import Command, { CommandError } from '../../../../Command';
import request from '../../../../request';
import { sinonUtil } from '../../../../utils';
import commands from '../../commands';
const command: Command = require('./group-set');

const validId = 1;
const validName = "Project leaders";
const validWebUrl = 'https://contoso.sharepoint.com/sites/project-x';
const validOwnerEmail = '[email protected]';
const validOwnerUserName = '[email protected]';

const userInfoResponse = {
userPrincipalName: validOwnerUserName
};

const ensureUserResponse = {
Id: 3
};

describe(commands.GROUP_SET, () => {
let log: string[];
let logger: Logger;

before(() => {
sinon.stub(auth, 'restoreAuth').callsFake(() => Promise.resolve());
sinon.stub(appInsights, 'trackEvent').callsFake(() => { });
auth.service.connected = true;
});

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

afterEach(() => {
sinonUtil.restore([
request.post,
request.patch,
Cli.executeCommandWithOutput
]);
});

after(() => {
sinonUtil.restore([
auth.restoreAuth,
appInsights.trackEvent
]);
auth.service.connected = false;
});

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

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

it('defines correct option sets', () => {
const optionSets = command.optionSets();
assert.deepStrictEqual(optionSets, [['id', 'name']]);
});

it('fails validation when group id is not a number', (done) => {
const actual = command.validate({
options: {
webUrl: validWebUrl,
id: 'invalid id'
}
});
assert.notStrictEqual(actual, true);
done();
});

it('fails validation when both ownerEmail and ownerUserName are specified', (done) => {
const actual = command.validate({
options: {
webUrl: validWebUrl,
ownerEmail: validOwnerEmail,
ownerUserName: validOwnerUserName
}
});
assert.notStrictEqual(actual, true);
done();
});

it('fails validation when invalid boolean is passed as option', (done) => {
const actual = command.validate({
options: {
webUrl: validWebUrl,
id: validId,
autoAcceptRequestToJoinLeave: 'invalid'
}
});
assert.notStrictEqual(actual, true);
done();
});

it('fails validation when invalid web URL is passed', (done) => {
const actual = command.validate({
options: {
webUrl: 'invalid'
}
});
assert.notStrictEqual(actual, true);
done();
});

it('passes validation when valid options specified', (done) => {
const actual = command.validate({
options: {
webUrl: validWebUrl,
id: validId,
allowRequestToJoinLeave: 'true'
}
});
assert.strictEqual(actual, true);
done();
});

it('successfully updates group settings by id', (done) => {
sinon.stub(request, 'patch').callsFake((opts) => {
if (opts.url === `${validWebUrl}/_api/web/sitegroups/GetById(${validId})`) {
return Promise.resolve();
}

return Promise.reject('Invalid Request');
});

command.action(logger, {
options: {
webUrl: validWebUrl,
id: validId,
allowRequestToJoinLeave: 'true'
}
}, (err?: any) => {
try {
assert.strictEqual(typeof err, 'undefined', err?.message);
done();
}
catch (e) {
done(e);
}
});
});

it('successfully updates group settings by name', (done) => {
sinon.stub(request, 'patch').callsFake((opts) => {
if (opts.url === `${validWebUrl}/_api/web/sitegroups/GetByName('${validName}')`) {
return Promise.resolve();
}

return Promise.reject('Invalid Request');
});

command.action(logger, {
options: {
webUrl: validWebUrl,
name: validName,
allowRequestToJoinLeave: 'true'
}
}, (err?: any) => {
try {
assert.strictEqual(typeof err, 'undefined', err?.message);
done();
}
catch (e) {
done(e);
}
});
});

it('successfully updates group owner by ownerEmail', (done) => {
sinon.stub(Cli, 'executeCommandWithOutput').callsFake(() => Promise.resolve({
stdout: JSON.stringify(userInfoResponse),
stderr: ''
}));
sinon.stub(request, 'patch').callsFake((opts) => {
if (opts.url === `${validWebUrl}/_api/web/sitegroups/GetById(${validId})`) {
return Promise.resolve();
}

return Promise.reject('Invalid Request');
});
sinon.stub(request, 'post').callsFake((opts) => {
if (opts.url === `${validWebUrl}/_api/web/ensureUser('${userInfoResponse.userPrincipalName}')?$select=Id`) {
return Promise.resolve(ensureUserResponse);
}

if (opts.url === `${validWebUrl}/_api/web/sitegroups/GetById(${validId})/SetUserAsOwner(${ensureUserResponse.Id})`) {
return Promise.resolve();
}

return Promise.reject('Invalid Request');
});

command.action(logger, {
options: {
webUrl: validWebUrl,
id: validId,
ownerEmail: validOwnerEmail
}
}, (err?: any) => {
try {
assert.strictEqual(typeof err, 'undefined', err?.message);
done();
}
catch (e) {
done(e);
}
});
});

it('successfully updates group owner by ownerEmail', (done) => {
sinon.stub(Cli, 'executeCommandWithOutput').callsFake(() => Promise.resolve({
stdout: JSON.stringify(userInfoResponse),
stderr: ''
}));
sinon.stub(request, 'patch').callsFake((opts) => {
if (opts.url === `${validWebUrl}/_api/web/sitegroups/GetByName('${validName}')`) {
return Promise.resolve();
}

return Promise.reject('Invalid Request');
});
sinon.stub(request, 'post').callsFake((opts) => {
if (opts.url === `${validWebUrl}/_api/web/ensureUser('${userInfoResponse.userPrincipalName}')?$select=Id`) {
return Promise.resolve(ensureUserResponse);
}

if (opts.url === `${validWebUrl}/_api/web/sitegroups/GetByName('${validName}')/SetUserAsOwner(${ensureUserResponse.Id})`) {
return Promise.resolve();
}

return Promise.reject('Invalid Request');
});

command.action(logger, {
options: {
webUrl: validWebUrl,
name: validName,
ownerUserName: validOwnerUserName
}
}, (err?: any) => {
try {
assert.strictEqual(typeof err, 'undefined', err?.message);
done();
}
catch (e) {
done(e);
}
});
});

it('correctly handles random API error', (done) => {
sinon.stub(request, 'patch').callsFake((opts) => {
if (opts.url === `${validWebUrl}/_api/web/sitegroups/GetByName('${validName}')`) {
return Promise.reject('An error has occurred');
}

return Promise.reject('Invalid Request');
});

command.action(logger, {
options: {
webUrl: validWebUrl,
name: validName,
autoAcceptRequestToJoinLeave: 'true'
}
}, (err?: any) => {
try {
assert.strictEqual(JSON.stringify(err), JSON.stringify(new CommandError('An error has occurred')));
done();
}
catch (e) {
done(e);
}
});
});

it('supports debug mode', () => {
const options = command.options();
let containsOption = false;
options.forEach(o => {
if (o.option === '--debug') {
containsOption = true;
}
});
assert(containsOption);
});
});
Loading

0 comments on commit 7851c9e

Please sign in to comment.