Skip to content

Commit 3c469e8

Browse files
committed
Adds command 'spe container add'. Closes #6081
1 parent add4854 commit 3c469e8

File tree

10 files changed

+782
-0
lines changed

10 files changed

+782
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import Global from '/docs/cmd/_global.mdx';
2+
import Tabs from '@theme/Tabs';
3+
import TabItem from '@theme/TabItem';
4+
5+
# spe container add
6+
7+
Creates a new container
8+
9+
## Usage
10+
11+
```sh
12+
m365 spe container add [options]
13+
```
14+
15+
## Options
16+
17+
```md definition-list
18+
`-n, --name <name>`
19+
: The display name of the new container.
20+
21+
`-d, --description [description]`
22+
: The description of the new container.
23+
24+
`--containerTypeId [containerTypeId]`
25+
: The container type ID of the container instance. Use either `containerTypeId`, or `containerTypeName` but not both.
26+
27+
`--containerTypeName [containerTypeName]`
28+
: The container type name of the container instance. Use either `containerTypeId`, or `containerTypeName` but not both.
29+
30+
`--ocrEnabled [ocrEnabled]`
31+
: Indicates whether Optical Character Recognition (OCR) is enabled for the container. Possible values: `true`, `false`. Defaults to `false`.
32+
33+
`--itemMajorVersionLimit [itemMajorVersionLimit]`
34+
: The maximum major versions allowed for items in the container. Defaults to `500`.
35+
36+
`--itemVersioningEnabled [itemVersioningEnabled]`
37+
: Indicates whether versioning is enabled for items in the container. Possible values: `true`, `false`. Defaults to `true`.
38+
```
39+
40+
<Global />
41+
42+
## Examples
43+
44+
Creates a new container by specifying the container type ID
45+
46+
```sh
47+
m365 spe container add --name Invoices --containerTypeId bba89883-47c2-455b-956b-7a3d8db007fb
48+
```
49+
50+
Creates a new container by specifying the container type name
51+
52+
```sh
53+
m365 spe container add --name Invoices --containerTypeName "Invoice app container type"
54+
```
55+
56+
Creates a new container with additional options
57+
58+
```sh
59+
m365 spe container add --name Invoices --containerTypeId bba89883-47c2-455b-956b-7a3d8db007fb --ocrEnabled true --itemMajorVersionLimit 200 --itemVersioningEnabled true
60+
```
61+
62+
## Response
63+
64+
<Tabs>
65+
<TabItem value="JSON">
66+
67+
```json
68+
{
69+
"id": "b!ISJs1WRro0y0EWgkUYcktDa0mE8zSlFEqFzqRn70Zwp1CEtDEBZgQICPkRbil_5Z",
70+
"displayName": "Invoices",
71+
"description": "This container is used to store invoices",
72+
"containerTypeId": "bfdd048e-e03f-47d2-bd16-dbbc27281aa3",
73+
"status": "inactive",
74+
"createdDateTime": "2025-04-15T13:31:09.62Z",
75+
"lockState": "unlocked",
76+
"settings": {
77+
"isOcrEnabled": false,
78+
"itemMajorVersionLimit": 500,
79+
"isItemVersioningEnabled": true
80+
}
81+
}
82+
```
83+
84+
</TabItem>
85+
<TabItem value="Text">
86+
87+
```text
88+
containerTypeId: bfdd048e-e03f-47d2-bd16-dbbc27281aa3
89+
createdDateTime: 2025-04-15T15:14:03.89Z
90+
description : This container is used to store invoices
91+
displayName : Invoices
92+
id : b!ISJs1WRro0y0EWgkUYcktDa0mE8zSlFEqFzqRn70Zwp1CEtDEBZgQICPkRbil_5Z
93+
lockState : unlocked
94+
settings : {"isOcrEnabled":false,"itemMajorVersionLimit":500,"isItemVersioningEnabled":true}
95+
status : inactive
96+
```
97+
98+
</TabItem>
99+
<TabItem value="CSV">
100+
101+
```csv
102+
id,displayName,description,containerTypeId,status,createdDateTime,lockState
103+
b!ISJs1WRro0y0EWgkUYcktDa0mE8zSlFEqFzqRn70Zwp1CEtDEBZgQICPkRbil_5Z,Invoices,This container is used to store invoices,bfdd048e-e03f-47d2-bd16-dbbc27281aa3,inactive,2025-04-15T15:14:45.317Z,unlocked
104+
```
105+
106+
</TabItem>
107+
<TabItem value="Markdown">
108+
109+
```md
110+
# spe container add --name "Invoices" --containerTypeId "bfdd048e-e03f-47d2-bd16-dbbc27281aa3" --description "This container is used to store invoices"
111+
112+
Date: 15/04/2025
113+
114+
## Invoices (b!ISJs1WRro0y0EWgkUYcktDa0mE8zSlFEqFzqRn70Zwp1CEtDEBZgQICPkRbil_5Z)
115+
116+
Property | Value
117+
---------|-------
118+
id | b!ISJs1WRro0y0EWgkUYcktDa0mE8zSlFEqFzqRn70Zwp1CEtDEBZgQICPkRbil_5Z
119+
displayName | Invoices
120+
description | This container is used to store invoices
121+
containerTypeId | bfdd048e-e03f-47d2-bd16-dbbc27281aa3
122+
status | inactive
123+
createdDateTime | 2025-04-15T15:15:19.123Z
124+
lockState | unlocked
125+
```
126+
127+
</TabItem>
128+
</Tabs>

