Skip to content
Open
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
18 changes: 18 additions & 0 deletions .devproxy/api-specs/sharepoint.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,24 @@ paths:
responses:
200:
description: OK
/_api/web/Alerts/DeleteAlert({id}):
delete:
parameters:
- name: id
in: path
required: true
description: GUID of the alert to delete
schema:
type: string
example: "'f55e3c17-63ea-456a-8451-48d2839760f7'"
security:
- delegated:
- AllSites.FullControl
- application:
- Sites.FullControl.All
responses:
200:
description: OK
/_api/web/folders/addUsingPath(decodedUrl={folderPath}):
post:
parameters:
Expand Down
65 changes: 65 additions & 0 deletions docs/docs/cmd/spo/site/site-alert-remove.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import Global from '/docs/cmd/_global.mdx';
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';

# spo site alert remove

Removes an alert from a SharePoint list

## Usage

```sh
m365 spo site alert remove [options]
```

## Options

```md definition-list
`-u, --webUrl <webUrl>`
: The URL of the SharePoint site.

`--id <id>`
: The ID of the alert.

`-f, --force`
: Don't prompt for confirmation.
```

<Global />

## Permissions

<Tabs>
<TabItem value="Delegated">

| Resource | Permissions |
|------------|----------------------|
| SharePoint | AllSites.FullControl |

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

| Resource | Permissions |
|------------|-----------------------|
| SharePoint | Sites.FullControl.All |

</TabItem>
</Tabs>

## Examples

Remove an alert by ID

```sh
m365 spo site alert remove --webUrl https://contoso.sharepoint.com/sites/Marketing --id 7cbb4c8d-8e4d-4d2e-9c6f-3f1d8b2e6a0e
```

Remove another alert without confirmation

