Skip to content

Adds command 'spe container add' #6688

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

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
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
128 changes: 128 additions & 0 deletions docs/docs/cmd/spe/container/container-add.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import Global from '/docs/cmd/_global.mdx';
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';

# spe container add

Creates a new container

## Usage

```sh
m365 spe container add [options]
```

## Options

```md definition-list
`-n, --name <name>`
: The display name of the new container.

`-d, --description [description]`
: The description of the new container.

`--containerTypeId [containerTypeId]`
: The container type ID of the container instance. Use either `containerTypeId`, or `containerTypeName` but not both.

`--containerTypeName [containerTypeName]`
: The container type name of the container instance. Use either `containerTypeId`, or `containerTypeName` but not both.

`--ocrEnabled [ocrEnabled]`
: Indicates whether Optical Character Recognition (OCR) is enabled for the container. Possible values: `true`, `false`. Defaults to `false`.

`--itemMajorVersionLimit [itemMajorVersionLimit]`
: The maximum major versions allowed for items in the container. Defaults to `500`.

`--itemVersioningEnabled [itemVersioningEnabled]`
: Indicates whether versioning is enabled for items in the container. Possible values: `true`, `false`. Defaults to `true`.
```

<Global />

## Examples

Creates a new container by specifying the container type ID

```sh
m365 spe container add --name Invoices --containerTypeId bba89883-47c2-455b-956b-7a3d8db007fb
```

Creates a new container by specifying the container type name

```sh
m365 spe container add --name Invoices --containerTypeName "Invoice app container type"
```

Creates a new container with additional options

```sh
m365 spe container add --name Invoices --containerTypeId bba89883-47c2-455b-956b-7a3d8db007fb --ocrEnabled true --itemMajorVersionLimit 200 --itemVersioningEnabled true
```

## Response

<Tabs>
<TabItem value="JSON">

```json
{
"id": "b!ISJs1WRro0y0EWgkUYcktDa0mE8zSlFEqFzqRn70Zwp1CEtDEBZgQICPkRbil_5Z",
"displayName": "Invoices",
"description": "This container is used to store invoices",
"containerTypeId": "bfdd048e-e03f-47d2-bd16-dbbc27281aa3",
"status": "inactive",
"createdDateTime": "2025-04-15T13:31:09.62Z",
"lockState": "unlocked",
"settings": {
"isOcrEnabled": false,
"itemMajorVersionLimit": 500,
"isItemVersioningEnabled": true
}
}
```

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

```text
containerTypeId: bfdd048e-e03f-47d2-bd16-dbbc27281aa3
createdDateTime: 2025-04-15T15:14:03.89Z
description : This container is used to store invoices
displayName : Invoices
id : b!ISJs1WRro0y0EWgkUYcktDa0mE8zSlFEqFzqRn70Zwp1CEtDEBZgQICPkRbil_5Z
lockState : unlocked
settings : {"isOcrEnabled":false,"itemMajorVersionLimit":500,"isItemVersioningEnabled":true}
status : inactive
```

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

```csv
id,displayName,description,containerTypeId,status,createdDateTime,lockState
b!ISJs1WRro0y0EWgkUYcktDa0mE8zSlFEqFzqRn70Zwp1CEtDEBZgQICPkRbil_5Z,Invoices,This container is used to store invoices,bfdd048e-e03f-47d2-bd16-dbbc27281aa3,inactive,2025-04-15T15:14:45.317Z,unlocked
```

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

```md
# spe container add --name "Invoices" --containerTypeId "bfdd048e-e03f-47d2-bd16-dbbc27281aa3" --description "This container is used to store invoices"

Date: 15/04/2025

## Invoices (b!ISJs1WRro0y0EWgkUYcktDa0mE8zSlFEqFzqRn70Zwp1CEtDEBZgQICPkRbil_5Z)

Property | Value
---------|-------
id | b!ISJs1WRro0y0EWgkUYcktDa0mE8zSlFEqFzqRn70Zwp1CEtDEBZgQICPkRbil_5Z
displayName | Invoices
description | This container is used to store invoices
containerTypeId | bfdd048e-e03f-47d2-bd16-dbbc27281aa3
status | inactive
createdDateTime | 2025-04-15T15:15:19.123Z
lockState | unlocked
```