docs/src/config/sidebars.ts

+5
Original file line numberDiff line numberDiff line change
@@ -2108,6 +2108,11 @@ const sidebars: SidebarsConfig = {
21082108
label: 'container activate',
21092109
id: 'cmd/spe/container/container-activate'
21102110
},
2111+
{
2112+
type: 'doc',
2113+
label: 'container add',
2114+
id: 'cmd/spe/container/container-add'
2115+
},
21112116
{
21122117
type: 'doc',
21132118
label: 'container get',

src/config.ts

+1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export default {
1919
'https://graph.microsoft.com/Directory.ReadWrite.All',
2020
'https://graph.microsoft.com/ExternalConnection.ReadWrite.All',
2121
'https://graph.microsoft.com/ExternalItem.ReadWrite.All',
22+
'https://graph.microsoft.com/FileStorageContainer.Selected',
2223
'https://graph.microsoft.com/Group.ReadWrite.All',
2324
'https://graph.microsoft.com/IdentityProvider.ReadWrite.All',
2425
'https://graph.microsoft.com/InformationProtectionPolicy.Read',

src/m365/spe/commands.ts

+1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ const prefix: string = 'spe';
22

33
export default {
44
CONTAINER_ACTIVATE: `${prefix} container activate`,
5+
CONTAINER_ADD: `${prefix} container add`,
56
CONTAINER_GET: `${prefix} container get`,
67
CONTAINER_LIST: `${prefix} container list`,
78
CONTAINERTYPE_ADD: `${prefix} containertype add`,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
import assert from 'assert';
2+
import sinon from 'sinon';
3+
import auth from '../../../../Auth.js';
4+
import { CommandInfo } from "../../../../cli/CommandInfo.js";
5+
import { Logger } from '../../../../cli/Logger.js';
6+
import request from '../../../../request.js';
7+
import { telemetry } from '../../../../telemetry.js';
8+
import { pid } from '../../../../utils/pid.js';
9+
import { session } from '../../../../utils/session.js';
10+
import { sinonUtil } from '../../../../utils/sinonUtil.js';
11+
import { cli } from '../../../../cli/cli.js';
12+
import commands from '../../commands.js';
13+
import command from './container-add.js';
14+
import { spo } from '../../../../utils/spo.js';
15+
import { z } from 'zod';
16+
import { CommandError } from '../../../../Command.js';
17+
import { spe } from '../../../../utils/spe.js';
18+
19+
describe(commands.CONTAINER_ADD, () => {
20+
const spoAdminUrl = 'https://contoso-admin.sharepoint.com';
21+
const containerTypeId = 'c6f08d91-77fa-485f-9369-f246ec0fc19c';
22+
const containerTypeName = 'Container type name';
23+
const containerName = 'Invoices';
24+
25+
const requestResponse = {
26+
id: 'b!ISJs1WRro0y0EWgkUYcktDa0mE8zSlFEqFzqRn70Zwp1CEtDEBZgQICPkRbil_5Z',
27+
displayName: containerName,
28+
description: 'Description of My Application Storage Container',
29+
containerTypeId: containerTypeId,
30+
status: 'inactive',
31+
createdDateTime: '2025-04-15T13:31:09.62Z',
32+
lockState: 'unlocked',
33+
settings: {
34+
isOcrEnabled: false,
35+
itemMajorVersionLimit: 500,
36+
isItemVersioningEnabled: true
37+
}
38+
};
39+
40+
let log: string[];
41+
let logger: Logger;
42+
let loggerLogSpy: sinon.SinonSpy;
43+
let commandInfo: CommandInfo;
44+
let commandOptionsSchema: z.ZodTypeAny;
45+
46+
before(() => {
47+
sinon.stub(auth, 'restoreAuth').resolves();
48+
sinon.stub(telemetry, 'trackEvent').resolves();
49+
sinon.stub(pid, 'getProcessName').returns('');
50+
sinon.stub(session, 'getId').returns('');
51+
52+
sinon.stub(spe, 'getContainerTypeIdByName').resolves(containerTypeId);
53+
54+
auth.connection.active = true;
55+
auth.connection.spoUrl = spoAdminUrl.replace('-admin.sharepoint.com', '.sharepoint.com');
56+
commandInfo = cli.getCommandInfo(command);
57+
commandOptionsSchema = commandInfo.command.getSchemaToParse()!;
58+
});
59+
60+
beforeEach(() => {
61+
log = [];
62+
logger = {
63+
log: async (msg: string) => {
64+
log.push(msg);
65+
},
66+
logRaw: async (msg: string) => {
67+
log.push(msg);
68+
},
69+
logToStderr: async (msg: string) => {
70+
log.push(msg);
71+
}
72+
};
73+
loggerLogSpy = sinon.spy(logger, 'log');
74+
});
75+
76+
afterEach(() => {
77+
sinonUtil.restore([
78+
request.post
79+
]);
80+
});
81+
82+
after(() => {
83+
sinon.restore();
84+
auth.connection.active = false;
85+
auth.connection.spoUrl = undefined;
86+
});
87+
88+
it('has correct name', () => {
89+
assert.strictEqual(command.name, commands.CONTAINER_ADD);
90+
});
91+
92+
it('has a description', () => {
93+
assert.notStrictEqual(command.description, null);
94+
});
95+
96+
it('fails validation if both containerTypeId and containerTypeName options are passed', async () => {
97+
const actual = commandOptionsSchema.safeParse({ name: containerName, containerTypeId: containerTypeId, containerTypeName: containerTypeName });
98+
assert.strictEqual(actual.success, false);
99+
});
100+
101+
it('fails validation if neither containerTypeId nor containerTypeName options are passed', async () => {
102+
const actual = commandOptionsSchema.safeParse({ name: containerName });
103+
assert.strictEqual(actual.success, false);
104+
});
105+
106+
it('fails validation if containerTypeId is not a valid GUID', async () => {
107+
const actual = commandOptionsSchema.safeParse({ name: containerName, containerTypeId: 'invalid' });
108+
assert.strictEqual(actual.success, false);
109+
});
110+
111+
it('passes validation if containerTypeId is a valid GUID', async () => {
112+
const actual = commandOptionsSchema.safeParse({ name: containerName, containerTypeId: containerTypeId });
113+
assert.strictEqual(actual.success, true);
114+
});
115+
116+
it('fails validation if itemMajorVersionLimit is not a positive integer', async () => {
117+
const actual = commandOptionsSchema.safeParse({ name: containerName, itemMajorVersionLimit: 12.5 });
118+
assert.strictEqual(actual.success, false);
119+
});
120+
121+
it('correctly logs an output', async () => {
122+
sinon.stub(request, 'post').callsFake(async (opts) => {
123+
if (opts.url === 'https://graph.microsoft.com/v1.0/storage/fileStorage/containers') {
124+
return requestResponse;
125+
}
126+
127+
throw 'Invalid POST request: ' + opts.url;
128+
});
129+
130+
await command.action(logger, { options: { name: containerName, containerTypeId: containerTypeId } });
131+
assert(loggerLogSpy.calledOnceWith(requestResponse));
132+
});
133+
134+
it('correctly creates a new container with containerTypeId', async () => {
135+
const postStub = sinon.stub(request, 'post').callsFake(async (opts) => {
136+
if (opts.url === 'https://graph.microsoft.com/v1.0/storage/fileStorage/containers') {
137+
return requestResponse;
138+
}
139+
140+
throw 'Invalid POST request: ' + opts.url;
141+
});
142+
143+
await command.action(logger, { options: { name: containerName, description: 'Lorem ipsum', ocrEnabled: true, itemMajorVersionLimit: 250, itemVersioningEnabled: true, containerTypeId: containerTypeId } });
144+
assert.deepStrictEqual(postStub.lastCall.args[0].data, {
145+
displayName: containerName,
146+
description: 'Lorem ipsum',
147+
containerTypeId: containerTypeId,
148+
settings: {
149+
isOcrEnabled: true,
150+
itemMajorVersionLimit: 250,
151+
isItemVersioningEnabled: true
152+
}
153+
});
154+
});
155+
156+
it('correctly creates a new container with containerTypeName', async () => {
157+
const postStub = sinon.stub(request, 'post').callsFake(async (opts) => {
158+
if (opts.url === 'https://graph.microsoft.com/v1.0/storage/fileStorage/containers') {
159+
return requestResponse;
160+
}
161+
162+
throw 'Invalid POST request: ' + opts.url;
163+
});
164+
165+
sinon.stub(spo, 'getAllContainerTypes').resolves([
166+
{
167+
AzureSubscriptionId: '/Guid(f08575e2-36c4-407f-a891-eabae23f66bc)/',
168+
ContainerTypeId: `/Guid(${containerTypeId})/`,
169+
CreationDate: '3/11/2024 2:38:56 PM',
170+
DisplayName: containerTypeName,
171+
ExpiryDate: '3/11/2028 2:38:56 PM',
172+
IsBillingProfileRequired: true,
173+
OwningAppId: '/Guid(1b3b8660-9a44-4a7c-9c02-657f3ff5d5ac)/',
174+
OwningTenantId: '/Guid(e1dd4023-a656-480a-8a0e-c1b1eec51e1d)/',
175+
Region: 'West Europe',
176+
ResourceGroup: 'Standard group',
177+
SPContainerTypeBillingClassification: 'Standard'
178+
}
179+
]);
180+
181+
await command.action(logger, { options: { name: containerName, description: 'Lorem ipsum', ocrEnabled: true, itemMajorVersionLimit: 250, itemVersioningEnabled: true, containerTypeName: containerTypeName, verbose: true } });
182+
assert.deepStrictEqual(postStub.lastCall.args[0].data, {
183+
displayName: containerName,
184+
description: 'Lorem ipsum',
185+
containerTypeId: containerTypeId,
186+
settings: {
187+
isOcrEnabled: true,
188+
itemMajorVersionLimit: 250,
189+
isItemVersioningEnabled: true
190+
}
191+
});
192+
});
193+
194+
it('correctly handles error', async () => {
195+
sinon.stub(request, 'post').rejects({
196+
error: {
197+
code: 'accessDenied',
198+
message: 'Access denied'
199+
}
200+
});
201+
202+
await assert.rejects(command.action(logger, { options: { name: containerName, containerTypeId: containerTypeId } }),
203+
new CommandError('Access denied'));
204+
});
205+
});

0 commit comments

Comments
 (0)