Skip to content

Commit

Permalink
Removes M365 group and connected site. Closes #5224
Browse files Browse the repository at this point in the history
  • Loading branch information
Saurabh7019 committed Sep 6, 2023
1 parent 515b009 commit d234b7e
Show file tree
Hide file tree
Showing 3 changed files with 250 additions and 143 deletions.
2 changes: 1 addition & 1 deletion docs/docs/cmd/aad/m365group/m365group-remove.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ m365 aad m365group remove [options]

## Remarks

If the specified _id_ doesn't refer to an existing group, you will get a `Resource does not exist` error.
If the specified _id_ doesn't refer to an existing group, you will get a `Resource does not exist` error. Additionally, if you do not have access to the group or the associated group-connected site, you will get an `Access denied` error.

## Examples

Expand Down
239 changes: 117 additions & 122 deletions src/m365/aad/commands/m365group/m365group-remove.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,21 +11,70 @@ import { pid } from '../../../../utils/pid.js';
import { session } from '../../../../utils/session.js';
import { sinonUtil } from '../../../../utils/sinonUtil.js';
import commands from '../../commands.js';
import { spo } from '../../../../utils/spo.js';
import command from './m365group-remove.js';

describe(commands.M365GROUP_REMOVE, () => {
let log: string[];
let logger: Logger;
let loggerLogSpy: sinon.SinonSpy;
let loggerLogToStderrSpy: sinon.SinonSpy;
let commandInfo: CommandInfo;
let promptOptions: any;

const groupId = "3e6e705d-6fb5-4ca7-84dc-3c8f5154fe2c";

const defaultGetStub = (): sinon.SinonStub => {
return sinon.stub(request, 'get').callsFake(async (opts) => {
if (opts.url === `https://graph.microsoft.com/v1.0/groups/${groupId}/drive?$select=webUrl`) {
return { webUrl: "https://contoso.sharepoint.com/teams/sales/Shared%20Documents" };
}

if (opts.url === `https://graph.microsoft.com/v1.0/directory/deletedItems/${groupId}`) {
return { id: groupId };
}

throw 'Invalid request';
});
};

const defaultPostStub = (): sinon.SinonStub => {
return sinon.stub(request, 'post').callsFake(async (opts) => {
if (opts.url === `https://contoso-admin.sharepoint.com/_api/GroupSiteManager/Delete?siteUrl='https://contoso.sharepoint.com/teams/sales'`) {
return Promise.resolve({
"data": {
"odata.null": true
}
});
}

if ((opts.url as string).indexOf('/_vti_bin/client.svc/ProcessQuery') > -1) {
return JSON.stringify([{ "SchemaVersion": "15.0.0.0", "LibraryVersion": "16.0.7324.1200", "ErrorInfo": null, "TraceCorrelationId": "e13c489e-304e-5000-8242-705e26a87302" }, 185, { "IsNull": false }]);
}

throw 'Invalid request';
});
};

const defaultDeleteStub = (): sinon.SinonStub => {
return sinon.stub(request, 'delete').callsFake(async (opts) => {
if (opts.url === `https://graph.microsoft.com/v1.0/directory/deletedItems/${groupId}`) {
return;
}
throw 'Invalid request';
});
};

before(() => {
sinon.stub(auth, 'restoreAuth').resolves();
sinon.stub(telemetry, 'trackEvent').returns();
sinon.stub(pid, 'getProcessName').returns('');
sinon.stub(session, 'getId').returns('');
sinon.stub(global, 'setTimeout').callsFake((fn) => {
fn();
return {} as any;
});
auth.service.connected = true;
auth.service.spoUrl = 'https://contoso.sharepoint.com';
commandInfo = Cli.getCommandInfo(command);
});

Expand All @@ -42,25 +91,33 @@ describe(commands.M365GROUP_REMOVE, () => {
log.push(msg);
}
};
sinon.stub(spo, 'getSpoAdminUrl').callsFake(() => Promise.resolve('https://contoso-admin.sharepoint.com'));
const futureDate = new Date();
futureDate.setSeconds(futureDate.getSeconds() + 1800);
sinon.stub(spo, 'ensureFormDigest').callsFake(() => { return Promise.resolve({ FormDigestValue: 'abc', FormDigestTimeoutSeconds: 1800, FormDigestExpiresAt: futureDate, WebFullUrl: 'https://contoso.sharepoint.com/sites/hr' }); });
sinon.stub(Cli, 'prompt').callsFake(async (options: any) => {
promptOptions = options;
return { continue: false };
});
loggerLogSpy = sinon.spy(logger, 'log');
loggerLogToStderrSpy = sinon.spy(logger, 'logToStderr');
promptOptions = undefined;
});

afterEach(() => {
sinonUtil.restore([
request.get,
request.post,
request.delete,
global.setTimeout,
spo.getSpoAdminUrl,
spo.ensureFormDigest,
Cli.prompt
]);
});

after(() => {
sinon.restore();
auth.service.connected = false;
auth.service.spoUrl = undefined;
});