</TabItem>
</Tabs>
5 changes: 5 additions & 0 deletions docs/src/config/sidebars.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2108,6 +2108,11 @@ const sidebars: SidebarsConfig = {
label: 'container activate',
id: 'cmd/spe/container/container-activate'
},
{
type: 'doc',
label: 'container add',
id: 'cmd/spe/container/container-add'
},
{
type: 'doc',
label: 'container get',
Expand Down
1 change: 1 addition & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export default {
'https://graph.microsoft.com/Directory.ReadWrite.All',
'https://graph.microsoft.com/ExternalConnection.ReadWrite.All',
'https://graph.microsoft.com/ExternalItem.ReadWrite.All',
'https://graph.microsoft.com/FileStorageContainer.Selected',
'https://graph.microsoft.com/Group.ReadWrite.All',
'https://graph.microsoft.com/IdentityProvider.ReadWrite.All',
'https://graph.microsoft.com/InformationProtectionPolicy.Read',
Expand Down
1 change: 1 addition & 0 deletions src/m365/spe/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ const prefix: string = 'spe';

export default {
CONTAINER_ACTIVATE: `${prefix} container activate`,
CONTAINER_ADD: `${prefix} container add`,
CONTAINER_GET: `${prefix} container get`,
CONTAINER_LIST: `${prefix} container list`,
CONTAINERTYPE_ADD: `${prefix} containertype add`,
Expand Down
205 changes: 205 additions & 0 deletions src/m365/spe/commands/container/container-add.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
import assert from 'assert';
import sinon from 'sinon';
import auth from '../../../../Auth.js';
import { CommandInfo } from "../../../../cli/CommandInfo.js";
import { Logger } from '../../../../cli/Logger.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 { cli } from '../../../../cli/cli.js';
import commands from '../../commands.js';
import command from './container-add.js';
import { spo } from '../../../../utils/spo.js';
import { z } from 'zod';
import { CommandError } from '../../../../Command.js';
import { spe } from '../../../../utils/spe.js';

describe(commands.CONTAINER_ADD, () => {
const spoAdminUrl = 'https://contoso-admin.sharepoint.com';
const containerTypeId = 'c6f08d91-77fa-485f-9369-f246ec0fc19c';
const containerTypeName = 'Container type name';
const containerName = 'Invoices';

const requestResponse = {
id: 'b!ISJs1WRro0y0EWgkUYcktDa0mE8zSlFEqFzqRn70Zwp1CEtDEBZgQICPkRbil_5Z',
displayName: containerName,
description: 'Description of My Application Storage Container',
containerTypeId: containerTypeId,
status: 'inactive',
createdDateTime: '2025-04-15T13:31:09.62Z',
lockState: 'unlocked',
settings: {
isOcrEnabled: false,
itemMajorVersionLimit: 500,
isItemVersioningEnabled: true
}
};

let log: string[];
let logger: Logger;
let loggerLogSpy: sinon.SinonSpy;
let commandInfo: CommandInfo;
let commandOptionsSchema: z.ZodTypeAny;

before(() => {
sinon.stub(auth, 'restoreAuth').resolves();
sinon.stub(telemetry, 'trackEvent').resolves();
sinon.stub(pid, 'getProcessName').returns('');
sinon.stub(session, 'getId').returns('');

sinon.stub(spe, 'getContainerTypeIdByName').resolves(containerTypeId);

auth.connection.active = true;
auth.connection.spoUrl = spoAdminUrl.replace('-admin.sharepoint.com', '.sharepoint.com');
commandInfo = cli.getCommandInfo(command);
commandOptionsSchema = commandInfo.command.getSchemaToParse()!;
});

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');
});

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.CONTAINER_ADD);
});

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

it('fails validation if both containerTypeId and containerTypeName options are passed', async () => {
const actual = commandOptionsSchema.safeParse({ name: containerName, containerTypeId: containerTypeId, containerTypeName: containerTypeName });
assert.strictEqual(actual.success, false);
});

