Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds m365 spo tenant homesite add. Closes #6488 #6533

Draft
wants to merge 9 commits into
base: main
Choose a base branch
from
116 changes: 116 additions & 0 deletions docs/docs/cmd/spo/tenant/tenant-homesite-add.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import Global from '/docs/cmd/_global.mdx';
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';

# spo tenant homesite add

Adds a Home Site.

## Usage

```sh
m365 spo tenant homesite add [options]
```

## Options

```md definition-list
`-u, --url <url>`
: URL of the site to use as a home site.

`--isInDraftMode [isInDraftMode]`
: Specifies whether the home site is in draft mode. Accepts `true` or `false`.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's also mention which value is the default value.


`--vivaConnectionsDefaultStart [vivaConnectionsDefaultStart]`
: Specifies whether the home site is the default start for Viva Connections. Accepts `true` or `false`.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's mention the default value here as well.


`--audiences [audiences]`
: Comma-separated list of audience GUIDs.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are GUIDs of Microsoft Entra groups, right? Let's clarify that a bit more.


`--order [order]`
: Order of the home site. Must be an integer.
```

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's not forget to include the global options that are available in every command.

## Examples

Add a Home Site

```sh
m365 spo tenant homesite add --url "https://contoso.sharepoint.com/sites/testcomms"
```

Add a Home Site with optional parameters

```sh
m365 spo tenant 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
```
## Response
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's add a blank line between content and title


<Tabs>
<TabItem value="JSON">

```json
{
"Audiences": [],
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we add 1 audience value to the response? This way it's clear for the user to know what value they can expect.
Let's update the other responses as well.

"IsInDraftMode": true,
"IsVivaBackendSite": false,
"SiteId": "ca49054c-85f3-41eb-a290-46ffda8f219c",
"TargetedLicenseType": 0,
"Title": "testcommsite",
"Url": "https://contoso.sharepoint.com/sites/testcomms",
"VivaConnectionsDefaultStart": false,
"WebId": "256c4f0f-e372-47b4-a891-b4888e829e20"
}
```

</TabItem>
<TabItem value="Text">

```text
Audiences : []
IsInDraftMode : true
IsVivaBackendSite : false
SiteId : ca49054c-85f3-41eb-a290-46ffda8f219c
TargetedLicenseType : 0
Title : testcommsite
Url : https://contoso.sharepoint.com/sites/testcomms
VivaConnectionsDefaultStart: false
WebId : 256c4f0f-e372-47b4-a891-b4888e829e20
```

</TabItem>
<TabItem value="CSV">

```csv
IsInDraftMode,IsVivaBackendSite,SiteId,TargetedLicenseType,Title,Url,VivaConnectionsDefaultStart,WebId
1,0,ca49054c-85f3-41eb-a290-46ffda8f219c,0,testcommsite,https://contoso.sharepoint.com/sites/testcomms,0,256c4f0f-e372-47b4-a891-b4888e829e20
```

</TabItem>
<TabItem value="Markdown">

```md
# spo tenant homesite add --url "https://contoso.sharepoint.com/sites/testcomms"

Date: 12/23/2024

## testcommsite (https://contoso.sharepoint.com/sites/testcomms)

Property | Value
---------|-------
IsInDraftMode | true
IsVivaBackendSite | false
SiteId | ca49054c-85f3-41eb-a290-46ffda8f219c
TargetedLicenseType | 0
Title | testcommsite
Url | https://contoso.sharepoint.com/sites/testcomms
VivaConnectionsDefaultStart | false
WebId | 256c4f0f-e372-47b4-a891-b4888e829e20
```
</TabItem>
</Tabs>

## More information