it('has correct name', () => {
Expand All @@ -71,30 +128,14 @@ describe(commands.M365GROUP_REMOVE, () => {
assert.notStrictEqual(command.description, null);
});

it('removes the specified group without prompting for confirmation when confirm option specified', async () => {
sinon.stub(request, 'delete').callsFake(async (opts) => {
if (opts.url === 'https://graph.microsoft.com/v1.0/groups/28beab62-7540-4db1-a23f-29a6018a3848') {
return;
}

throw 'Invalid request';
});

await command.action(logger, { options: { id: '28beab62-7540-4db1-a23f-29a6018a3848', force: false } });
assert(loggerLogSpy.notCalled);
it('fails validation if the id is not a valid GUID', async () => {
const actual = await command.validate({ options: { id: 'abc' } }, commandInfo);
assert.notStrictEqual(actual, true);
});

it('removes the specified group without prompting for confirmation when confirm option specified (debug)', async () => {
sinon.stub(request, 'delete').callsFake(async (opts) => {
if (opts.url === 'https://graph.microsoft.com/v1.0/groups/28beab62-7540-4db1-a23f-29a6018a3848') {
return;
}

throw 'Invalid request';
});

await command.action(logger, { options: { debug: true, id: '28beab62-7540-4db1-a23f-29a6018a3848', force: false } });
assert(loggerLogSpy.notCalled);
it('passes validation when the id is a valid GUID', async () => {
const actual = await command.validate({ options: { id: '2c1ba4c4-cd9b-4417-832f-92a34bc34b2a' } }, commandInfo);
assert.strictEqual(actual, true);
});

it('prompts before removing the specified group when confirm option not passed', async () => {
Expand All @@ -108,130 +149,84 @@ describe(commands.M365GROUP_REMOVE, () => {
assert(promptIssued);
});

it('prompts before removing the specified group when confirm option not passed (debug)', async () => {
await command.action(logger, { options: { debug: true, id: '28beab62-7540-4db1-a23f-29a6018a3848' } });
let promptIssued = false;

if (promptOptions && promptOptions.type === 'confirm') {
promptIssued = true;
}

assert(promptIssued);
});

it('aborts removing the group when prompt not confirmed', async () => {
const postSpy = sinon.spy(request, 'delete');
sinonUtil.restore(Cli.prompt);
sinon.stub(Cli, 'prompt').resolves({ continue: false });
const getGroupSpy: sinon.SinonStub = defaultGetStub();

await command.action(logger, { options: { id: '28beab62-7540-4db1-a23f-29a6018a3848' } });
assert(postSpy.notCalled);
assert(getGroupSpy.notCalled);
});

it('aborts removing the group when prompt not confirmed (debug)', async () => {
const postSpy = sinon.spy(request, 'delete');
sinonUtil.restore(Cli.prompt);
sinon.stub(Cli, 'prompt').resolves({ continue: false });

await command.action(logger, { options: { debug: true, id: '28beab62-7540-4db1-a23f-29a6018a3848' } });
assert(postSpy.notCalled);
});
it('deletes the group site for the sepcified group id when prompt confirmed', async () => {
defaultGetStub();
const deletedGroupSpy: sinon.SinonStub = defaultPostStub();

it('removes the group when prompt confirmed', async () => {
const postStub = sinon.stub(request, 'delete').resolves();
sinonUtil.restore(Cli.prompt);
sinon.stub(Cli, 'prompt').callsFake(async () => (
{ continue: true }
));
await command.action(logger, { options: { id: '28beab62-7540-4db1-a23f-29a6018a3848' } });
assert(postStub.called);

await command.action(logger, { options: { id: groupId, verbose: true } });
assert(deletedGroupSpy.calledOnce);
assert(loggerLogToStderrSpy.calledWith(`Deleting the group site: 'https://contoso.sharepoint.com/teams/sales'...`));
});

it('removes the group when prompt confirmed (debug)', async () => {
const postStub = sinon.stub(request, 'delete').resolves();
sinonUtil.restore(Cli.prompt);
sinon.stub(Cli, 'prompt').resolves({ continue: true });
it('deletes the group without moving it to the Recycle Bin', async () => {
defaultGetStub();
defaultPostStub();
const deleteStub: sinon.SinonStub = defaultDeleteStub();

await command.action(logger, { options: { debug: true, id: '28beab62-7540-4db1-a23f-29a6018a3848' } });
assert(postStub.called);
await command.action(logger, { options: { id: groupId, verbose: true, skipRecycleBin: true, force: true } });
assert(deleteStub.called);
assert(loggerLogToStderrSpy.calledWith("Group has been deleted and is now available in the deleted items list. Removing permanently..."));
});

it('removes the group permanently when prompt confirmed', async () => {
let groupPermDeleteCallIssued = false;
sinon.stub(request, 'delete').callsFake(async (opts) => {
if (opts.url === `https://graph.microsoft.com/v1.0/groups/28beab62-7540-4db1-a23f-29a6018a3848`) {
return;
}
it('verifies if the group is deleted and available in the deleted groups list, retry and delete the group', async () => {
const getCallStub: sinon.SinonStub = sinon.stub(request, 'get');

if (opts.url === `https://graph.microsoft.com/v1.0/directory/deletedItems/28beab62-7540-4db1-a23f-29a6018a3848`) {
groupPermDeleteCallIssued = true;
return;
}
getCallStub.withArgs(sinon.match({ url: `https://graph.microsoft.com/v1.0/groups/${groupId}/drive?$select=webUrl` }))
.resolves({ webUrl: "https://contoso.sharepoint.com/teams/sales/Shared%20Documents" });

throw 'Invalid request';
});
sinonUtil.restore(Cli.prompt);
sinon.stub(Cli, 'prompt').resolves({ continue: true });
getCallStub.withArgs(sinon.match({ url: `https://graph.microsoft.com/v1.0/directory/deletedItems/${groupId}` }))
.onFirstCall().rejects({ response: { status: 404 } })
.onSecondCall().resolves({ id: groupId });

await command.action(logger, { options: { debug: true, id: '28beab62-7540-4db1-a23f-29a6018a3848', skipRecycleBin: true } });
assert(groupPermDeleteCallIssued);
});
defaultPostStub();
const deleteStub: sinon.SinonStub = defaultDeleteStub();

it('removes the group permanently when with confirm option', async () => {
let groupPermDeleteCallIssued = false;
sinon.stub(request, 'delete').callsFake(async (opts) => {
if (opts.url === `https://graph.microsoft.com/v1.0/groups/28beab62-7540-4db1-a23f-29a6018a3848`) {
return;
}
await command.action(logger, { options: { id: groupId, verbose: true, skipRecycleBin: true, force: true } });
assert(loggerLogToStderrSpy.calledWith("Group has not been deleted yet. Waiting and retrying..."));
assert(deleteStub.called);
});

if (opts.url === `https://graph.microsoft.com/v1.0/directory/deletedItems/28beab62-7540-4db1-a23f-29a6018a3848`) {
groupPermDeleteCallIssued = true;
return;
}
it('handles error if unexpected error occurs while finding the group in the deleted groups list', async () => {
const getCallStub: sinon.SinonStub = sinon.stub(request, 'get');

throw 'Invalid request';
});
getCallStub.withArgs(sinon.match({ url: `https://graph.microsoft.com/v1.0/groups/${groupId}/drive?$select=webUrl` }))
.resolves({ webUrl: "https://contoso.sharepoint.com/teams/sales/Shared%20Documents" });

await command.action(logger, { options: { debug: true, id: '28beab62-7540-4db1-a23f-29a6018a3848', skipRecycleBin: true, force: true } });
assert(groupPermDeleteCallIssued);
});
getCallStub.withArgs(sinon.match({ url: `https://graph.microsoft.com/v1.0/directory/deletedItems/${groupId}` }))
.rejects();

it('correctly handles error when group is not found', async () => {
sinon.stub(request, 'delete').rejects({ error: { 'odata.error': { message: { value: 'File Not Found.' } } } });
defaultPostStub();
defaultDeleteStub();

await assert.rejects(command.action(logger, { options: { force: true, id: '28beab62-7540-4db1-a23f-29a6018a3848' } } as any),
new CommandError('File Not Found.'));
await assert.rejects(command.action(logger, { options: { id: groupId, verbose: true, skipRecycleBin: true, force: true } }),
new CommandError('Error'));
});

it('supports specifying id', () => {
const options = command.options;
let containsOption = false;
options.forEach(o => {
if (o.option.indexOf('--id') > -1) {
containsOption = true;
}
});
assert(containsOption);
});
it('handles group not found after all retries', async () => {
const getCallStub: sinon.SinonStub = sinon.stub(request, 'get');

it('supports specifying confirmation flag', () => {
const options = command.options;
let containsOption = false;
options.forEach(o => {
if (o.option.indexOf('--force') > -1) {
containsOption = true;
}
});
assert(containsOption);
});
getCallStub.withArgs(sinon.match({ url: `https://graph.microsoft.com/v1.0/groups/${groupId}/drive?$select=webUrl` }))
.resolves({ webUrl: "https://contoso.sharepoint.com/teams/sales/Shared%20Documents" });

it('fails validation if the id is not a valid GUID', async () => {
const actual = await command.validate({ options: { id: 'abc' } }, commandInfo);
assert.notStrictEqual(actual, true);
});
getCallStub.withArgs(sinon.match({ url: `https://graph.microsoft.com/v1.0/directory/deletedItems/${groupId}` }))
.rejects({ response: { status: 404 } });

it('passes validation when the id is a valid GUID', async () => {
const actual = await command.validate({ options: { id: '2c1ba4c4-cd9b-4417-832f-92a34bc34b2a' } }, commandInfo);
assert.strictEqual(actual, true);
defaultPostStub();
const deleteStub: sinon.SinonStub = defaultDeleteStub();

await command.action(logger, { options: { id: groupId, verbose: true, skipRecycleBin: true, force: true } });
assert(deleteStub.notCalled);
});
});
});
Loading

0 comments on commit d234b7e

Please sign in to comment.