Skip to content

Commit 71b45af

Browse files
reshmee011milanholemans
authored andcommitted
Adds command 'spo homesite add'. Closes #6488
1 parent d99742c commit 71b45af

File tree

5 files changed

+375
-0
lines changed

5 files changed

+375
-0
lines changed
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
import Global from '/docs/cmd/_global.mdx';
2+
import Tabs from '@theme/Tabs';
3+
import TabItem from '@theme/TabItem';
4+
5+
# spo homesite add
6+
7+
Adds a home site
8+
9+
## Usage
10+
11+
```sh
12+
m365 spo homesite add [options]
13+
```
14+
15+
## Options
16+
17+
```md definition-list
18+
`-u, --url <url>`
19+
: URL of the site to use as a home site.
20+
21+
`--isInDraftMode [isInDraftMode]`
22+
: Specifies whether the home site is in draft mode. Accepts `true` or `false`. Default is `false`.
23+
24+
`--vivaConnectionsDefaultStart [vivaConnectionsDefaultStart]`
25+
: Specifies whether the home site is the default start for Viva Connections. Accepts `true` or `false`. Default is `true`.
26+
27+
`--audiences [audiences]`
28+
: Comma-separated list of Microsoft Entra group IDs that will be used as audience.
29+
30+
`--order [order]`
31+
: Order of the home site. Must be a positive integer.
32+
```
33+
34+
<Global />
35+
36+
## Examples
37+
38+
Add a home site
39+
40+
```sh
41+
m365 spo homesite add --url "https://contoso.sharepoint.com/sites/testcomms"
42+
```
43+
44+
Add a home site with additional options
45+
46+
```sh
47+
m365 spo homesite add --url "https://contoso.sharepoint.com/sites/testcomms" --isInDraftMode true --vivaConnectionsDefaultStart false --audiences "af8c0bc8-7b1b-44b4-b087-ffcc8df70d16,754ff15c-76b1-44cb-88c7-0065a4d3cfb7" --order 2
48+
```
49+
50+
## Response
51+
52+
<Tabs>
53+
<TabItem value="JSON">
54+
55+
```json
56+
{
57+
"Audiences": [
58+
{
59+
"Email": "active@contoso.onmicrosoft.com",
60+
"Id": "7a1eea7f-9ab0-40ff-8f2e-0083d9d63451",
61+
"Title": "active Members"
62+
}
63+
],
64+
"IsInDraftMode": true,
65+
"IsVivaBackendSite": false,
66+
"SiteId": "ca49054c-85f3-41eb-a290-46ffda8f219c",
67+
"TargetedLicenseType": 0,
68+
"Title": "testcommsite",
69+
"Url": "https://contoso.sharepoint.com/sites/testcomms",
70+
"VivaConnectionsDefaultStart": false,
71+
"WebId": "256c4f0f-e372-47b4-a891-b4888e829e20"
72+
}
73+
```
74+
75+
</TabItem>
76+
<TabItem value="Text">
77+
78+
```text
79+
Audiences : [{"Email":"active@contoso.onmicrosoft.com","Id":"7a1eea7f-9ab0-40ff-8f2e-0083d9d63451","Title":"active Members"}]
80+
IsInDraftMode : true
81+
IsVivaBackendSite : false
82+
SiteId : ca49054c-85f3-41eb-a290-46ffda8f219c
83+
TargetedLicenseType : 0
84+
Title : testcommsite
85+
Url : https://contoso.sharepoint.com/sites/testcomms
86+
VivaConnectionsDefaultStart: false
87+
WebId : 256c4f0f-e372-47b4-a891-b4888e829e20
88+
```
89+
90+
</TabItem>
91+
<TabItem value="CSV">
92+
93+
```csv
94+
IsInDraftMode,IsVivaBackendSite,SiteId,TargetedLicenseType,Title,Url,VivaConnectionsDefaultStart,WebId
95+
1,0,ca49054c-85f3-41eb-a290-46ffda8f219c,0,testcommsite,https://contoso.sharepoint.com/sites/testcomms,0,256c4f0f-e372-47b4-a891-b4888e829e20
96+
```
97+
98+
</TabItem>
99+
<TabItem value="Markdown">
100+
101+
```md
102+
# spo homesite add --url "https://contoso.sharepoint.com/sites/testcomms"
103+
104+
Date: 12/23/2024
105+
106+
## testcommsite (https://contoso.sharepoint.com/sites/testcomms)
107+
108+
Property | Value
109+
---------|-------
110+
IsInDraftMode | true
111+
IsVivaBackendSite | false
112+
SiteId | ca49054c-85f3-41eb-a290-46ffda8f219c
113+
TargetedLicenseType | 0
114+
Title | testcommsite
115+
Url | https://contoso.sharepoint.com/sites/testcomms
116+
VivaConnectionsDefaultStart | false
117+
WebId | 256c4f0f-e372-47b4-a891-b4888e829e20
118+
```
119+
</TabItem>
120+
</Tabs>
121+
122+
## More information
123+
124+
- SharePoint home sites [Viva Connections set up](https://learn.microsoft.com/en-us/viva/connections/set-up-admin-center)

docs/src/config/sidebars.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2847,6 +2847,11 @@ const sidebars: SidebarsConfig = {
28472847
},
28482848
{
28492849
homesite: [
2850+
{
2851+
type: 'doc',
2852+
label: 'homesite add',
2853+
id: 'cmd/spo/homesite/homesite-add'
2854+
},
28502855
{
28512856
type: 'doc',
28522857
label: 'homesite get',

src/m365/spo/commands.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ export default {
117117
GROUP_MEMBER_REMOVE: `${prefix} group member remove`,
118118
HIDEDEFAULTTHEMES_GET: `${prefix} hidedefaultthemes get`,
119119
HIDEDEFAULTTHEMES_SET: `${prefix} hidedefaultthemes set`,
120+
HOMESITE_ADD: `${prefix} homesite add`,
120121
HOMESITE_GET: `${prefix} homesite get`,
121122
HOMESITE_LIST: `${prefix} homesite list`,
122123
HOMESITE_REMOVE: `${prefix} homesite remove`,
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
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 { Logger } from '../../../../cli/Logger.js';
6+
import { CommandError } from '../../../../Command.js';
7+
import { CommandInfo } from '../../../../cli/CommandInfo.js';
8+
import request from '../../../../request.js';
9+
import { telemetry } from '../../../../telemetry.js';
10+
import { pid } from '../../../../utils/pid.js';
11+
import { session } from '../../../../utils/session.js';
12+
import { sinonUtil } from '../../../../utils/sinonUtil.js';
13+
import commands from '../../commands.js';
14+
import command from '../homesite/homesite-add.js';
15+
import { z } from 'zod';
16+
17+
describe(commands.HOMESITE_ADD, () => {
18+
let log: string[];
19+
let logger: Logger;
20+
let loggerLogSpy: sinon.SinonSpy;
21+
let commandInfo: CommandInfo;
22+
let commandOptionsSchema: z.ZodTypeAny;
23+
24+
const homeSite = "https://contoso.sharepoint.com/sites/testcomms";
25+
const homeSites = {
26+
"Audiences": [],
27+
"IsInDraftMode": true,
28+
"IsVivaBackendSite": false,
29+
"SiteId": "ca49054c-85f3-41eb-a290-46ffda8f219c",
30+
"TargetedLicenseType": 0,
31+
"Title": "testcommsite",
32+
"Url": homeSite,
33+
"VivaConnectionsDefaultStart": false,
34+
"WebId": "256c4f0f-e372-47b4-a891-b4888e829e20"
35+
};
36+
37+
const homeSiteConfig = {
38+
"Audiences": [
39+
{
40+
"Email": "SharingTest@reshmeeauckloo.onmicrosoft.com",
41+
"Id": "af8c0bc8-7b1b-44b4-b087-ffcc8df70d16",
42+
"Title": "SharingTest Members"
43+
}
44+
],
45+
"IsInDraftMode": true,
46+
"IsVivaBackendSite": false,
47+
"SiteId": "ca49054c-85f3-41eb-a290-46ffda8f219c",
48+
"TargetedLicenseType": 0,
49+
"Title": "testcommsite",
50+
"Url": "https://contoso.sharepoint.com/sites/testcomms",
51+
"VivaConnectionsDefaultStart": false,
52+
"WebId": "256c4f0f-e372-47b4-a891-b4888e829e20"
53+
};
54+
55+
before(() => {
56+
sinon.stub(auth, 'restoreAuth').resolves();
57+
sinon.stub(telemetry, 'trackEvent').resolves();
58+
sinon.stub(pid, 'getProcessName').returns('');
59+
sinon.stub(session, 'getId').returns('');
60+
auth.connection.active = true;
61+
auth.connection.spoUrl = 'https://contoso.sharepoint.com';
62+
commandInfo = cli.getCommandInfo(command);
63+
commandOptionsSchema = commandInfo.command.getSchemaToParse()!;
64+
});
65+
66+
beforeEach(() => {
67+
log = [];
68+
logger = {
69+
log: async (msg: string) => {
70+
log.push(msg);
71+
},
72+
logRaw: async (msg: string) => {
73+
log.push(msg);
74+
},
75+
logToStderr: async (msg: string) => {
76+
log.push(msg);
77+
}
78+
};
79+
loggerLogSpy = sinon.spy(logger, 'log');
80+
});
81+
82+
afterEach(() => {
83+
sinonUtil.restore([
84+
request.post
85+
]);
86+
});
87+
88+
after(() => {
89+
sinon.restore();
90+
auth.connection.active = false;
91+
auth.connection.spoUrl = undefined;
92+
});
93+
94+
it('has correct name', () => {
95+
assert.strictEqual(command.name, commands.HOMESITE_ADD);
96+
});
97+
98+
it('has a description', () => {
99+
assert.notStrictEqual(command.description, null);
100+
});
101+
102+
it('correctly logs command response', async () => {
103+
sinon.stub(request, 'post').callsFake(async (opts) => {
104+
if (opts.url === `https://contoso-admin.sharepoint.com/_api/SPHSite/AddHomeSite`) {
105+
return homeSites;
106+
}
107+
108+
throw opts.url;
109+
});
110+
111+
await command.action(logger, { options: { url: homeSite, verbose: true } });
112+
assert(loggerLogSpy.calledWith(homeSites));
113+
});
114+
115+
it('adds a home site with the specified URL, isInDraftMode, vivaConnectionsDefaultStart, and audiences', async () => {
116+
const postStub = sinon.stub(request, 'post').callsFake(async (opts) => {
117+
if (opts.url === `https://contoso-admin.sharepoint.com/_api/SPHSite/AddHomeSite`) {
118+
return homeSiteConfig;
119+
}
120+
throw 'Invalid request';
121+
});
122+
123+
await command.action(logger, {
124+
options: {
125+
url: homeSite,
126+
isInDraftMode: true,
127+
vivaConnectionsDefaultStart: false,
128+
audiences: 'af8c0bc8-7b1b-44b4-b087-ffcc8df70d16',
129+
order: 2
130+
}
131+
});
132+
133+
const expectedData = {
134+
"audiences": [
135+
"af8c0bc8-7b1b-44b4-b087-ffcc8df70d16"
136+
],
137+
"isInDraftMode": true,
138+
"order": 2,
139+
"siteUrl": "https://contoso.sharepoint.com/sites/testcomms",
140+
"vivaConnectionsDefaultStart": false
141+
};
142+
assert.deepStrictEqual(postStub.lastCall.args[0].data, expectedData);
143+
});
144+
145+
it('fails validation if the url is not a valid SharePoint url', async () => {
146+
const actual = commandOptionsSchema.safeParse({ url: 'invalid' });
147+
assert.strictEqual(actual.success, false);
148+
});
149+
150+
it('correctly handles non-integer order', async () => {
151+
const actual = commandOptionsSchema.safeParse({ url: homeSite, order: -1 });
152+
assert.strictEqual(actual.success, false);
153+
});
154+
155+
it('correctly handles invalid GUIDs in audiences', async () => {
156+
const actual = commandOptionsSchema.safeParse({ url: homeSite, audiences: 'invalid-guid' });
157+
assert.strictEqual(actual.success, false);
158+
});
159+
160+
it('correctly handles OData error when adding a home site', async () => {
161+
sinon.stub(request, 'post').rejects({ error: { 'odata.error': { message: { value: 'An error has occurred' } } } });
162+
163+
await assert.rejects(command.action(logger, { options: { url: 'https://contoso.sharepoint.com' } }), new CommandError('An error has occurred'));
164+
});
165+
});
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import { z } from 'zod';
2+
import { zod } from '../../../../utils/zod.js';
3+
import { Logger } from '../../../../cli/Logger.js';
4+
import { globalOptionsZod } from '../../../../Command.js';
5+
import { spo } from '../../../../utils/spo.js';
6+
import { validation } from '../../../../utils/validation.js';
7+
import SpoCommand from '../../../base/SpoCommand.js';
8+
import commands from '../../commands.js';
9+
import request, { CliRequestOptions } from '../../../../request.js';
10+
11+
const options = globalOptionsZod
12+
.extend({
13+
url: zod.alias('u', z.string()
14+
.refine((url: string) => validation.isValidSharePointUrl(url) === true, url => ({
15+
message: `'${url}' is not a valid SharePoint Online site URL.`
16+
}))
17+
),
18+
audiences: z.string()
19+
.refine(audiences => validation.isValidGuidArray(audiences) === true, audiences => ({
20+
message: `The following GUIDs are invalid: ${validation.isValidGuidArray(audiences)}.`
21+
})).optional(),
22+
vivaConnectionsDefaultStart: z.boolean().optional(),
23+
isInDraftMode: z.boolean().optional(),
24+
order: z.number()
25+
.refine(order => validation.isValidPositiveInteger(order) === true, order => ({
26+
message: `'${order}' is not a positive integer.`
27+
})).optional()
28+
})
29+
.strict();
30+
31+
declare type Options = z.infer<typeof options>;
32+
interface CommandArgs {
33+
options: Options;
34+
}
35+
36+
class SpoHomeSiteAddCommand extends SpoCommand {
37+
public get name(): string {
38+
return commands.HOMESITE_ADD;
39+
}
40+
41+
public get description(): string {
42+
return 'Adds a home site';
43+
}
44+
45+
public get schema(): z.ZodTypeAny {
46+
return options;
47+
}
48+
49+
public async commandAction(logger: Logger, args: CommandArgs): Promise<void> {
50+
try {
51+
const spoAdminUrl: string = await spo.getSpoAdminUrl(logger, this.verbose);
52+
const requestOptions: CliRequestOptions = {
53+
url: `${spoAdminUrl}/_api/SPHSite/AddHomeSite`,
54+
headers: {
55+
accept: 'application/json;odata=nometadata'
56+
},
57+
responseType: 'json',
58+
data: {
59+
siteUrl: args.options.url,
60+
audiences: args.options.audiences?.split(','),
61+
vivaConnectionsDefaultStart: args.options.vivaConnectionsDefaultStart ?? true,
62+
isInDraftMode: args.options.isInDraftMode ?? false,
63+
order: args.options.order
64+
}
65+
};
66+
67+
if (this.verbose) {
68+
await logger.logToStderr(`Adding home site with URL: ${args.options.url}...`);
69+
}
70+
71+
const res = await request.post(requestOptions);
72+
await logger.log(res);
73+
}
74+
catch (err: any) {
75+
this.handleRejectedODataJsonPromise(err);
76+
}
77+
}
78+
}
79+
80+
export default new SpoHomeSiteAddCommand();

0 commit comments

Comments
 (0)