- SharePoint home sites [Viva Connections set up](https://learn.microsoft.com/en-us/viva/connections/set-up-admin-center)

5 changes: 5 additions & 0 deletions docs/src/config/sidebars.ts
Original file line number Diff line number Diff line change
@@ -3766,6 +3766,11 @@ const sidebars: SidebarsConfig = {
label: 'tenant commandset set',
id: 'cmd/spo/tenant/tenant-commandset-set'
},
{
type: 'doc',
label: 'tenant homesite add',
id: 'cmd/spo/tenant/tenant-homesite-add'
},
{
type: 'doc',
label: 'tenant homesite list',
1 change: 1 addition & 0 deletions src/m365/spo/commands.ts
Original file line number Diff line number Diff line change
@@ -318,6 +318,7 @@ export default {
TENANT_COMMANDSET_LIST: `${prefix} tenant commandset list`,
TENANT_COMMANDSET_REMOVE: `${prefix} tenant commandset remove`,
TENANT_COMMANDSET_SET: `${prefix} tenant commandset set`,
TENANT_HOMESITE_ADD: `${prefix} tenant homesite add`,
TENANT_HOMESITE_LIST: `${prefix} tenant homesite list`,
TENANT_RECYCLEBINITEM_LIST: `${prefix} tenant recyclebinitem list`,
TENANT_RECYCLEBINITEM_REMOVE: `${prefix} tenant recyclebinitem remove`,
175 changes: 175 additions & 0 deletions src/m365/spo/commands/tenant/tenant-homesite-add.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
import assert from 'assert';
import sinon from 'sinon';
import auth from '../../../../Auth.js';
import { cli } from '../../../../cli/cli.js';
import { Logger } from '../../../../cli/Logger.js';
import { CommandError } from '../../../../Command.js';
import { CommandInfo } from '../../../../cli/CommandInfo.js';
import request from '../../../../request.js';
import { telemetry } from '../../../../telemetry.js';
import { pid } from '../../../../utils/pid.js';
import { session } from '../../../../utils/session.js';
import { sinonUtil } from '../../../../utils/sinonUtil.js';
import commands from '../../commands.js';
import command from './tenant-homesite-add.js';

describe(commands.TENANT_HOMESITE_ADD, () => {
let log: string[];
let logger: Logger;
let loggerLogSpy: sinon.SinonSpy;
let commandInfo: CommandInfo;
const homeSite = "https://contoso.sharepoint.com/sites/testcomms";
const homeSites = {
"Audiences": [],
"IsInDraftMode": true,
"IsVivaBackendSite": false,
"SiteId": "ca49054c-85f3-41eb-a290-46ffda8f219c",
"TargetedLicenseType": 0,
"Title": "testcommsite",
"Url": homeSite,
"VivaConnectionsDefaultStart": false,
"WebId": "256c4f0f-e372-47b4-a891-b4888e829e20"
};

const homeSiteConfig = {
"Audiences": [
{
"Email": "[email protected]",
"Id": "af8c0bc8-7b1b-44b4-b087-ffcc8df70d16",
"Title": "SharingTest Members"
}
],
"IsInDraftMode": true,
"IsVivaBackendSite": false,
"SiteId": "ca49054c-85f3-41eb-a290-46ffda8f219c",
"TargetedLicenseType": 0,
"Title": "testcommsite",
"Url": "https://contoso.sharepoint.com/sites/testcomms",
"VivaConnectionsDefaultStart": false,
"WebId": "256c4f0f-e372-47b4-a891-b4888e829e20"
};

before(() => {
sinon.stub(auth, 'restoreAuth').resolves();
sinon.stub(telemetry, 'trackEvent').returns();
sinon.stub(pid, 'getProcessName').returns('');
sinon.stub(session, 'getId').returns('');
auth.connection.active = true;
auth.connection.spoUrl = 'https://contoso.sharepoint.com';
});

beforeEach(() => {
log = [];
logger = {
log: async (msg: string) => {
log.push(msg);
},
logRaw: async (msg: string) => {
log.push(msg);
},
logToStderr: async (msg: string) => {
log.push(msg);
}
};
loggerLogSpy = sinon.spy(logger, 'log');
commandInfo = cli.getCommandInfo(command);
});

afterEach(() => {
sinonUtil.restore([
request.post
]);
});

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

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

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

it('correctly logs command response', async () => {
sinon.stub(request, 'post').callsFake(async (opts) => {
if (opts.url === `https://contoso-admin.sharepoint.com/_api/SPHSite/AddHomeSite`) {
return homeSites;
}

throw opts.url;
});

await command.action(logger, { options: { url: homeSite, verbose: true } });
assert(loggerLogSpy.calledWith(homeSites));
});

it('adds a home site with the specified URL, isInDraftMode, vivaConnectionsDefaultStart, and audiences', async () => {
sinon.stub(request, 'post').callsFake(async (opts) => {
if (opts.url === `https://contoso-admin.sharepoint.com/_api/SPHSite/AddHomeSite`) {
return homeSiteConfig;
}

throw 'Invalid request';
});

await command.action(logger, {
options: {
url: homeSite,
isInDraftMode: 'true',
vivaConnectionsDefaultStart: 'false',
audiences: 'af8c0bc8-7b1b-44b4-b087-ffcc8df70d16,754ff15c-76b1-44cb-88c7-0065a4d3cfb7',
order: 2
}
});
assert(loggerLogSpy.calledWith(homeSiteConfig));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of checking just this, let's check if we actually provided the right arguments in our request to SharePoint. An example can be found here:

await command.action(logger, { options: { siteUrl: siteUrl, groupName: groupName, asAdmin: true } });
assert.deepStrictEqual(postStub.lastCall.args[0].data, {
secondaryAdministratorsFieldsData: {
siteId: siteId, secondaryAdministratorLoginNames:
['i:0#.f|membership|[email protected]', 'i:0#.f|membership|[email protected]', `c:0t.c|tenant|${groupId}`]
}
});

});

it('correctly handles invalid GUID in audiences', async () => {
const result = await command.validate({
options: {
url: homeSite,
audiences: "invalidGuid,af8c0bc8-7b1b-44b4-b087-ffcc8df70d16"
}
}, commandInfo);
assert.strictEqual(result, `invalidGuid is not a valid GUID`);
});

it('correctly handles non-integer order', async () => {
const result = await command.validate({
options: {
url: homeSite,
order: 'invalid-order'
}
}, commandInfo);
assert.strictEqual(result, 'Order must be an integer');
});

it('fails validation if the url is not a valid SharePoint url', async () => {
const actual = await command.validate({
options: {
url: "test"
}
}, commandInfo);
assert.notStrictEqual(actual, true);
});

it('passes validation with URL', async () => {
const actual = await command.validate({
options: {
url: homeSite
}
}, commandInfo);
assert.strictEqual(actual, true);
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's add a similar test with all possible options.


it('correctly handles OData error when adding a home site', async () => {
sinon.stub(request, 'post').rejects({ error: { 'odata.error': { message: { value: 'An error has occurred' } } } });

await assert.rejects(command.action(logger, { options: { url: 'https://....' } } as any), new CommandError('An error has occurred'));
});
});
Loading