it('fails validation if neither containerTypeId nor containerTypeName options are passed', async () => {
const actual = commandOptionsSchema.safeParse({ name: containerName });
assert.strictEqual(actual.success, false);
});

it('fails validation if containerTypeId is not a valid GUID', async () => {
const actual = commandOptionsSchema.safeParse({ name: containerName, containerTypeId: 'invalid' });
assert.strictEqual(actual.success, false);
});

it('passes validation if containerTypeId is a valid GUID', async () => {
const actual = commandOptionsSchema.safeParse({ name: containerName, containerTypeId: containerTypeId });
assert.strictEqual(actual.success, true);
});

it('fails validation if itemMajorVersionLimit is not a positive integer', async () => {
const actual = commandOptionsSchema.safeParse({ name: containerName, itemMajorVersionLimit: 12.5 });
assert.strictEqual(actual.success, false);
});

it('correctly logs an output', async () => {
sinon.stub(request, 'post').callsFake(async (opts) => {
if (opts.url === 'https://graph.microsoft.com/v1.0/storage/fileStorage/containers') {
return requestResponse;
}

throw 'Invalid POST request: ' + opts.url;
});

await command.action(logger, { options: { name: containerName, containerTypeId: containerTypeId } });
assert(loggerLogSpy.calledOnceWith(requestResponse));
});

it('correctly creates a new container with containerTypeId', async () => {
const postStub = sinon.stub(request, 'post').callsFake(async (opts) => {
if (opts.url === 'https://graph.microsoft.com/v1.0/storage/fileStorage/containers') {
return requestResponse;
}

throw 'Invalid POST request: ' + opts.url;
});

await command.action(logger, { options: { name: containerName, description: 'Lorem ipsum', ocrEnabled: true, itemMajorVersionLimit: 250, itemVersioningEnabled: true, containerTypeId: containerTypeId } });
assert.deepStrictEqual(postStub.lastCall.args[0].data, {
displayName: containerName,
description: 'Lorem ipsum',
containerTypeId: containerTypeId,
settings: {
isOcrEnabled: true,
itemMajorVersionLimit: 250,
isItemVersioningEnabled: true
}
});
});

it('correctly creates a new container with containerTypeName', async () => {
const postStub = sinon.stub(request, 'post').callsFake(async (opts) => {
if (opts.url === 'https://graph.microsoft.com/v1.0/storage/fileStorage/containers') {
return requestResponse;
}

throw 'Invalid POST request: ' + opts.url;
});

sinon.stub(spo, 'getAllContainerTypes').resolves([
{
AzureSubscriptionId: '/Guid(f08575e2-36c4-407f-a891-eabae23f66bc)/',
ContainerTypeId: `/Guid(${containerTypeId})/`,
CreationDate: '3/11/2024 2:38:56 PM',
DisplayName: containerTypeName,
ExpiryDate: '3/11/2028 2:38:56 PM',
IsBillingProfileRequired: true,
OwningAppId: '/Guid(1b3b8660-9a44-4a7c-9c02-657f3ff5d5ac)/',
OwningTenantId: '/Guid(e1dd4023-a656-480a-8a0e-c1b1eec51e1d)/',
Region: 'West Europe',
ResourceGroup: 'Standard group',
SPContainerTypeBillingClassification: 'Standard'
}
]);

await command.action(logger, { options: { name: containerName, description: 'Lorem ipsum', ocrEnabled: true, itemMajorVersionLimit: 250, itemVersioningEnabled: true, containerTypeName: containerTypeName, verbose: true } });
assert.deepStrictEqual(postStub.lastCall.args[0].data, {
displayName: containerName,
description: 'Lorem ipsum',
containerTypeId: containerTypeId,
settings: {
isOcrEnabled: true,
itemMajorVersionLimit: 250,
isItemVersioningEnabled: true
}
});
});

it('correctly handles error', async () => {
sinon.stub(request, 'post').rejects({
error: {
code: 'accessDenied',
message: 'Access denied'
}
});

await assert.rejects(command.action(logger, { options: { name: containerName, containerTypeId: containerTypeId } }),
new CommandError('Access denied'));
});
});
Loading