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
13 changes: 11 additions & 2 deletions docs/docs/cmd/spo/storageentity/storageentity-get.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -17,22 +17,31 @@ m365 spo storageentity get [options]
```md definition-list
`-k, --key <key>`
: Name of the tenant property to retrieve

`-u, --appCatalogUrl [appCatalogUrl]`
: URL of the app catalog site. If not specified, the tenant app catalog URL will be used.
```

<Global />

## Remarks

Tenant properties are stored in the app catalog site associated with the site to which you are currently connected. When retrieving the specified tenant property, SharePoint will automatically find the associated app catalog and try to retrieve the property from it.
Tenant properties are stored in the app catalog site associated with the site to which you are currently connected. When retrieving the specified tenant property, SharePoint will use the specified app catalog URL or automatically resolve the tenant app catalog URL.

## Examples

Show the value, description and comment of the _AnalyticsId_ tenant property
Show the value, description and comment of the _AnalyticsId_ tenant property using the tenant app catalog

```sh
m365 spo storageentity get -k AnalyticsId
```

Show the value, description and comment of the _AnalyticsId_ tenant property from a specific app catalog site

```sh
m365 spo storageentity get -k AnalyticsId -u https://contoso.sharepoint.com/sites/appcatalog
```

## Response

<Tabs>
Expand Down
12 changes: 9 additions & 3 deletions docs/docs/cmd/spo/storageentity/storageentity-list.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,24 @@ m365 spo storageentity list [options]
## Options

```md definition-list
`-u, --appCatalogUrl <appCatalogUrl>`
: URL of the app catalog site
`-u, --appCatalogUrl [appCatalogUrl]`
: URL of the app catalog site. If not specified, the tenant app catalog URL will be used.
```

<Global />

## Remarks

Tenant properties are stored in the app catalog site. To list all tenant properties, you have to specify the absolute URL of the app catalog site. If you specify an incorrect URL, or the site at the given URL is not an app catalog site, no properties will be retrieved.
Tenant properties are stored in the app catalog site. To list all tenant properties, you can specify the absolute URL of the app catalog site. If not specified, the command will automatically resolve the tenant app catalog URL. If you specify an incorrect URL, or the site at the given URL is not an app catalog site, no properties will be retrieved.

## Examples

List all tenant properties using the tenant app catalog

```sh
m365 spo storageentity list
```

List all tenant properties stored in the _https://contoso.sharepoint.com/sites/appcatalog_ app catalog site

```sh
Expand Down
12 changes: 6 additions & 6 deletions docs/docs/cmd/spo/storageentity/storageentity-remove.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ m365 spo storageentity remove [options]
## Options

```md definition-list
`-u, --appCatalogUrl <appCatalogUrl>`
: URL of the app catalog site
`-u, --appCatalogUrl [appCatalogUrl]`
: URL of the app catalog site. If not specified, the tenant app catalog URL will be used.

`-k, --key <key>`
: Name of the tenant property to retrieve
Expand All @@ -27,7 +27,7 @@ m365 spo storageentity remove [options]

## Remarks

Tenant properties are stored in the app catalog site associated with that tenant. To remove a property, you have to specify the absolute URL of the app catalog site. If you specify the URL of a site different than the app catalog, you will get an access denied error.
Tenant properties are stored in the app catalog site associated with that tenant. To remove a property, you can specify the absolute URL of the app catalog site. If not specified, the command will automatically resolve the tenant app catalog URL. If you specify the URL of a site different than the app catalog, you will get an access denied error.

:::info

Expand All @@ -37,13 +37,13 @@ To use this command you must be either **SharePoint Administrator** or **Global

## Examples

Remove the _AnalyticsId_ tenant property. Yields a confirmation prompt before actually removing the property
Remove the _AnalyticsId_ tenant property using the tenant app catalog. Yields a confirmation prompt before actually removing the property

```sh
m365 spo storageentity remove -k AnalyticsId -u https://contoso.sharepoint.com/sites/appcatalog
m365 spo storageentity remove -k AnalyticsId
```

Remove the _AnalyticsId_ tenant property. Suppresses the confirmation prompt
Remove the _AnalyticsId_ tenant property from a specific app catalog. Suppresses the confirmation prompt