```sh
m365 spo site alert remove --webUrl https://contoso.sharepoint.com/sites/Marketing --id 2b6f1c8a-3e6a-4c7e-b8c0-7bf4c8e6d7f1 --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 @@ -3709,6 +3709,11 @@ const sidebars: SidebarsConfig = {
label: 'site alert list',
id: 'cmd/spo/site/site-alert-list'
},
{
type: 'doc',
label: 'site alert remove',
id: 'cmd/spo/site/site-alert-remove'
},
{
type: 'doc',
label: 'site appcatalog add',
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 @@ -259,6 +259,7 @@ export default {
SITE_ADMIN_LIST: `${prefix} site admin list`,
SITE_ADMIN_REMOVE: `${prefix} site admin remove`,
SITE_ALERT_LIST: `${prefix} site alert list`,
SITE_ALERT_REMOVE: `${prefix} site alert remove`,
SITE_APPCATALOG_ADD: `${prefix} site appcatalog add`,
SITE_APPCATALOG_LIST: `${prefix} site appcatalog list`,
SITE_APPCATALOG_REMOVE: `${prefix} site appcatalog remove`,
Expand Down
130 changes: 130 additions & 0 deletions src/m365/spo/commands/site/site-alert-remove.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import assert from 'assert';
import sinon from 'sinon';
import auth from '../../../../Auth.js';
import { cli } from '../../../../cli/cli.js';
import { CommandInfo } from '../../../../cli/CommandInfo.js';
import { Logger } from '../../../../cli/Logger.js';
import { CommandError } from '../../../../Command.js';
import request from '../../../../request.js';
import { telemetry } from '../../../../telemetry.js';
import { formatting } from '../../../../utils/formatting.js';
import { pid } from '../../../../utils/pid.js';
import { session } from '../../../../utils/session.js';
import { sinonUtil } from '../../../../utils/sinonUtil.js';
import { z } from 'zod';
import commands from '../../commands.js';
import command from './site-alert-remove.js';

describe(commands.SITE_ALERT_REMOVE, () => {
let log: any[];
let logger: Logger;
let commandInfo: CommandInfo;
let commandOptionsSchema: z.ZodTypeAny;
let confirmationPromptStub: sinon.SinonStub;

const webUrl = 'https://contoso.sharepoint.com/sites/marketing';
const alertId = '39d9e102-9e8f-4e74-8f17-84a92f972fcf';

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

beforeEach(() => {
log = [];
logger = {
log: async (msg: string) => {
log.push(msg);
},
logRaw: async (msg: string) => {
log.push(msg);
},
logToStderr: async (msg: string) => {
log.push(msg);
}
};
confirmationPromptStub = sinon.stub(cli, 'promptForConfirmation').resolves(false);
});

afterEach(() => {
sinonUtil.restore([
request.delete,
cli.promptForConfirmation
]);
});

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

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

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

it('fails validation if webUrl is not a valid URL', async () => {
const actual = commandOptionsSchema.safeParse({ webUrl: 'foo' });
assert.strictEqual(actual.success, false);
});

it('fails validation if alertId is not a valid GUID', async () => {
const actual = commandOptionsSchema.safeParse({ webUrl: webUrl, id: 'invalid' });
assert.strictEqual(actual.success, false);
});

it('passes validation when valid webUrl and alertId are provided', async () => {
const actual = commandOptionsSchema.safeParse({ webUrl: webUrl, id: alertId });
assert.strictEqual(actual.success, true);
});

it('prompts before removing the alert', async () => {
await command.action(logger, { options: { webUrl: webUrl, id: alertId } });
assert(confirmationPromptStub.calledOnce);
});

it('aborts removing the alert when prompt is not confirmed', async () => {
const deleteStub = sinon.stub(request, 'delete').resolves();

await command.action(logger, { options: { webUrl: webUrl, id: alertId } });
assert(deleteStub.notCalled);
});

it('correctly removes the alert', async () => {
const deleteStub = sinon.stub(request, 'delete').callsFake(async (opts) => {
if (opts.url === `${webUrl}/_api/web/Alerts/DeleteAlert('${formatting.encodeQueryParameter(alertId)}')`) {
return;
}

throw 'Invalid request: ' + opts.url;
});

await command.action(logger, { options: { webUrl: webUrl, id: alertId, force: true, verbose: true } });
assert(deleteStub.calledOnce);
});

it('handles error correctly', async () => {
const error = {
error: {
'odata.error': {
code: '-2146232832, Microsoft.SharePoint.SPException',
message: {
value: 'The alert you are trying to access does not exist or has just been deleted.'
}
}
}
};
sinon.stub(request, 'delete').rejects(error);

await assert.rejects(command.action(logger, { options: commandOptionsSchema.parse({ force: true, webUrl: webUrl, id: alertId }) }),
new CommandError(error.error['odata.error'].message.value));
});
});
75 changes: 75 additions & 0 deletions src/m365/spo/commands/site/site-alert-remove.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import commands from '../../commands.js';
import { Logger } from '../../../../cli/Logger.js';
import SpoCommand from '../../../base/SpoCommand.js';
import { globalOptionsZod } from '../../../../Command.js';
import { z } from 'zod';
import { zod } from '../../../../utils/zod.js';
import { validation } from '../../../../utils/validation.js';
import { formatting } from '../../../../utils/formatting.js';
import request, { CliRequestOptions } from '../../../../request.js';
import { cli } from '../../../../cli/cli.js';

const options = globalOptionsZod
.extend({
webUrl: zod.alias('u', z.string()
.refine(url => validation.isValidSharePointUrl(url) === true, url => ({
message: `'${url}' is not a valid SharePoint URL.`
}))),
id: z.string()
.refine(id => validation.isValidGuid(id), id => ({
message: `'${id}' is not a valid GUID.`
})),
force: zod.alias('f', z.boolean().optional())
})
.strict();

declare type Options = z.infer<typeof options>;

interface CommandArgs {
options: Options;
}

class SpoSiteAlertRemoveCommand extends SpoCommand {
public get name(): string {
return commands.SITE_ALERT_REMOVE;
}

public get description(): string {
return 'Removes an alert from a SharePoint list';
}

public get schema(): z.ZodTypeAny | undefined {
return options;
}

public async commandAction(logger: Logger, args: CommandArgs): Promise<void> {
if (!args.options.force) {
const result = await cli.promptForConfirmation({ message: `Are you sure you want to remove the alert with id '${args.options.id}' from site '${args.options.webUrl}'?` });

if (!result) {
return;
}
}

try {
if (this.verbose) {
await logger.logToStderr(`Removing alert with ID '${args.options.id}' from site '${args.options.webUrl}'...`);
}

const requestOptions: CliRequestOptions = {
url: `${args.options.webUrl}/_api/web/Alerts/DeleteAlert('${formatting.encodeQueryParameter(args.options.id)}')`,
headers: {
accept: 'application/json;'
},
responseType: 'json'
};

await request.delete(requestOptions);
}
catch (err: any) {
this.handleRejectedODataJsonPromise(err);
}
}
}

export default new SpoSiteAlertRemoveCommand();