Skip to content

Commit ee38081

Browse files
Saurabh7019milanholemans
authored andcommitted
Fixes removing multiple home sites. Closes #6491
1 parent 1bb5ba0 commit ee38081

File tree

3 files changed

+123
-62
lines changed

3 files changed

+123
-62
lines changed

docs/docs/cmd/spo/homesite/homesite-remove.mdx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import TabItem from '@theme/TabItem';
44

55
# spo homesite remove
66

7-
Removes the current Home Site
7+
Removes a Home Site
88

99
## Usage
1010

@@ -15,6 +15,9 @@ m365 spo homesite remove [options]
1515
## Options
1616

1717
```md definition-list
18+
`-u, --url [url]`
19+
: URL of the home site to remove.
20+
1821
`-f, --force`
1922
: Do not prompt for confirmation before removing the Home Site.
2023
```
@@ -31,10 +34,10 @@ To use this command you must be either **SharePoint Administrator** or **Global
3134

3235
## Examples
3336

34-
Removes the current Home Site without confirmation.
37+
Removes a Home site specified by URL without prompting for confirmation.
3538

3639
```sh
37-
m365 spo homesite remove --force
40+
m365 spo homesite remove --url "https://contoso.sharepoint.com/sites/testcomms" --force
3841
```
3942

4043
## Response

src/m365/spo/commands/homesite/homesite-remove.spec.ts

Lines changed: 50 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,16 @@ import { sinonUtil } from '../../../../utils/sinonUtil.js';
1313
import { spo } from '../../../../utils/spo.js';
1414
import commands from '../../commands.js';
1515
import command from './homesite-remove.js';
16+
import { CommandInfo } from '../../../../cli/CommandInfo.js';
17+
import { z } from 'zod';
1618