```sh
m365 spo storageentity remove -k AnalyticsId --force -u https://contoso.sharepoint.com/sites/appcatalog
Expand Down
14 changes: 10 additions & 4 deletions docs/docs/cmd/spo/storageentity/storageentity-set.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ m365 spo storageentity set [options]
## Options

```md definition-list
`-u, --appCatalogUrl <appCatalogUrl>`
: URL of the app catalog site
`-u, --appCatalogUrl [appCatalogUrl]`
: URL of the app catalog site. If not specified, the tenant app catalog URL will be used.

`-k, --key <key>`
: Name of the tenant property to retrieve
Expand All @@ -33,7 +33,7 @@ m365 spo storageentity set [options]

## Remarks

Tenant properties are stored in the app catalog site associated with that tenant. To set a property, you have to specify the absolute URL of the app catalog site without a trailing slash. If you specify the URL with trailing slash you get the error `The managed path sites/apps is not a managed path in this tenant.`
Tenant properties are stored in the app catalog site associated with that tenant. To set a property, you can specify the absolute URL of the app catalog site without a trailing slash. If not specified, the command will automatically resolve the tenant app catalog URL. If you specify the URL with trailing slash you get the error `The managed path sites/apps is not a managed path in this tenant.`

If you specify the URL of a site different than the app catalog, you will get an access denied error.

Expand All @@ -45,7 +45,13 @@ To use this command you must be either **SharePoint Administrator** or **Global

## Examples

Set _123_ as the value of the _AnalyticsId_ tenant property. Also include a description and a comment for additional clarification of the usage of the property.
Set _123_ as the value of the _AnalyticsId_ tenant property using the tenant app catalog

```sh
m365 spo storageentity set -k AnalyticsId -v 123 -d 'Web analytics ID' -c 'Use on all sites'
```

Set _123_ as the value of the _AnalyticsId_ tenant property on a specific app catalog site

```sh
m365 spo storageentity set -k AnalyticsId -v 123 -d 'Web analytics ID' -c 'Use on all sites' -u https://contoso.sharepoint.com/sites/appcatalog
Expand Down
112 changes: 64 additions & 48 deletions src/m365/spo/commands/storageentity/storageentity-get.spec.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,25 @@
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 { pid } from '../../../../utils/pid.js';
import { session } from '../../../../utils/session.js';
import { sinonUtil } from '../../../../utils/sinonUtil.js';
import { spo } from '../../../../utils/spo.js';
import commands from '../../commands.js';
import command from './storageentity-get.js';
import command, { options } from './storageentity-get.js';

describe(commands.STORAGEENTITY_GET, () => {
let log: string[];
let logger: Logger;
let loggerLogSpy: sinon.SinonSpy;
let commandInfo: CommandInfo;
let commandOptionsSchema: typeof options;

before(() => {
sinon.stub(auth, 'restoreAuth').resolves();
Expand All @@ -23,46 +28,28 @@ describe(commands.STORAGEENTITY_GET, () => {
sinon.stub(session, 'getId').returns('');
auth.connection.active = true;
auth.connection.spoUrl = 'https://contoso.sharepoint.com';
commandInfo = cli.getCommandInfo(command);
commandOptionsSchema = commandInfo.command.getSchemaToParse() as typeof options;

sinon.stub(request, 'get').callsFake(async (opts) => {
if ((opts.url as string).indexOf(`/_api/web/GetStorageEntity('existingproperty')`) > -1) {
if (opts.headers &&
opts.headers.accept &&
(opts.headers.accept as string).indexOf('application/json') === 0) {
return { Comment: 'Lorem', Description: 'ipsum', Value: 'dolor' };
}
if (opts.url === `https://contoso.sharepoint.com/sites/appcatalog/_api/web/GetStorageEntity('existingproperty')`) {
return { Comment: 'Lorem', Description: 'ipsum', Value: 'dolor' };
}

