Skip to content

Commit 4253703

Browse files
authored
Adds command 'spo site alert remove'. Closes #6863
1 parent 53bc672 commit 4253703

File tree

6 files changed

+294
-0
lines changed

6 files changed

+294
-0
lines changed

.devproxy/api-specs/sharepoint.yaml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,24 @@ paths:
113113
responses:
114114
200:
115115
description: OK
116+
/_api/web/Alerts/DeleteAlert({id}):
117+
delete:
118+
parameters:
119+
- name: id
120+
in: path
121+
required: true
122+
description: GUID of the alert to delete
123+
schema:
124+
type: string
125+
example: "'f55e3c17-63ea-456a-8451-48d2839760f7'"
126+
security:
127+
- delegated:
128+
- AllSites.FullControl
129+
- application:
130+
- Sites.FullControl.All
131+
responses:
132+
200:
133+
description: OK
116134
/_api/web/folders/addUsingPath(decodedUrl={folderPath}):
117135
post:
118136
parameters:
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import Global from '/docs/cmd/_global.mdx';
2+
import Tabs from '@theme/Tabs';
3+
import TabItem from '@theme/TabItem';
4+
5+
# spo site alert remove
6+
7+
Removes an alert from a SharePoint list
8+
9+
## Usage
10+
11+
```sh
12+
m365 spo site alert remove [options]
13+
```
14+
15+
## Options
16+
17+
```md definition-list
18+
`-u, --webUrl <webUrl>`
19+
: The URL of the SharePoint site.
20+
21+
`--id <id>`
22+
: The ID of the alert.
23+
24+
`-f, --force`
25+
: Don't prompt for confirmation.
26+
```
27+
28+
<Global />
29+
30+
## Permissions
31+
32+
<Tabs>
33+
<TabItem value="Delegated">
34+
35+
| Resource | Permissions |
36+
|------------|----------------------|
37+
| SharePoint | AllSites.FullControl |
38+
39+
</TabItem>
40+
<TabItem value="Application">
41+
42+
| Resource | Permissions |
43+
|------------|-----------------------|
44+
| SharePoint | Sites.FullControl.All |
45+
46+
</TabItem>
47+
</Tabs>
48+
49+
## Examples
50+
51+
Remove an alert by ID
52+
53+
```sh
54+
m365 spo site alert remove --webUrl https://contoso.sharepoint.com/sites/Marketing --id 7cbb4c8d-8e4d-4d2e-9c6f-3f1d8b2e6a0e
55+
```
56+
57+
Remove another alert without confirmation
58+
59+
```sh
60+
m365 spo site alert remove --webUrl https://contoso.sharepoint.com/sites/Marketing --id 2b6f1c8a-3e6a-4c7e-b8c0-7bf4c8e6d7f1 --force
61+
```
62+
63+
## Response
64+
65+
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
@@ -3709,6 +3709,11 @@ const sidebars: SidebarsConfig = {
37093709
label: 'site alert list',
37103710
id: 'cmd/spo/site/site-alert-list'
37113711
},
3712+
{
3713+
type: 'doc',
3714+
label: 'site alert remove',
3715+
id: 'cmd/spo/site/site-alert-remove'
3716+
},
37123717
{
37133718
type: 'doc',
37143719
label: 'site appcatalog add',

src/m365/spo/commands.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,7 @@ export default {
259259
SITE_ADMIN_LIST: `${prefix} site admin list`,
260260
SITE_ADMIN_REMOVE: `${prefix} site admin remove`,
261261
SITE_ALERT_LIST: `${prefix} site alert list`,
262+
SITE_ALERT_REMOVE: `${prefix} site alert remove`,
262263
SITE_APPCATALOG_ADD: `${prefix} site appcatalog add`,
263264
SITE_APPCATALOG_LIST: `${prefix} site appcatalog list`,
264265
SITE_APPCATALOG_REMOVE: `${prefix} site appcatalog remove`,
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
import assert from 'assert';
2+
import sinon from 'sinon';
3+
import auth from '../../../../Auth.js';
4+
import { cli } from '../../../../cli/cli.js';
5+
import { CommandInfo } from '../../../../cli/CommandInfo.js';
6+
import { Logger } from '../../../../cli/Logger.js';
7+
import { CommandError } from '../../../../Command.js';
8+
import request from '../../../../request.js';
9+
import { telemetry } from '../../../../telemetry.js';
10+
import { formatting } from '../../../../utils/formatting.js';
11+
import { pid } from '../../../../utils/pid.js';
12+
import { session } from '../../../../utils/session.js';
13+
import { sinonUtil } from '../../../../utils/sinonUtil.js';
14+
import { z } from 'zod';
15+
import commands from '../../commands.js';
16+
import command from './site-alert-remove.js';
17+
18+
describe(commands.SITE_ALERT_REMOVE, () => {
19+
let log: any[];
20+
let logger: Logger;
21+
let commandInfo: CommandInfo;
22+
let commandOptionsSchema: z.ZodTypeAny;
23+
let confirmationPromptStub: sinon.SinonStub;
24+
25+
const webUrl = 'https://contoso.sharepoint.com/sites/marketing';
26+
const alertId = '39d9e102-9e8f-4e74-8f17-84a92f972fcf';
27+
28+
before(() => {
29+
sinon.stub(auth, 'restoreAuth').resolves();
30+
sinon.stub(telemetry, 'trackEvent').resolves();
31+
sinon.stub(pid, 'getProcessName').returns('');
32+
sinon.stub(session, 'getId').returns('');
33+
commandInfo = cli.getCommandInfo(command);
34+
commandOptionsSchema = commandInfo.command.getSchemaToParse()!;
35+
auth.connection.active = true;
36+
});
37+
38+
beforeEach(() => {
39+
log = [];
40+
logger = {
41+
log: async (msg: string) => {
42+
log.push(msg);
43+
},
44+
logRaw: async (msg: string) => {
45+
log.push(msg);
46+
},
47+
logToStderr: async (msg: string) => {
48+
log.push(msg);
49+
}
50+
};
51+
confirmationPromptStub = sinon.stub(cli, 'promptForConfirmation').resolves(false);
52+
});
53+
54+
afterEach(() => {
55+
sinonUtil.restore([
56+
request.delete,
57+
cli.promptForConfirmation
58+
]);
59+
});
60+
61+
after(() => {
62+
sinon.restore();
63+
auth.connection.active = false;
64+
});
65+
66+
it('has correct name', () => {
67+
assert.strictEqual(command.name, commands.SITE_ALERT_REMOVE);
68+
});
69+
70+
it('has a description', () => {
71+
assert.notStrictEqual(command.description, null);
72+
});
73+
74+
it('fails validation if webUrl is not a valid URL', async () => {
75+
const actual = commandOptionsSchema.safeParse({ webUrl: 'foo', id: alertId });
76+
assert.strictEqual(actual.success, false);
77+
});
78+
79+
it('fails validation if alertId is not a valid GUID', async () => {
80+
const actual = commandOptionsSchema.safeParse({ webUrl: webUrl, id: 'invalid' });
81+
assert.strictEqual(actual.success, false);
82+
});
83+
84+
it('passes validation when valid webUrl and alertId are provided', async () => {
85+
const actual = commandOptionsSchema.safeParse({ webUrl: webUrl, id: alertId });
86+
assert.strictEqual(actual.success, true);
87+
});
88+
89+
it('prompts before removing the alert', async () => {
90+
await command.action(logger, { options: { webUrl: webUrl, id: alertId } });
91+
assert(confirmationPromptStub.calledOnce);
92+
});
93+
94+
it('aborts removing the alert when prompt is not confirmed', async () => {
95+
const deleteStub = sinon.stub(request, 'delete').resolves();
96+
97+
await command.action(logger, { options: { webUrl: webUrl, id: alertId } });
98+
assert(deleteStub.notCalled);
99+
});
100+
101+
it('correctly removes the alert', async () => {
102+
const deleteStub = sinon.stub(request, 'delete').callsFake(async (opts) => {
103+
if (opts.url === `${webUrl}/_api/web/Alerts/DeleteAlert('${formatting.encodeQueryParameter(alertId)}')`) {
104+
return;
105+
}
106+
107+
throw 'Invalid request: ' + opts.url;
108+
});
109+
110+
await command.action(logger, { options: { webUrl: webUrl, id: alertId, force: true, verbose: true } });
111+
assert(deleteStub.calledOnce);
112+
});
113+
114+
it('handles error correctly', async () => {
115+
const error = {
116+
error: {
117+
'odata.error': {
118+
code: '-2146232832, Microsoft.SharePoint.SPException',
119+
message: {
120+
value: 'The alert you are trying to access does not exist or has just been deleted.'
121+
}
122+
}
123+
}
124+
};
125+
sinon.stub(request, 'delete').rejects(error);
126+
127+
await assert.rejects(command.action(logger, { options: { force: true, webUrl: webUrl, id: alertId } }),
128+
new CommandError(error.error['odata.error'].message.value));
129+
});
130+
});
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import commands from '../../commands.js';
2+
import { Logger } from '../../../../cli/Logger.js';
3+
import SpoCommand from '../../../base/SpoCommand.js';
4+
import { globalOptionsZod } from '../../../../Command.js';
5+
import { z } from 'zod';
6+
import { zod } from '../../../../utils/zod.js';
7+
import { validation } from '../../../../utils/validation.js';
8+
import { formatting } from '../../../../utils/formatting.js';
9+
import request, { CliRequestOptions } from '../../../../request.js';
10+
import { cli } from '../../../../cli/cli.js';
11+
12+
const options = globalOptionsZod
13+
.extend({
14+
webUrl: zod.alias('u', z.string()
15+
.refine(url => validation.isValidSharePointUrl(url) === true, url => ({
16+
message: `'${url}' is not a valid SharePoint URL.`
17+
}))),
18+
id: z.string()
19+
.refine(id => validation.isValidGuid(id), id => ({
20+
message: `'${id}' is not a valid GUID.`
21+
})),
22+
force: zod.alias('f', z.boolean().optional())
23+
})
24+
.strict();
25+
26+
declare type Options = z.infer<typeof options>;
27+
28+
interface CommandArgs {
29+
options: Options;
30+
}
31+
32+
class SpoSiteAlertRemoveCommand extends SpoCommand {
33+
public get name(): string {
34+
return commands.SITE_ALERT_REMOVE;
35+
}
36+
37+
public get description(): string {
38+
return 'Removes an alert from a SharePoint list';
39+
}
40+
41+
public get schema(): z.ZodTypeAny | undefined {
42+
return options;
43+
}
44+
45+
public async commandAction(logger: Logger, args: CommandArgs): Promise<void> {
46+
if (!args.options.force) {
47+
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}'?` });
48+
49+
if (!result) {
50+
return;
51+
}
52+
}
53+
54+
try {
55+
if (this.verbose) {
56+
await logger.logToStderr(`Removing alert with ID '${args.options.id}' from site '${args.options.webUrl}'...`);
57+
}
58+
59+
const requestOptions: CliRequestOptions = {
60+
url: `${args.options.webUrl}/_api/web/Alerts/DeleteAlert('${formatting.encodeQueryParameter(args.options.id)}')`,
61+
headers: {
62+
accept: 'application/json;odata=nometadata'
63+
},
64+
responseType: 'json'
65+
};
66+
67+
await request.delete(requestOptions);
68+
}
69+
catch (err: any) {
70+
this.handleRejectedODataJsonPromise(err);
71+
}
72+
}
73+
}
74+
75+
export default new SpoSiteAlertRemoveCommand();

0 commit comments

Comments
 (0)