1719
describe(commands.HOMESITE_REMOVE, () => {
1820
let log: any[];
1921
let logger: Logger;
2022
let promptIssued: boolean = false;
23+
let commandInfo: CommandInfo;
24+
let commandOptionsSchema: z.ZodTypeAny;
25+
const siteId = '00000000-0000-0000-0000-000000000010';
2126

2227
before(() => {
2328
sinon.stub(auth, 'restoreAuth').resolves();
@@ -32,6 +37,8 @@ describe(commands.HOMESITE_REMOVE, () => {
3237
});
3338
auth.connection.active = true;
3439
auth.connection.spoUrl = 'https://contoso.sharepoint.com';
40+
commandInfo = cli.getCommandInfo(command);
41+
commandOptionsSchema = commandInfo.command.getSchemaToParse()!;
3542
});
3643

3744
beforeEach(() => {
@@ -58,7 +65,8 @@ describe(commands.HOMESITE_REMOVE, () => {
5865
afterEach(() => {
5966
sinonUtil.restore([
6067
request.post,
61-
cli.promptForConfirmation
68+
cli.promptForConfirmation,
69+
spo.getSiteAdminPropertiesByUrl
6270
]);
6371
});
6472

@@ -92,6 +100,16 @@ describe(commands.HOMESITE_REMOVE, () => {
92100
assert(postSpy.notCalled);
93101
});
94102

103+
it('fails validation if the url option is not a valid SharePoint site url', async () => {
104+
const actual = commandOptionsSchema.safeParse({ url: 'invalid' });
105+
assert.strictEqual(actual.success, false);
106+
});
107+
108+
it('passes validation if the url option is a valid SharePoint site URL', async () => {
109+
const actual = commandOptionsSchema.safeParse({ url: 'https://contoso.sharepoint.com' });
110+
assert.strictEqual(actual.success, true);
111+
});
112+
95113
it('removes the Home Site when prompt confirmed', async () => {
96114
let homeSiteRemoveCallIssued = false;
97115

@@ -147,6 +165,25 @@ describe(commands.HOMESITE_REMOVE, () => {
147165
assert(homeSiteRemoveCallIssued);
148166
});
149167

168+
it('removes the Home Site specified by URL', async () => {
169+
sinon.stub(spo, 'getSiteAdminPropertiesByUrl').resolves({ SiteId: siteId } as any);
170+
171+
const postStub = sinon.stub(request, 'post').callsFake(async (opts) => {
172+
if (opts.url === `https://contoso-admin.sharepoint.com/_api/SPO.Tenant/RemoveTargetedSite`) {
173+
return;
174+
}
175+
176+
throw 'Invalid request';
177+
});
178+
179+
sinonUtil.restore(cli.promptForConfirmation);
180+
sinon.stub(cli, 'promptForConfirmation').resolves(true);
181+
182+
await command.action(logger, { options: { url: 'https://contoso.sharepoint.com', verbose: true } });
183+
assert(postStub.calledOnce);
184+
assert.deepStrictEqual(postStub.lastCall.args[0].data, { siteId });
185+
});
186+
150187
it('correctly handles error when removing the Home Site (debug)', async () => {
151188
sinon.stub(request, 'post').callsFake(async (opts) => {
152189
if (opts.data === `<Request AddExpandoFieldTypeSuffix="true" SchemaVersion="15.0.0.0" LibraryVersion="16.0.0.0" ApplicationName="${config.applicationName}" xmlns="http://schemas.microsoft.com/sharepoint/clientquery/2009"><Actions><ObjectPath Id="28" ObjectPathId="27" /><Method Name="RemoveSPHSite" Id="29" ObjectPathId="27" /></Actions><ObjectPaths><Constructor Id="27" TypeId="{268004ae-ef6b-4e9b-8425-127220d84719}" /></ObjectPaths></Request>`) {
@@ -168,24 +205,22 @@ describe(commands.HOMESITE_REMOVE, () => {
168205
new CommandError(`The requested operation is part of an experimental feature that is not supported in the current environment.`));
169206
});
170207

171-
it('correctly handles random API error', async () => {
172-
const error = {
208+
it('correctly handles error when attempting to remove a site that is not a home site or Viva Connections', async () => {
209+
sinon.stub(request, 'post').rejects({
173210
error: {
174-
'odata.error': {
175-
code: '-1, Microsoft.SharePoint.Client.InvalidOperationException',
176-
message: {
177-
value: 'An error has occurred'
211+
"odata.error": {
212+
"code": "-2146232832, Microsoft.SharePoint.SPException",
213+
"message": {
214+
"lang": "en-US",
215+
"value": "[Error ID: 03fc404e-0f70-4607-82e8-8fdb014e8658] The site with ID \"8e4686ed-b00c-4c5f-a0e2-4197081df5d5\" has not been added as a home site or Viva Connections. Check aka.ms/homesites for details."
178216
}
179217
}
180218
}
181-
};
182-
183-
sinon.stub(request, 'post').rejects(error);
219+
});
184220

185-
await assert.rejects(command.action(logger, {
186-
options: {
187-
force: true
188-
}
189-
} as any), new CommandError(error.error['odata.error'].message.value));
221+
await assert.rejects(
222+
command.action(logger, { options: { debug: true, force: true } } as any),
223+
new CommandError('[Error ID: 03fc404e-0f70-4607-82e8-8fdb014e8658] The site with ID \"8e4686ed-b00c-4c5f-a0e2-4197081df5d5\" has not been added as a home site or Viva Connections. Check aka.ms/homesites for details.')
224+
);
190225
});
191226
});
Lines changed: 67 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,95 +1,118 @@
1+
import { z } from 'zod';
2+
import { zod } from '../../../../utils/zod.js';
3+
import { globalOptionsZod } from '../../../../Command.js';
4+
import { validation } from '../../../../utils/validation.js';
15
import { cli } from '../../../../cli/cli.js';
26
import { Logger } from '../../../../cli/Logger.js';
37
import config from '../../../../config.js';
4-
import GlobalOptions from '../../../../GlobalOptions.js';
58
import request, { CliRequestOptions } from '../../../../request.js';
69
import { ClientSvcResponse, ClientSvcResponseContents, spo } from '../../../../utils/spo.js';
710
import SpoCommand from '../../../base/SpoCommand.js';
811
import commands from '../../commands.js';
912

13+
const options = globalOptionsZod
14+
.extend({
15+
url: zod.alias('u', z.string()
16+
.refine(url => validation.isValidSharePointUrl(url) === true, url => ({
17+
message: `'${url}' is not a valid SharePoint Online site URL.`
18+
})).optional()
19+
),
20+
force: zod.alias('f', z.boolean().optional())
21+
})
22+
.strict();
23+
24+
declare type Options = z.infer<typeof options>;
1025
interface CommandArgs {
1126
options: Options;
1227
}
1328

14-
interface Options extends GlobalOptions {
15-
force?: boolean;
16-
}
17-
1829
class SpoHomeSiteRemoveCommand extends SpoCommand {
1930
public get name(): string {
2031
return commands.HOMESITE_REMOVE;
2132
}
2233

2334
public get description(): string {
24-
return 'Removes the current Home Site';
25-
}
26-
27-
constructor() {
28-
super();
29-
30-
this.#initTelemetry();
31-
this.#initOptions();
35+
return 'Removes a Home Site';
3236
}
3337

34-
#initTelemetry(): void {
35-
this.telemetry.push((args: CommandArgs) => {
36-
Object.assign(this.telemetryProperties, {
37-
force: args.options.force || false
38-
});
39-
});
40-
}
41-
42-
#initOptions(): void {
43-
this.options.unshift(
44-
{
45-
option: '-f, --force'
46-
}
47-
);
38+
public get schema(): z.ZodTypeAny {
39+
return options;
4840
}
4941

5042
public async commandAction(logger: Logger, args: CommandArgs): Promise<void> {
5143

5244
const removeHomeSite: () => Promise<void> = async (): Promise<void> => {
5345
try {
46+
if (this.verbose) {
47+
await logger.logToStderr(`Removing ${args.options.url ? `'${args.options.url}' as home site` : 'the current home site'}...`);
48+
}
49+
5450
const spoAdminUrl = await spo.getSpoAdminUrl(logger, this.debug);
5551
const reqDigest = await spo.getRequestDigest(spoAdminUrl);
5652

57-
const requestOptions: CliRequestOptions = {
58-
url: `${spoAdminUrl}/_vti_bin/client.svc/ProcessQuery`,
59-
headers: {
60-
'X-RequestDigest': reqDigest.FormDigestValue
61-
},
62-
data: `<Request AddExpandoFieldTypeSuffix="true" SchemaVersion="15.0.0.0" LibraryVersion="16.0.0.0" ApplicationName="${config.applicationName}" xmlns="http://schemas.microsoft.com/sharepoint/clientquery/2009"><Actions><ObjectPath Id="28" ObjectPathId="27" /><Method Name="RemoveSPHSite" Id="29" ObjectPathId="27" /></Actions><ObjectPaths><Constructor Id="27" TypeId="{268004ae-ef6b-4e9b-8425-127220d84719}" /></ObjectPaths></Request>`
63-
};
64-
65-
const res = await request.post<string>(requestOptions);
66-
67-
const json: ClientSvcResponse = JSON.parse(res);
68-
const response: ClientSvcResponseContents = json[0];
69-
if (response.ErrorInfo) {
70-
throw response.ErrorInfo.ErrorMessage;
53+
if (args.options.url) {
54+
await this.removeHomeSiteByUrl(args.options.url, spoAdminUrl, logger);
55+
await logger.log(`${args.options.url} has been removed as a Home Site. It may take some time for the change to apply. Check aka.ms/homesites for details.`);
7156
}
7257
else {
73-
await logger.log(json[json.length - 1]);
58+
await this.warn(logger, `The current way this command works is deprecated and will change in the next major release. The '--url' option will become required.`);
59+
60+
const requestOptions: CliRequestOptions = {
61+
url: `${spoAdminUrl}/_vti_bin/client.svc/ProcessQuery`,
62+
headers: {
63+
'X-RequestDigest': reqDigest.FormDigestValue
64+
},
65+
data: `<Request AddExpandoFieldTypeSuffix="true" SchemaVersion="15.0.0.0" LibraryVersion="16.0.0.0" ApplicationName="${config.applicationName}" xmlns="http://schemas.microsoft.com/sharepoint/clientquery/2009"><Actions><ObjectPath Id="28" ObjectPathId="27" /><Method Name="RemoveSPHSite" Id="29" ObjectPathId="27" /></Actions><ObjectPaths><Constructor Id="27" TypeId="{268004ae-ef6b-4e9b-8425-127220d84719}" /></ObjectPaths></Request>`
66+
};
67+
68+
const res = await request.post<string>(requestOptions);
69+
70+
const json: ClientSvcResponse = JSON.parse(res);
71+
const response: ClientSvcResponseContents = json[0];
72+
if (response.ErrorInfo) {
73+
throw response.ErrorInfo.ErrorMessage;
74+
}
75+
else {
76+
await logger.log(json[json.length - 1]);
77+
}
7478
}
7579
}
7680
catch (err: any) {
7781
this.handleRejectedODataJsonPromise(err);
7882
}
7983
};
8084

81-
8285
if (args.options.force) {
8386
await removeHomeSite();
8487
}
8588
else {
86-
const result = await cli.promptForConfirmation({ message: `Are you sure you want to remove the Home Site?` });
89+
const result = await cli.promptForConfirmation({
90+
message: args.options.url
91+
? `Are you sure you want to remove '${args.options.url}' as home site?`
92+
: `Are you sure you want to remove the current home site?`
93+
});
8794

8895
if (result) {
8996
await removeHomeSite();
9097
}
9198
}
9299
}
100+
101+
private async removeHomeSiteByUrl(siteUrl: string, spoAdminUrl: string, logger: Logger): Promise<void> {
102+
const siteAdminProperties = await spo.getSiteAdminPropertiesByUrl(siteUrl, false, logger, this.verbose);
103+
104+
const requestOptions: CliRequestOptions = {
105+
url: `${spoAdminUrl}/_api/SPO.Tenant/RemoveTargetedSite`,
106+
headers: {
107+
accept: 'application/json;odata=nometadata'
108+
},
109+
data: {
110+
siteId: siteAdminProperties.SiteId
111+
}
112+
};
113+
114+
await request.post(requestOptions);
115+
}
93116
}
94117

95118
export default new SpoHomeSiteRemoveCommand();

0 commit comments

Comments
 (0)