if ((opts.url as string).indexOf(`/_api/web/GetStorageEntity('propertywithoutdescription')`) > -1) {
if (opts.headers &&
opts.headers.accept &&
(opts.headers.accept as string).indexOf('application/json') === 0) {
return { Comment: 'Lorem', Value: 'dolor' };
}
if (opts.url === `https://contoso.sharepoint.com/sites/appcatalog/_api/web/GetStorageEntity('propertywithoutdescription')`) {
return { Comment: 'Lorem', Value: 'dolor' };
}

if ((opts.url as string).indexOf(`/_api/web/GetStorageEntity('propertywithoutcomments')`) > -1) {
if (opts.headers &&
opts.headers.accept &&
(opts.headers.accept as string).indexOf('application/json') === 0) {
return { Description: 'ipsum', Value: 'dolor' };
}
if (opts.url === `https://contoso.sharepoint.com/sites/appcatalog/_api/web/GetStorageEntity('propertywithoutcomments')`) {
return { Description: 'ipsum', Value: 'dolor' };
}

if ((opts.url as string).indexOf(`/_api/web/GetStorageEntity('nonexistingproperty')`) > -1) {
if (opts.headers &&
opts.headers.accept &&
(opts.headers.accept as string).indexOf('application/json') === 0) {
return { "odata.null": true };
}
if (opts.url === `https://contoso.sharepoint.com/sites/appcatalog/_api/web/GetStorageEntity('nonexistingproperty')`) {
return { "odata.null": true };
}

if ((opts.url as string).indexOf(`/_api/web/GetStorageEntity('%23myprop')`) > -1) {
if (opts.headers &&
opts.headers.accept &&
(opts.headers.accept as string).indexOf('application/json') === 0) {
return { Description: 'ipsum', Value: 'dolor' };
}
if (opts.url === `https://contoso.sharepoint.com/sites/appcatalog/_api/web/GetStorageEntity('%23myprop')`) {
return { Description: 'ipsum', Value: 'dolor' };
}

throw 'Invalid request';
Expand All @@ -85,6 +72,12 @@ describe(commands.STORAGEENTITY_GET, () => {
loggerLogSpy = sinon.spy(logger, 'log');
});

afterEach(() => {
sinonUtil.restore([
spo.getTenantAppCatalogUrl
]);
});

after(() => {
sinon.restore();
auth.connection.active = false;
Expand All @@ -100,7 +93,7 @@ describe(commands.STORAGEENTITY_GET, () => {
});

it('retrieves the details of an existing tenant property', async () => {
await command.action(logger, { options: { debug: true, key: 'existingproperty', appCatalogUrl: 'https://contoso.sharepoint.com/sites/appcatalog' } });
await command.action(logger, { options: commandOptionsSchema.parse({ debug: true, key: 'existingproperty', appCatalogUrl: 'https://contoso.sharepoint.com/sites/appcatalog' }) });
assert(loggerLogSpy.calledWith({
Key: 'existingproperty',
Value: 'dolor',
Expand All @@ -110,7 +103,7 @@ describe(commands.STORAGEENTITY_GET, () => {
});

it('retrieves the details of an existing tenant property without a description', async () => {
await command.action(logger, { options: { debug: true, key: 'propertywithoutdescription', appCatalogUrl: 'https://contoso.sharepoint.com/sites/appcatalog' } });
await command.action(logger, { options: commandOptionsSchema.parse({ debug: true, key: 'propertywithoutdescription', appCatalogUrl: 'https://contoso.sharepoint.com/sites/appcatalog' }) });
assert(loggerLogSpy.calledWith({
Key: 'propertywithoutdescription',
Value: 'dolor',
Expand All @@ -120,7 +113,7 @@ describe(commands.STORAGEENTITY_GET, () => {
});

it('retrieves the details of an existing tenant property without a comment', async () => {
await command.action(logger, { options: { key: 'propertywithoutcomments', appCatalogUrl: 'https://contoso.sharepoint.com/sites/appcatalog' } });
await command.action(logger, { options: commandOptionsSchema.parse({ key: 'propertywithoutcomments', appCatalogUrl: 'https://contoso.sharepoint.com/sites/appcatalog' }) });
assert(loggerLogSpy.calledWith({
Key: 'propertywithoutcomments',
Value: 'dolor',
Expand All @@ -129,25 +122,44 @@ describe(commands.STORAGEENTITY_GET, () => {
}));
});

it('retrieves tenant property using tenant app catalog URL when appCatalogUrl is not specified', async () => {
sinon.stub(spo, 'getTenantAppCatalogUrl').resolves('https://contoso.sharepoint.com/sites/appcatalog');

await command.action(logger, { options: commandOptionsSchema.parse({ key: 'existingproperty' }) });
assert(loggerLogSpy.calledWith({
Key: 'existingproperty',
Value: 'dolor',
Description: 'ipsum',
Comment: 'Lorem'
}));
});

it('throws error when tenant app catalog is not found and appCatalogUrl is not specified', async () => {
sinon.stub(spo, 'getTenantAppCatalogUrl').resolves(null);

await assert.rejects(command.action(logger, { options: commandOptionsSchema.parse({ key: 'existingproperty' }) }),
new CommandError('Tenant app catalog URL not found. Specify the URL of the app catalog site using the appCatalogUrl option.'));
});

it('handles a non-existent tenant property', async () => {
await command.action(logger, { options: { key: 'nonexistingproperty', appCatalogUrl: 'https://contoso.sharepoint.com/sites/appcatalog' } });
await command.action(logger, { options: commandOptionsSchema.parse({ key: 'nonexistingproperty', appCatalogUrl: 'https://contoso.sharepoint.com/sites/appcatalog' }) });
});

it('handles a non-existent tenant property (debug)', async () => {
await command.action(logger, { options: { debug: true, key: 'nonexistingproperty', appCatalogUrl: 'https://contoso.sharepoint.com/sites/appcatalog' } });
await command.action(logger, { options: commandOptionsSchema.parse({ debug: true, key: 'nonexistingproperty', appCatalogUrl: 'https://contoso.sharepoint.com/sites/appcatalog' }) });
let correctValue: boolean = false;
log.forEach(l => {
if (l &&
typeof l === 'string' &&
l.indexOf('Property with key nonexistingproperty not found') > -1) {
l.includes('Property with key nonexistingproperty not found')) {
correctValue = true;
}
});
assert(correctValue);
});

it('escapes special characters in property name', async () => {
await command.action(logger, { options: { debug: true, key: '#myprop', appCatalogUrl: 'https://contoso.sharepoint.com/sites/appcatalog' } });
await command.action(logger, { options: commandOptionsSchema.parse({ debug: true, key: '#myprop', appCatalogUrl: 'https://contoso.sharepoint.com/sites/appcatalog' }) });
assert(loggerLogSpy.calledWith({
Key: '#myprop',
Value: 'dolor',
Expand All @@ -156,21 +168,25 @@ describe(commands.STORAGEENTITY_GET, () => {
}));
});

it('requires tenant property name', () => {
const options = command.options;
let requiresTenantPropertyName = false;
options.forEach(o => {
if (o.option.indexOf('<key>') > -1) {
requiresTenantPropertyName = true;
}
});
assert(requiresTenantPropertyName);
it('fails validation if appCatalogUrl is not a valid URL', () => {
const actual = commandOptionsSchema.safeParse({ key: 'prop', appCatalogUrl: 'foo' });
assert.strictEqual(actual.success, false);
});

it('passes validation when appCatalogUrl is a valid SharePoint URL', () => {
const actual = commandOptionsSchema.safeParse({ key: 'prop', appCatalogUrl: 'https://contoso.sharepoint.com/sites/appcatalog' });
assert.strictEqual(actual.success, true);
});

it('passes validation when appCatalogUrl is not specified', () => {
const actual = commandOptionsSchema.safeParse({ key: 'prop' });
assert.strictEqual(actual.success, true);
});

it('handles promise rejection', async () => {
sinonUtil.restore(request.get);
sinon.stub(request, 'get').rejects(new Error('error'));

await assert.rejects(command.action(logger, { options: { debug: true, key: '#myprop' } } as any), new CommandError('error'));
await assert.rejects(command.action(logger, { options: commandOptionsSchema.parse({ debug: true, key: '#myprop', appCatalogUrl: 'https://contoso.sharepoint.com/sites/appcatalog' }) }), new CommandError('error'));
});
});
Loading