diff --git a/docs/docs/cmd/spe/container/container-recyclebinitem-list.mdx b/docs/docs/cmd/spe/container/container-recyclebinitem-list.mdx
new file mode 100644
index 00000000000..407ab3d810a
--- /dev/null
+++ b/docs/docs/cmd/spe/container/container-recyclebinitem-list.mdx
@@ -0,0 +1,96 @@
+import Global from '/docs/cmd/_global.mdx';
+import Tabs from '@theme/Tabs';
+import TabItem from '@theme/TabItem';
+
+# spe container recyclebinitem list
+
+Lists deleted containers of a specific container type
+
+## Usage
+
+```sh
+m365 spe container recyclebinitem list [options]
+```
+
+## Options
+
+```md definition-list
+`--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.
+```
+
+
+
+## Examples
+
+List deleted containers of a specific container type specified by id.
+
+```sh
+m365 spe container recyclebinitem list --containerTypeId "91710488-5756-407f-9046-fbe5f0b4de73"
+```
+
+List deleted containers of a specific container type specified by name.
+
+```sh
+m365 spe container recyclebinitem list --containerTypeName "My container type name"
+```
+
+## Response
+
+
+
+
+ ```json
+ [
+ {
+ "id": "b!ISJs1WRro0y0EWgkUYcktDa0mE8zSlFEqFzqRn70Zwp1CEtDEBZgQICPkRbil_5Z",
+ "displayName": "My Application Storage Container",
+ "containerTypeId": "1a55ba46-a673-45a4-b0d9-bd9913d06957",
+ "createdDateTime": "2025-04-15T21:51:48Z",
+ "settings": {
+ "isOcrEnabled": false
+ }
+ }
+ ]
+ ```
+
+
+
+
+ ```text
+ id displayName
+ ------------------------------------------------------------------ --------------------------------
+ b!ISJs1WRro0y0EWgkUYcktDa0mE8zSlFEqFzqRn70Zwp1CEtDEBZgQICPkRbil_5Z My Application Storage Container
+ ```
+
+
+
+
+ ```csv
+ id,displayName,containerTypeId,createdDateTime
+ b!ISJs1WRro0y0EWgkUYcktDa0mE8zSlFEqFzqRn70Zwp1CEtDEBZgQICPkRbil_5Z,My Application Storage Container,1a55ba46-a673-45a4-b0d9-bd9913d06957,2025-04-15T21:51:48Z
+ ```
+
+
+
+
+ ```md
+ # spe container recyclebinitem list --containerTypeId "1a55ba46-a673-45a4-b0d9-bd9913d06957"
+
+ Date: 18/04/2025
+
+ ## My Application Storage Container (b!ISJs1WRro0y0EWgkUYcktDa0mE8zSlFEqFzqRn70Zwp1CEtDEBZgQICPkRbil_5Z)
+
+ Property | Value
+ ---------|-------
+ id | b!ISJs1WRro0y0EWgkUYcktDa0mE8zSlFEqFzqRn70Zwp1CEtDEBZgQICPkRbil_5Z
+ displayName | My Application Storage Container
+ containerTypeId | 1a55ba46-a673-45a4-b0d9-bd9913d06957
+ createdDateTime | 2025-04-15T21:51:48Z
+ ```
+
+
+
diff --git a/docs/src/config/sidebars.ts b/docs/src/config/sidebars.ts
index 7cdeba757cb..191a29f478b 100644
--- a/docs/src/config/sidebars.ts
+++ b/docs/src/config/sidebars.ts
@@ -2132,6 +2132,11 @@ const sidebars: SidebarsConfig = {
type: 'doc',
label: 'container list',
id: 'cmd/spe/container/container-list'
+ },
+ {
+ type: 'doc',
+ label: 'container recyclebinitem list',
+ id: 'cmd/spe/container/container-recyclebinitem-list'
}
]
},
diff --git a/src/m365/spe/commands.ts b/src/m365/spe/commands.ts
index d8031a2682d..c0ca9abf5f1 100644
--- a/src/m365/spe/commands.ts
+++ b/src/m365/spe/commands.ts
@@ -4,6 +4,7 @@ export default {
CONTAINER_ACTIVATE: `${prefix} container activate`,
CONTAINER_GET: `${prefix} container get`,
CONTAINER_LIST: `${prefix} container list`,
+ CONTAINER_RECYCLEBINITEM_LIST: `${prefix} container recyclebinitem list`,
CONTAINERTYPE_ADD: `${prefix} containertype add`,
CONTAINERTYPE_GET: `${prefix} containertype get`,
CONTAINERTYPE_LIST: `${prefix} containertype list`
diff --git a/src/m365/spe/commands/container/container-list.spec.ts b/src/m365/spe/commands/container/container-list.spec.ts
index 35a90a69624..ad45cf234a9 100644
--- a/src/m365/spe/commands/container/container-list.spec.ts
+++ b/src/m365/spe/commands/container/container-list.spec.ts
@@ -9,10 +9,10 @@ import { session } from '../../../../utils/session.js';
import { sinonUtil } from '../../../../utils/sinonUtil.js';
import commands from '../../commands.js';
import command from './container-list.js';
-import { spo } from '../../../../utils/spo.js';
import { CommandError } from '../../../../Command.js';
import { CommandInfo } from '../../../../cli/CommandInfo.js';
import { cli } from '../../../../cli/cli.js';
+import { spe } from '../../../../utils/spe.js';
describe(commands.CONTAINER_LIST, () => {
let log: string[];
@@ -34,40 +34,12 @@ describe(commands.CONTAINER_LIST, () => {
"createdDateTime": "2021-11-24T15:41:52.347Z"
}];
- const containerTypedata = [{
- "AzureSubscriptionId": "/Guid(f08575e2-36c4-407f-a891-eabae23f66bc)",
- "ContainerTypeId": "/Guid(e2756c4d-fa33-4452-9c36-2325686e1082)",
- "CreationDate": "3/11/2024 2:38:56 PM",
- "DisplayName": "standard container",
- "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"
- },
- {
- "AzureSubscriptionId": "/Guid(f08575e2-36c4-407f-a891-eabae23f66bc)",
- "ContainerTypeId": "/Guid(e2756c4d-fa33-4452-9c36-2325686e1082)",
- "CreationDate": "3/11/2024 2:38:56 PM",
- "DisplayName": "trial container",
- "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"
- }];
-
before(() => {
sinon.stub(auth, 'restoreAuth').resolves();
sinon.stub(telemetry, 'trackEvent').resolves();
sinon.stub(pid, 'getProcessName').returns('');
sinon.stub(session, 'getId').returns('');
- sinon.stub(spo, 'getSpoAdminUrl').resolves(adminUrl);
- sinon.stub(spo, 'ensureFormDigest').resolves({ FormDigestValue: 'abc', FormDigestTimeoutSeconds: 1800, FormDigestExpiresAt: new Date(), WebFullUrl: 'https://contoso.sharepoint.com' });
+
auth.connection.active = true;
auth.connection.spoUrl = 'https://contoso.sharepoint.com';
commandInfo = cli.getCommandInfo(command);
@@ -87,14 +59,15 @@ describe(commands.CONTAINER_LIST, () => {
}
};
loggerLogSpy = sinon.spy(logger, 'log');
+
+ sinon.stub(spe, 'getContainerTypeIdByName').withArgs(adminUrl, 'standard container').resolves('e2756c4d-fa33-4452-9c36-2325686e1082');
});
afterEach(() => {
sinonUtil.restore([
request.get,
request.post,
- spo.getSpoAdminUrl,
- spo.getAllContainerTypes
+ spe.getContainerTypeIdByName
]);
});
@@ -135,13 +108,11 @@ describe(commands.CONTAINER_LIST, () => {
throw 'Invalid request';
});
- await command.action(logger, { options: { containerTypeId: "e2756c4d-fa33-4452-9c36-2325686e1082", debug: true } });
+ await command.action(logger, { options: { containerTypeId: "e2756c4d-fa33-4452-9c36-2325686e1082", verbose: true } });
assert(loggerLogSpy.calledWith(containersList));
});
it('retrieves list of container type by name', async () => {
- sinon.stub(spo, 'getAllContainerTypes').resolves(containerTypedata);
-
sinon.stub(request, 'get').callsFake(async (opts) => {
if (opts.url === 'https://graph.microsoft.com/v1.0/storage/fileStorage/containers?$filter=containerTypeId eq e2756c4d-fa33-4452-9c36-2325686e1082') {
return { "value": containersList };
@@ -150,32 +121,19 @@ describe(commands.CONTAINER_LIST, () => {
throw 'Invalid request';
});
- await command.action(logger, { options: { containerTypeName: "standard container", debug: true } });
+ await command.action(logger, { options: { containerTypeName: "standard container", verbose: true } });
assert(loggerLogSpy.calledWith(containersList));
});
- it('throws an error when service principal is not found', async () => {
- sinon.stub(request, 'get').callsFake(async (opts) => {
- if (opts.url === 'https://graph.microsoft.com/v1.0/storage/fileStorage/containers?$filter=containerTypeId eq e2756c4d-fa33-4452-9c36-2325686e1086') {
- return [];
- }
-
- throw 'Invalid request';
- });
-
- sinon.stub(spo, 'getAllContainerTypes').resolves(containerTypedata);
-
- await assert.rejects(command.action(logger, { options: { containerTypeName: "nonexisting container", debug: true } }),
- new CommandError(`Container type with name nonexisting container not found`));
- });
-
it('correctly handles error when retrieving containers', async () => {
const error = 'An error has occurred';
- sinon.stub(spo, 'getAllContainerTypes').rejects(new Error(error));
+ sinonUtil.restore(spe.getContainerTypeIdByName);
+ sinon.stub(spe, 'getContainerTypeIdByName').rejects(new Error(error));
await assert.rejects(command.action(logger, {
options: {
- debug: true
+ containerTypeName: "nonexisting container",
+ verbose: true
}
}), new CommandError('An error has occurred'));
});
diff --git a/src/m365/spe/commands/container/container-list.ts b/src/m365/spe/commands/container/container-list.ts
index 3149436ab36..3abb3599419 100644
--- a/src/m365/spe/commands/container/container-list.ts
+++ b/src/m365/spe/commands/container/container-list.ts
@@ -6,7 +6,8 @@ import { validation } from '../../../../utils/validation.js';
import GraphCommand from '../../../base/GraphCommand.js';
import commands from '../../commands.js';
import { ContainerProperties } from '../../ContainerProperties.js';
-import { ContainerTypeProperties, spo } from '../../../../utils/spo.js';
+import { spe } from '../../../../utils/spe.js';
+import { spo } from '../../../../utils/spo.js';
interface CommandArgs {
options: Options;
@@ -101,17 +102,7 @@ class SpeContainerListCommand extends GraphCommand {
}
const spoAdminUrl = await spo.getSpoAdminUrl(logger, this.debug);
- const containerTypes: ContainerTypeProperties[] = await spo.getAllContainerTypes(spoAdminUrl, logger, this.debug);
-
- // Get id of the container type by name
- const containerType: ContainerTypeProperties | undefined = containerTypes.find(c => c.DisplayName === options.containerTypeName);
- if (!containerType) {
- throw new Error(`Container type with name ${options.containerTypeName} not found`);
- }
-
- // The value is returned as "/Guid(073269af-f1d2-042d-2ef5-5bdd6ac83115)/". We need to extract the GUID from it.
- const containerTypeValue = containerType.ContainerTypeId.toString();
- return containerTypeValue.substring(containerTypeValue.indexOf('(') + 1, containerTypeValue.lastIndexOf(')'));
+ return spe.getContainerTypeIdByName(spoAdminUrl, options.containerTypeName!);
}
}
diff --git a/src/m365/spe/commands/container/container-recyclebinitem-list.spec.ts b/src/m365/spe/commands/container/container-recyclebinitem-list.spec.ts
new file mode 100644
index 00000000000..d3b2d11d28a
--- /dev/null
+++ b/src/m365/spe/commands/container/container-recyclebinitem-list.spec.ts
@@ -0,0 +1,193 @@
+import assert from 'assert';
+import sinon from 'sinon';
+import auth from '../../../../Auth.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 commands from '../../commands.js';
+import command from './container-recyclebinitem-list.js';
+import { spe } from '../../../../utils/spe.js';
+import { CommandError } from '../../../../Command.js';
+import { CommandInfo } from '../../../../cli/CommandInfo.js';
+import { z } from 'zod';
+import { cli } from '../../../../cli/cli.js';
+
+describe(commands.CONTAINER_RECYCLEBINITEM_LIST, () => {
+ const containerTypeId = 'dda3cb36-a16a-40b9-8f04-b01e39fc035d';
+ const requestResponse = [
+ {
+ id: 'b!ISJs1WRro0y0EWgkUYcktDa0mE8zSlFEqFzqRn70Zwp1CEtDEBZgQICPkRbil_5Z',
+ displayName: 'Playground container',
+ containerTypeId: containerTypeId,
+ createdDateTime: '2025-04-15T21:04:25Z',
+ settings: {
+ isOcrEnabled: true
+ }
+ },
+ {
+ id: 'b!3vQnoI2C-UOm3Z_bCtysBbDa0mE8zSlFEqFzqRn70Zwp1CEtDEBZgQICPkRbil_5Z',
+ displayName: 'My Application Storage Container',
+ containerTypeId: containerTypeId,
+ createdDateTime: '2025-04-15T21:51:48Z',
+ settings: {
+ isOcrEnabled: false
+ }
+ }
+ ];
+
+ 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 = 'https://contoso.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.get
+ ]);
+ });
+
+ after(() => {
+ sinon.restore();
+ auth.connection.active = false;
+ auth.connection.spoUrl = undefined;
+ });
+
+ it('has correct name', () => {
+ assert.strictEqual(command.name, commands.CONTAINER_RECYCLEBINITEM_LIST);
+ });
+
+ it('has a description', () => {
+ assert.notStrictEqual(command.description, null);
+ });
+
+ it('defines correct properties for the default output', () => {
+ assert.deepStrictEqual(command.defaultProperties(), ['id', 'displayName']);
+ });
+
+ it('fails validation if both containerTypeId and containerTypeName options are passed', async () => {
+ const actual = commandOptionsSchema.safeParse({ containerTypeId: containerTypeId, containerTypeName: 'Container name' });
+ assert.strictEqual(actual.success, false);
+ });
+
+ it('fails validation if neither containerTypeId nor containerTypeName options are passed', async () => {
+ const actual = commandOptionsSchema.safeParse({});
+ assert.strictEqual(actual.success, false);
+ });
+
+ it('fails validation if containerTypeId is not a valid GUID', async () => {
+ const actual = commandOptionsSchema.safeParse({ containerTypeId: 'invalid' });
+ assert.strictEqual(actual.success, false);
+ });
+
+ it('passes validation if containerTypeId is a valid GUID', async () => {
+ const actual = commandOptionsSchema.safeParse({ containerTypeId: containerTypeId });
+ assert.strictEqual(actual.success, true);
+ });
+
+ it('correctly outputs a result when using containerTypeId', async () => {
+ sinon.stub(request, 'get').callsFake(async (opts) => {
+ if (opts.url === `https://graph.microsoft.com/v1.0/storage/fileStorage/deletedContainers?$filter=containerTypeId eq ${containerTypeId}`) {
+ return {
+ value: requestResponse
+ };
+ }
+
+ throw 'Invalid GET request: ' + opts.url;
+ });
+
+ await command.action(logger, { options: { containerTypeId: containerTypeId } });
+ assert(loggerLogSpy.calledOnceWith(requestResponse));
+ });
+
+ it('correctly outputs a result when using containerTypename', async () => {
+ sinon.stub(request, 'get').callsFake(async (opts) => {
+ if (opts.url === `https://graph.microsoft.com/v1.0/storage/fileStorage/deletedContainers?$filter=containerTypeId eq ${containerTypeId}`) {
+ return {
+ value: requestResponse
+ };
+ }
+
+ throw 'Invalid GET request: ' + opts.url;
+ });
+
+ await command.action(logger, { options: { containerTypeName: 'Container Type Name' } });
+ assert(loggerLogSpy.calledOnceWith(requestResponse));
+ });
+
+ it('retrieves list of container recycle bin items by using containerTypeId', async () => {
+ const getStub = sinon.stub(request, 'get').callsFake(async (opts) => {
+ if (opts.url === `https://graph.microsoft.com/v1.0/storage/fileStorage/deletedContainers?$filter=containerTypeId eq ${containerTypeId}`) {
+ return {
+ value: requestResponse
+ };
+ }
+
+ throw 'Invalid GET request: ' + opts.url;
+ });
+
+ await command.action(logger, { options: { containerTypeId: containerTypeId } });
+ assert(getStub.calledOnce);
+ });
+
+ it('retrieves list of container recycle bin items by using containerTypeName', async () => {
+ const getStub = sinon.stub(request, 'get').callsFake(async (opts) => {
+ if (opts.url === `https://graph.microsoft.com/v1.0/storage/fileStorage/deletedContainers?$filter=containerTypeId eq ${containerTypeId}`) {
+ return {
+ value: requestResponse
+ };
+ }
+
+ throw 'Invalid GET request: ' + opts.url;
+ });
+
+ await command.action(logger, { options: { containerTypeName: 'Container Type Name', verbose: true } });
+ assert(getStub.calledOnce);
+ });
+
+ it('correctly handles unexpected error', async () => {
+ const errorMessage = 'Access denied';
+ sinon.stub(request, 'get').rejects({
+ error: {
+ code: 'accessDenied',
+ message: errorMessage
+ }
+ });
+
+ await assert.rejects(command.action(logger, { options: { containerTypeId: containerTypeId } })
+ , new CommandError(errorMessage));
+ });
+});
\ No newline at end of file
diff --git a/src/m365/spe/commands/container/container-recyclebinitem-list.ts b/src/m365/spe/commands/container/container-recyclebinitem-list.ts
new file mode 100644
index 00000000000..325aab79f45
--- /dev/null
+++ b/src/m365/spe/commands/container/container-recyclebinitem-list.ts
@@ -0,0 +1,81 @@
+import { globalOptionsZod } from '../../../../Command.js';
+import { z } from 'zod';
+import { Logger } from '../../../../cli/Logger.js';
+import commands from '../../commands.js';
+import { validation } from '../../../../utils/validation.js';
+import { spo } from '../../../../utils/spo.js';
+import GraphCommand from '../../../base/GraphCommand.js';
+import { spe } from '../../../../utils/spe.js';
+import { odata } from '../../../../utils/odata.js';
+
+const options = globalOptionsZod
+ .extend({
+ containerTypeId: z.string()
+ .refine(id => validation.isValidGuid(id), id => ({
+ message: `'${id}' is not a valid GUID.`
+ })).optional(),
+ containerTypeName: z.string().optional()
+ })
+ .strict();
+
+declare type Options = z.infer;
+
+interface CommandArgs {
+ options: Options;
+}
+
+class SpeContainerRecycleBinItemListCommand extends GraphCommand {
+ public get name(): string {
+ return commands.CONTAINER_RECYCLEBINITEM_LIST;
+ }
+
+ public get description(): string {
+ return 'Lists deleted containers of a specific container type';
+ }
+
+ public get schema(): z.ZodTypeAny {
+ return options;
+ }
+
+ public defaultProperties(): string[] | undefined {
+ return ['id', 'displayName'];
+ }
+
+ public getRefinedSchema(schema: z.ZodTypeAny): z.ZodEffects | undefined {
+ return schema
+ .refine((options: Options) => [options.containerTypeId, options.containerTypeName].filter(o => o !== undefined).length === 1, {
+ message: 'Use one of the following options: containerTypeId or containerTypeName.'
+ });
+ }
+
+ public async commandAction(logger: Logger, args: CommandArgs): Promise {
+ try {
+ const containerTypeId = await this.getContainerTypeId(args.options, logger);
+
+ if (this.verbose) {
+ await logger.logToStderr(`Retrieving deleted containers of container type with ID '${containerTypeId}'...`);
+ }
+
+ const deletedContainers = await odata.getAllItems(`${this.resource}/v1.0/storage/fileStorage/deletedContainers?$filter=containerTypeId eq ${containerTypeId}`);
+ await logger.log(deletedContainers);
+ }
+ catch (err: any) {
+ this.handleRejectedODataJsonPromise(err);
+ }
+ }
+
+ private async getContainerTypeId(options: Options, logger: Logger): Promise {
+ if (options.containerTypeId) {
+ return options.containerTypeId;
+ }
+
+ if (this.verbose) {
+ await logger.logToStderr(`Retrieving container type id for container type '${options.containerTypeName}'...`);
+ }
+
+ const adminUrl = await spo.getSpoAdminUrl(logger, this.verbose);
+ return spe.getContainerTypeIdByName(adminUrl, options.containerTypeName!);
+ }
+}
+
+export default new SpeContainerRecycleBinItemListCommand();
\ No newline at end of file
diff --git a/src/m365/spe/commands/containertype/containertype-get.spec.ts b/src/m365/spe/commands/containertype/containertype-get.spec.ts
index 9b48ef2a95c..920e2e3bdd0 100644
--- a/src/m365/spe/commands/containertype/containertype-get.spec.ts
+++ b/src/m365/spe/commands/containertype/containertype-get.spec.ts
@@ -11,7 +11,6 @@ import { sinonUtil } from '../../../../utils/sinonUtil.js';
import { cli } from '../../../../cli/cli.js';
import commands from '../../commands.js';
import command from './containertype-get.js';
-import { spo } from '../../../../utils/spo.js';
import { CommandError } from '../../../../Command.js';
import config from '../../../../config.js';
@@ -43,7 +42,6 @@ describe(commands.CONTAINERTYPE_GET, () => {
sinon.stub(telemetry, 'trackEvent').resolves();
sinon.stub(pid, 'getProcessName').returns('');
sinon.stub(session, 'getId').returns('');
- sinon.stub(spo, 'ensureFormDigest').resolves({ FormDigestValue: 'abc', FormDigestTimeoutSeconds: 1800, FormDigestExpiresAt: new Date(), WebFullUrl: 'https://contoso.sharepoint.com' });
auth.connection.active = true;
auth.connection.spoUrl = 'https://contoso.sharepoint.com';
commandInfo = cli.getCommandInfo(command);
@@ -108,19 +106,16 @@ describe(commands.CONTAINERTYPE_GET, () => {
it('retrieves container type by ID', async () => {
sinon.stub(request, 'post').callsFake(async (opts) => {
- if ((opts.url as string).indexOf(`/_vti_bin/client.svc/ProcessQuery`) > -1) {
- if (opts.headers &&
- opts.headers['X-RequestDigest'] &&
- opts.headers['X-RequestDigest'] === 'abc' &&
- opts.data === `{${containerTypeId}}1`) {
- return JSON.stringify([
+ if (opts.url === `https://contoso-admin.sharepoint.com/_vti_bin/client.svc/ProcessQuery`) {
+ if (opts.data === `{${containerTypeId}}1`) {
+ return [
{
"SchemaVersion": "15.0.0.0", "LibraryVersion": "16.0.25117.12004", "ErrorInfo": null, "TraceCorrelationId": "df0a44a1-c013-9000-9064-f786729ad6a5"
}
, 49, {
"IsNull": false
}, 50, containerTypedata
- ]);
+ ];
}
}
@@ -133,18 +128,15 @@ describe(commands.CONTAINERTYPE_GET, () => {
it('retrieves container type by ID (debug)', async () => {
sinon.stub(request, 'post').callsFake(async (opts) => {
- if ((opts.url as string).indexOf(`/_vti_bin/client.svc/ProcessQuery`) > -1) {
- if (opts.headers &&
- opts.headers['X-RequestDigest'] &&
- opts.headers['X-RequestDigest'] === 'abc' &&
- opts.data === `{${containerTypeId}}1`) {
- return JSON.stringify([
+ if (opts.url === `https://contoso-admin.sharepoint.com/_vti_bin/client.svc/ProcessQuery`) {
+ if (opts.data === `{${containerTypeId}}1`) {
+ return [
{
"SchemaVersion": "15.0.0.0", "LibraryVersion": "16.0.25117.12004", "ErrorInfo": null, "TraceCorrelationId": "2d63d39f-3016-0000-a532-30514e76ae73"
}, 49, {
"IsNull": false
}, 50, containerTypedata
- ]);
+ ];
}
}
@@ -157,19 +149,14 @@ describe(commands.CONTAINERTYPE_GET, () => {
it('correctly handles error when retrieving container type by ID', async () => {
sinon.stub(request, 'post').callsFake(async (opts) => {
- if ((opts.url as string).indexOf(`/_vti_bin/client.svc/ProcessQuery`) > -1) {
- if (opts.headers &&
- opts.headers['X-RequestDigest'] &&
- opts.headers['X-RequestDigest'] === 'abc') {
-
- return JSON.stringify([
- {
- "SchemaVersion": "15.0.0.0", "LibraryVersion": "16.0.7324.1200", "ErrorInfo": {
- "ErrorMessage": "An error has occurred.", "ErrorValue": null, "TraceCorrelationId": "e13c489e-2026-5000-8242-7ec96d02ba1d", "ErrorCode": -1, "ErrorTypeName": "SPException"
- }, "TraceCorrelationId": "e13c489e-2026-5000-8242-7ec96d02ba1d"
- }
- ]);
- }
+ if (opts.url === `https://contoso-admin.sharepoint.com/_vti_bin/client.svc/ProcessQuery`) {
+ return [
+ {
+ "SchemaVersion": "15.0.0.0", "LibraryVersion": "16.0.7324.1200", "ErrorInfo": {
+ "ErrorMessage": "An error has occurred.", "ErrorValue": null, "TraceCorrelationId": "e13c489e-2026-5000-8242-7ec96d02ba1d", "ErrorCode": -1, "ErrorTypeName": "SPException"
+ }, "TraceCorrelationId": "e13c489e-2026-5000-8242-7ec96d02ba1d"
+ }
+ ];
}
throw 'Invalid request';
@@ -181,34 +168,28 @@ describe(commands.CONTAINERTYPE_GET, () => {
it('retrieves the container type by name successfully', async () => {
sinon.stub(request, 'post').callsFake(async (opts) => {
- if ((opts.url as string).indexOf(`/_vti_bin/client.svc/ProcessQuery`) > -1) {
- if (opts.headers &&
- opts.headers['X-RequestDigest'] &&
- opts.headers['X-RequestDigest'] === 'abc' &&
- opts.data === `1`) {
- return JSON.stringify([
+ if (opts.url === `https://contoso-admin.sharepoint.com/_vti_bin/client.svc/ProcessQuery`) {
+ if (opts.data === `1`) {
+ return [
{
"SchemaVersion": "15.0.0.0", "LibraryVersion": "16.0.24817.12005", "ErrorInfo": null, "TraceCorrelationId": "2d63d39f-3016-0000-a532-30514e76ae73"
}, 46, {
"IsNull": false
}, 47, [containerTypedata]
- ]);
+ ];
}
}
if ((opts.url as string).indexOf(`/_vti_bin/client.svc/ProcessQuery`) > -1) {
- if (opts.headers &&
- opts.headers['X-RequestDigest'] &&
- opts.headers['X-RequestDigest'] === 'abc' &&
- opts.data === `{${containerTypeId}}1`) {
- return JSON.stringify([
+ if (opts.data === `{${containerTypeId}}1`) {
+ return [
{
"SchemaVersion": "15.0.0.0", "LibraryVersion": "16.0.25117.12004", "ErrorInfo": null, "TraceCorrelationId": "df0a44a1-c013-9000-9064-f786729ad6a5"
}
, 49, {
"IsNull": false
}, 50, containerTypedata
- ]);
+ ];
}
}
throw "Invalid request";
@@ -220,24 +201,20 @@ describe(commands.CONTAINERTYPE_GET, () => {
it('correctly handles container type not found', async () => {
sinon.stub(request, 'post').callsFake(async (opts) => {
- if ((opts.url as string).indexOf(`/_vti_bin/client.svc/ProcessQuery`) > -1) {
- if (opts.headers &&
- opts.headers['X-RequestDigest'] &&
- opts.headers['X-RequestDigest'] === 'abc' &&
- opts.data === `1`) {
- return JSON.stringify([
+ if (opts.url === `https://contoso-admin.sharepoint.com/_vti_bin/client.svc/ProcessQuery`) {
+ if (opts.data === `1`) {
+ return [
{
"SchemaVersion": "15.0.0.0", "LibraryVersion": "16.0.24817.12005", "ErrorInfo": null, "TraceCorrelationId": "2d63d39f-3016-0000-a532-30514e76ae73"
}, 46, {
"IsNull": false
}, 47, []
- ]);
+ ];
}
}
throw "Invalid request";
});
- await assert.rejects(command.action(logger, { options: { name: 'test' } } as any), new CommandError("Container type with name test not found"));
+ await assert.rejects(command.action(logger, { options: { name: 'test' } } as any), new CommandError("The specified container type 'test' does not exist."));
});
-
});
diff --git a/src/m365/spe/commands/containertype/containertype-get.ts b/src/m365/spe/commands/containertype/containertype-get.ts
index c0addd80ae2..38c9c6302ad 100644
--- a/src/m365/spe/commands/containertype/containertype-get.ts
+++ b/src/m365/spe/commands/containertype/containertype-get.ts
@@ -2,7 +2,8 @@ import { Logger } from '../../../../cli/Logger.js';
import config from '../../../../config.js';
import GlobalOptions from '../../../../GlobalOptions.js';
import request, { CliRequestOptions } from '../../../../request.js';
-import { ClientSvcResponse, ClientSvcResponseContents, FormDigestInfo, ContainerTypeProperties, spo } from '../../../../utils/spo.js';
+import { spe, ContainerTypeProperties } from '../../../../utils/spe.js';
+import { ClientSvcResponse, ClientSvcResponseContents, spo } from '../../../../utils/spo.js';
import { validation } from '../../../../utils/validation.js';
import SpoCommand from '../../../base/SpoCommand.js';
import commands from '../../commands.js';
@@ -86,53 +87,42 @@ class SpeContainertypeGetCommand extends SpoCommand {
await logger.logToStderr(`Getting the Container type...`);
}
- const containerTypeId = await this.getContainerTypeId(args.options, spoAdminUrl, logger);
- const allContainerTypes = await this.getContainerTypeById(containerTypeId, spoAdminUrl, logger);
- await logger.log(allContainerTypes);
+ const containerTypeId = await this.getContainerTypeId(args.options, spoAdminUrl);
+ const containerType = await this.getContainerTypeById(containerTypeId, spoAdminUrl);
+ await logger.log(containerType);
}
catch (err: any) {
this.handleRejectedPromise(err);
}
}
- private async getContainerTypeById(containerTypeId: string, spoAdminUrl: string, logger: Logger): Promise {
- const formDigestInfo: FormDigestInfo = await spo.ensureFormDigest(spoAdminUrl, logger, undefined, this.debug);
-
+ private async getContainerTypeById(containerTypeId: string, spoAdminUrl: string): Promise {
const requestOptions: CliRequestOptions = {
url: `${spoAdminUrl}/_vti_bin/client.svc/ProcessQuery`,
headers: {
- 'X-RequestDigest': formDigestInfo.FormDigestValue
+ accept: 'application/json;odata=nometadata'
},
+ responseType: 'json',
data: `{${containerTypeId}}1`
};
- const res: string = await request.post(requestOptions);
- const json: ClientSvcResponse = JSON.parse(res);
- const response: ClientSvcResponseContents = json[0];
+ const res = await request.post(requestOptions);
+ const response: ClientSvcResponseContents = res[0];
if (response.ErrorInfo) {
throw response.ErrorInfo.ErrorMessage;
}
- const containerTypes: ContainerTypeProperties[] = json[json.length - 1];
+ const containerTypes: ContainerTypeProperties[] = res[res.length - 1];
return containerTypes;
}
- private async getContainerTypeId(options: Options, spoAdminUrl: string, logger: Logger): Promise {
+ private async getContainerTypeId(options: Options, spoAdminUrl: string): Promise {
if (options.id) {
return options.id;
}
- const containerTypes: ContainerTypeProperties[] = await spo.getAllContainerTypes(spoAdminUrl, logger, this.debug);
-
- // Get id of the container type by name
- const containerType: ContainerTypeProperties | undefined = containerTypes.find(c => c.DisplayName === options.name);
- if (!containerType) {
- throw new Error(`Container type with name ${options.name} not found`);
- }
-
- const match = containerType.ContainerTypeId.match(/\/Guid\(([^)]+)\)\//);
- return match![1];
+ return spe.getContainerTypeIdByName(spoAdminUrl, options.name!);
}
}
diff --git a/src/m365/spe/commands/containertype/containertype-list.spec.ts b/src/m365/spe/commands/containertype/containertype-list.spec.ts
index ad4843208d6..43acdab0b53 100644
--- a/src/m365/spe/commands/containertype/containertype-list.spec.ts
+++ b/src/m365/spe/commands/containertype/containertype-list.spec.ts
@@ -9,34 +9,34 @@ import { session } from '../../../../utils/session.js';
import { sinonUtil } from '../../../../utils/sinonUtil.js';
import commands from '../../commands.js';
import command from './containertype-list.js';
-import { spo } from '../../../../utils/spo.js';
+import { spe } from '../../../../utils/spe.js';
import { CommandError } from '../../../../Command.js';
-const containerTypedata = [{
- "AzureSubscriptionId": "/Guid(f08575e2-36c4-407f-a891-eabae23f66bc)",
- "ContainerTypeId": "/Guid(c33cfee5-c9b6-0a2a-02ee-060693a57f37)",
- "CreationDate": "3/11/2024 2:38:56 PM",
- "DisplayName": "standard container",
- "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"
+const containerTypeData = [{
+ AzureSubscriptionId: 'f08575e2-36c4-407f-a891-eabae23f66bc',
+ ContainerTypeId: 'c33cfee5-c9b6-0a2a-02ee-060693a57f37',
+ CreationDate: '3/11/2024 2:38:56 PM',
+ DisplayName: 'standard container',
+ ExpiryDate: '3/11/2028 2:38:56 PM',
+ IsBillingProfileRequired: true,
+ OwningAppId: '1b3b8660-9a44-4a7c-9c02-657f3ff5d5ac',
+ OwningTenantId: 'e1dd4023-a656-480a-8a0e-c1b1eec51e1d',
+ Region: 'West Europe',
+ ResourceGroup: 'Standard group',
+ SPContainerTypeBillingClassification: 'Standard'
},
{
- "AzureSubscriptionId": "/Guid(f08575e2-36c4-407f-a891-eabae23f66bc)",
- "ContainerTypeId": "/Guid(c33cfee5-c9b6-0a2a-02ee-060693a57f37)",
- "CreationDate": "3/11/2024 2:38:56 PM",
- "DisplayName": "trial container",
- "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"
+ AzureSubscriptionId: 'f08575e2-36c4-407f-a891-eabae23f66bc',
+ ContainerTypeId: 'a33cfee5-c9b6-0a2a-02ee-060693a57f37',
+ CreationDate: '3/11/2024 2:38:56 PM',
+ DisplayName: 'trial container',
+ ExpiryDate: '3/11/2028 2:38:56 PM',
+ IsBillingProfileRequired: true,
+ OwningAppId: '1b3b8660-9a44-4a7c-9c02-657f3ff5d5ac',
+ OwningTenantId: 'e1dd4023-a656-480a-8a0e-c1b1eec51e1d',
+ Region: 'West Europe',
+ ResourceGroup: 'Standard group',
+ SPContainerTypeBillingClassification: 'Standard'
}];
describe(commands.CONTAINERTYPE_LIST, () => {
@@ -49,7 +49,6 @@ describe(commands.CONTAINERTYPE_LIST, () => {
sinon.stub(telemetry, 'trackEvent').resolves();
sinon.stub(pid, 'getProcessName').returns('');
sinon.stub(session, 'getId').returns('');
- sinon.stub(spo, 'ensureFormDigest').resolves({ FormDigestValue: 'abc', FormDigestTimeoutSeconds: 1800, FormDigestExpiresAt: new Date(), WebFullUrl: 'https://contoso.sharepoint.com' });
auth.connection.active = true;
auth.connection.spoUrl = 'https://contoso.sharepoint.com';
});
@@ -73,7 +72,7 @@ describe(commands.CONTAINERTYPE_LIST, () => {
afterEach(() => {
sinonUtil.restore([
request.post,
- spo.getAllContainerTypes
+ spe.getAllContainerTypes
]);
});
@@ -96,18 +95,48 @@ describe(commands.CONTAINERTYPE_LIST, () => {
});
it('retrieves list of container type', async () => {
- sinon.stub(spo, 'getAllContainerTypes').resolves(containerTypedata);
- await command.action(logger, { options: { debug: true } });
- assert(loggerLogSpy.calledWith(containerTypedata));
+ sinon.stub(spe, 'getAllContainerTypes').resolves(containerTypeData);
+
+ await command.action(logger, { options: { verbose: true } });
+ assert(loggerLogSpy.calledOnceWith([
+ {
+ _ObjectType_: 'Microsoft.Online.SharePoint.TenantAdministration.SPContainerTypeProperties',
+ AzureSubscriptionId: '/Guid(f08575e2-36c4-407f-a891-eabae23f66bc)/',
+ ContainerTypeId: '/Guid(c33cfee5-c9b6-0a2a-02ee-060693a57f37)/',
+ CreationDate: '3/11/2024 2:38:56 PM',
+ DisplayName: 'standard container',
+ 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'
+ },
+ {
+ _ObjectType_: 'Microsoft.Online.SharePoint.TenantAdministration.SPContainerTypeProperties',
+ AzureSubscriptionId: '/Guid(f08575e2-36c4-407f-a891-eabae23f66bc)/',
+ ContainerTypeId: '/Guid(a33cfee5-c9b6-0a2a-02ee-060693a57f37)/',
+ CreationDate: '3/11/2024 2:38:56 PM',
+ DisplayName: 'trial container',
+ 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'
+ }
+ ]));
});
it('correctly handles error when retrieving container types', async () => {
const error = 'An error has occurred';
- sinon.stub(spo, 'getAllContainerTypes').rejects(new Error(error));
+ sinon.stub(spe, 'getAllContainerTypes').rejects(new Error(error));
await assert.rejects(command.action(logger, {
options: {
- debug: true
+ verbose: true
}
}), new CommandError('An error has occurred'));
});
diff --git a/src/m365/spe/commands/containertype/containertype-list.ts b/src/m365/spe/commands/containertype/containertype-list.ts
index 97e0f573a20..fffa61edd44 100644
--- a/src/m365/spe/commands/containertype/containertype-list.ts
+++ b/src/m365/spe/commands/containertype/containertype-list.ts
@@ -1,7 +1,8 @@
import { Logger } from '../../../../cli/Logger.js';
import SpoCommand from '../../../base/SpoCommand.js';
import commands from '../../commands.js';
-import { ContainerTypeProperties, spo } from '../../../../utils/spo.js';
+import { spe } from '../../../../utils/spe.js';
+import { spo } from '../../../../utils/spo.js';
class SpeContainertypeListCommand extends SpoCommand {
@@ -25,8 +26,19 @@ class SpeContainertypeListCommand extends SpoCommand {
await logger.logToStderr(`Retrieving list of Container types...`);
}
- const allContainerTypes: ContainerTypeProperties[] = await spo.getAllContainerTypes(spoAdminUrl, logger, this.debug);
- await logger.log(allContainerTypes);
+ const allContainerTypes = await spe.getAllContainerTypes(spoAdminUrl);
+
+ // The following conversion is done in order not to make breaking changes
+ const result = allContainerTypes.map(ct => ({
+ _ObjectType_: 'Microsoft.Online.SharePoint.TenantAdministration.SPContainerTypeProperties',
+ ...ct,
+ AzureSubscriptionId: `/Guid(${ct.AzureSubscriptionId})/`,
+ ContainerTypeId: `/Guid(${ct.ContainerTypeId})/`,
+ OwningAppId: `/Guid(${ct.OwningAppId})/`,
+ OwningTenantId: `/Guid(${ct.OwningTenantId})/`
+ }));
+
+ await logger.log(result);
}
catch (err: any) {
this.handleRejectedPromise(err);
diff --git a/src/utils/formatting.spec.ts b/src/utils/formatting.spec.ts
new file mode 100644
index 00000000000..592aef7c111
--- /dev/null
+++ b/src/utils/formatting.spec.ts
@@ -0,0 +1,19 @@
+import assert from 'assert';
+import { formatting } from './formatting.js';
+
+describe('utils/formatting', () => {
+ it('correctly returns GUID value from a CSOM GUID string when using extractCsomGuid', () => {
+ const result = formatting.extractCsomGuid('/Guid(5c51a9d1-0f07-4e61-879e-0a286568c232)/');
+ assert.strictEqual(result, '5c51a9d1-0f07-4e61-879e-0a286568c232');
+ });
+
+ it('correctly returns GUID value from a CSOM GUID string in capitals when using extractCsomGuid', () => {
+ const result = formatting.extractCsomGuid('/GUID(5C51A9D1-0F07-4E61-879E-0A286568C232)/');
+ assert.strictEqual(result, '5C51A9D1-0F07-4E61-879E-0A286568C232');
+ });
+
+ it('correctly returns default value from a when using invalid GUID string for extractCsomGuid', () => {
+ const result = formatting.extractCsomGuid('invalid');
+ assert.strictEqual(result, 'invalid');
+ });
+});
\ No newline at end of file
diff --git a/src/utils/formatting.ts b/src/utils/formatting.ts
index ef36bb2eb4e..62316245468 100644
--- a/src/utils/formatting.ts
+++ b/src/utils/formatting.ts
@@ -198,5 +198,18 @@ export const formatting = {
resultAsKeyValuePair[obj[key]] = obj;
});
return resultAsKeyValuePair;
+ },
+
+ /**
+ * Extracts the GUID from a string in CSOM format.
+ * @param str The string to extract the GUID from
+ * @description The string should be in the format /Guid(XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX)/
+ * @returns The extracted GUID or the original string if no match is found
+ * @example /Guid(eae15efb-ac09-49b9-8906-e579efd622e4)/ => eae15efb-ac09-49b9-8906-e579efd622e4
+ */
+ extractCsomGuid(str: string): string {
+ const guidPattern = /\/Guid\(([0-9a-f-]+)\)\//i;
+ const match = str.match(guidPattern);
+ return match ? match[1] : str;
}
};
\ No newline at end of file
diff --git a/src/utils/spe.spec.ts b/src/utils/spe.spec.ts
new file mode 100644
index 00000000000..1185a5e7993
--- /dev/null
+++ b/src/utils/spe.spec.ts
@@ -0,0 +1,227 @@
+
+import assert from 'assert';
+import sinon from 'sinon';
+import { spe } from './spe.js';
+import { sinonUtil } from './sinonUtil.js';
+import request from '../request.js';
+import auth from '../Auth.js';
+import config from '../config.js';
+import { cli } from '../cli/cli.js';
+
+describe('utils/spe', () => {
+ const siteUrl = 'https://contoso.sharepoint.com';
+ const adminUrl = siteUrl.replace('.sharepoint.com', '-admin.sharepoint.com');
+
+ const containerTypeResponse = [
+ {
+ _ObjectType_: 'Microsoft.Online.SharePoint.TenantAdministration.SPContainerTypeProperties',
+ ApplicationRedirectUrl: null,
+ AzureSubscriptionId: '/Guid(00000000-0000-0000-0000-000000000000)/',
+ ContainerTypeId: '/Guid(073269af-f1d2-042d-2ef5-5bdd6ac83115)/',
+ CreationDate: null,
+ DisplayName: 'test1',
+ ExpiryDate: null,
+ IsBillingProfileRequired: true,
+ OwningAppId: '/Guid(df4085cc-9a38-4255-badc-5c5225610475)/',
+ OwningTenantId: '/Guid(00000000-0000-0000-0000-000000000000)/',
+ Region: null,
+ ResourceGroup: null,
+ SPContainerTypeBillingClassification: 0
+ },
+ {
+ _ObjectType_: 'Microsoft.Online.SharePoint.TenantAdministration.SPContainerTypeProperties',
+ ApplicationRedirectUrl: null,
+ AzureSubscriptionId: '/Guid(00000000-0000-0000-0000-000000000000)/',
+ ContainerTypeId: '/Guid(880ab3bd-5b68-01d4-3744-01a7656cf2ba)/',
+ CreationDate: null,
+ DisplayName: 'test2',
+ ExpiryDate: null,
+ IsBillingProfileRequired: true,
+ OwningAppId: '/Guid(50785fde-3082-47ac-a36d-06282ac5c7da)/',
+ OwningTenantId: '/Guid(00000000-0000-0000-0000-000000000000)/',
+ Region: null,
+ ResourceGroup: null,
+ SPContainerTypeBillingClassification: 0
+ }
+ ];
+
+ before(() => {
+ auth.connection.active = true;
+ auth.connection.spoUrl = siteUrl;
+ });
+
+ afterEach(() => {
+ sinonUtil.restore([
+ request.post
+ ]);
+ });
+
+ after(() => {
+ auth.connection.active = false;
+ auth.connection.spoUrl = undefined;
+ sinon.restore();
+ });
+
+ it('correctly retrieves a list of container types', async () => {
+ const postStub = sinon.stub(request, 'post').callsFake(async (opts) => {
+ if (opts.url === `${adminUrl}/_vti_bin/client.svc/ProcessQuery`) {
+ return [
+ {
+ "SchemaVersion": "15.0.0.0", "LibraryVersion": "16.0.24817.12005", "ErrorInfo": null, "TraceCorrelationId": "2d63d39f-3016-0000-a532-30514e76ae73"
+ }, 46, {
+ "IsNull": false
+ }, 47, containerTypeResponse
+ ];
+ }
+
+ throw 'Invalid request';
+ });
+
+ await spe.getAllContainerTypes(adminUrl);
+ assert.deepStrictEqual(postStub.lastCall.args[0].data, `1`);
+ });
+
+ it('correctly outputs a list of container types', async () => {
+ sinon.stub(request, 'post').callsFake(async (opts) => {
+ if (opts.url === `${adminUrl}/_vti_bin/client.svc/ProcessQuery`) {
+ return [
+ {
+ "SchemaVersion": "15.0.0.0", "LibraryVersion": "16.0.24817.12005", "ErrorInfo": null, "TraceCorrelationId": "2d63d39f-3016-0000-a532-30514e76ae73"
+ }, 46, {
+ "IsNull": false
+ }, 47, containerTypeResponse
+ ];
+ }
+
+ throw 'Invalid request';
+ });
+
+ const actual = await spe.getAllContainerTypes(adminUrl);
+ assert.deepStrictEqual(actual, [
+ {
+ ApplicationRedirectUrl: null,
+ AzureSubscriptionId: '00000000-0000-0000-0000-000000000000',
+ ContainerTypeId: '073269af-f1d2-042d-2ef5-5bdd6ac83115',
+ CreationDate: null,
+ DisplayName: 'test1',
+ ExpiryDate: null,
+ IsBillingProfileRequired: true,
+ OwningAppId: 'df4085cc-9a38-4255-badc-5c5225610475',
+ OwningTenantId: '00000000-0000-0000-0000-000000000000',
+ Region: null,
+ ResourceGroup: null,
+ SPContainerTypeBillingClassification: 0
+ },
+ {
+ ApplicationRedirectUrl: null,
+ AzureSubscriptionId: '00000000-0000-0000-0000-000000000000',
+ ContainerTypeId: '880ab3bd-5b68-01d4-3744-01a7656cf2ba',
+ CreationDate: null,
+ DisplayName: 'test2',
+ ExpiryDate: null,
+ IsBillingProfileRequired: true,
+ OwningAppId: '50785fde-3082-47ac-a36d-06282ac5c7da',
+ OwningTenantId: '00000000-0000-0000-0000-000000000000',
+ Region: null,
+ ResourceGroup: null,
+ SPContainerTypeBillingClassification: 0
+ }
+ ]);
+ });
+
+ it('correctly throws error when retrieving container types', async () => {
+ sinon.stub(request, 'post').callsFake(async (opts) => {
+ if (opts.url === `${adminUrl}/_vti_bin/client.svc/ProcessQuery`) {
+ return [
+ {
+ "SchemaVersion": "15.0.0.0", "LibraryVersion": "16.0.7324.1200", "ErrorInfo": {
+ "ErrorMessage": "An error has occurred", "ErrorValue": null, "TraceCorrelationId": "e13c489e-2026-5000-8242-7ec96d02ba1d", "ErrorCode": -1, "ErrorTypeName": "SPException"
+ }, "TraceCorrelationId": "e13c489e-2026-5000-8242-7ec96d02ba1d"
+ }
+ ];
+ }
+
+ throw 'Invalid request';
+ });
+
+ await assert.rejects(spe.getAllContainerTypes(adminUrl), new Error('An error has occurred'));
+ });
+
+ it('correctly retrieves the container type ID by name when using getContainerTypeIdByName', async () => {
+ sinon.stub(request, 'post').callsFake(async (opts) => {
+ if (opts.url === `${adminUrl}/_vti_bin/client.svc/ProcessQuery`) {
+ return [
+ {
+ "SchemaVersion": "15.0.0.0", "LibraryVersion": "16.0.24817.12005", "ErrorInfo": null, "TraceCorrelationId": "2d63d39f-3016-0000-a532-30514e76ae73"
+ }, 46, {
+ "IsNull": false
+ }, 47, containerTypeResponse
+ ];
+ }
+
+ throw 'Invalid request';
+ });
+
+ const actual = await spe.getContainerTypeIdByName(adminUrl, 'test2');
+ assert.strictEqual(actual, '880ab3bd-5b68-01d4-3744-01a7656cf2ba');
+ });
+
+ it('correctly throws error when name not found when using getContainerTypeIdByName', async () => {
+ sinon.stub(request, 'post').callsFake(async (opts) => {
+ if (opts.url === `${adminUrl}/_vti_bin/client.svc/ProcessQuery`) {
+ return [
+ {
+ "SchemaVersion": "15.0.0.0", "LibraryVersion": "16.0.24817.12005", "ErrorInfo": null, "TraceCorrelationId": "2d63d39f-3016-0000-a532-30514e76ae73"
+ }, 46, {
+ "IsNull": false
+ }, 47, containerTypeResponse
+ ];
+ }
+
+ throw 'Invalid request';
+ });
+
+ await assert.rejects(spe.getContainerTypeIdByName(adminUrl, 'nonexistent'),
+ new Error(`The specified container type 'nonexistent' does not exist.`));
+ });
+
+ it('correctly handles multiple results when using getContainerTypeIdByName', async () => {
+ const containerTypes = [
+ ...containerTypeResponse,
+ {
+ _ObjectType_: 'Microsoft.Online.SharePoint.TenantAdministration.SPContainerTypeProperties',
+ ApplicationRedirectUrl: null,
+ AzureSubscriptionId: '/Guid(00000000-0000-0000-0000-000000000000)/',
+ ContainerTypeId: '/Guid(4c8bc473-2d5a-474d-b2f3-fc60b7d39726)/',
+ CreationDate: null,
+ DisplayName: 'test1',
+ ExpiryDate: null,
+ IsBillingProfileRequired: true,
+ OwningAppId: '/Guid(48cc3066-7f0d-4cb9-80fb-f7891069c0f9)/',
+ OwningTenantId: '/Guid(00000000-0000-0000-0000-000000000000)/',
+ Region: null,
+ ResourceGroup: null,
+ SPContainerTypeBillingClassification: 0
+ }
+ ];
+
+ sinon.stub(request, 'post').callsFake(async (opts) => {
+ if (opts.url === `${adminUrl}/_vti_bin/client.svc/ProcessQuery`) {
+ return [
+ {
+ "SchemaVersion": "15.0.0.0", "LibraryVersion": "16.0.24817.12005", "ErrorInfo": null, "TraceCorrelationId": "2d63d39f-3016-0000-a532-30514e76ae73"
+ }, 46, {
+ "IsNull": false
+ }, 47, containerTypes
+ ];
+ }
+
+ throw 'Invalid request';
+ });
+
+ const stubMultiResults = sinon.stub(cli, 'handleMultipleResultsFound').resolves(containerTypes.find(c => c.ContainerTypeId === '/Guid(4c8bc473-2d5a-474d-b2f3-fc60b7d39726)/')!);
+ const actual = await spe.getContainerTypeIdByName(adminUrl, 'test1');
+ assert(stubMultiResults.calledOnce);
+ assert.strictEqual(actual, '4c8bc473-2d5a-474d-b2f3-fc60b7d39726');
+ });
+});
\ No newline at end of file
diff --git a/src/utils/spe.ts b/src/utils/spe.ts
new file mode 100644
index 00000000000..e829dbb1dea
--- /dev/null
+++ b/src/utils/spe.ts
@@ -0,0 +1,79 @@
+import request, { CliRequestOptions } from '../request.js';
+import { ClientSvcResponse, ClientSvcResponseContents } from './spo.js';
+import { formatting } from './formatting.js';
+import { cli } from '../cli/cli.js';
+import config from '../config.js';
+
+export interface ContainerTypeProperties {
+ AzureSubscriptionId: string;
+ ContainerTypeId: string;
+ CreationDate: string;
+ DisplayName: string;
+ ExpiryDate: string;
+ IsBillingProfileRequired: boolean;
+ OwningAppId: string;
+ OwningTenantId: string;
+ Region?: string;
+ ResourceGroup?: string;
+ SPContainerTypeBillingClassification: string;
+}
+
+export const spe = {
+ /**
+ * Get all container types.
+ * @param spoAdminUrl The URL of the SharePoint Online admin center site (e.g. https://contoso-admin.sharepoint.com)
+ * @returns Array of container types
+ */
+ async getAllContainerTypes(spoAdminUrl: string): Promise {
+ const requestOptions: CliRequestOptions = {
+ url: `${spoAdminUrl}/_vti_bin/client.svc/ProcessQuery`,
+ headers: {
+ accept: 'application/json;odata=nometadata'
+ },
+ responseType: 'json',
+ data: `1`
+ };
+
+ const json = await request.post(requestOptions);
+ const response: ClientSvcResponseContents = json[0];
+
+ if (response.ErrorInfo) {
+ throw new Error(response.ErrorInfo.ErrorMessage);
+ }
+
+ const containerTypes: ContainerTypeProperties[] = json[json.length - 1];
+ // Format the response to remove CSOM GUIDs and convert them to real GUIDs
+ containerTypes.forEach(ct => {
+ delete (ct as any)._ObjectType_;
+ ct.AzureSubscriptionId = formatting.extractCsomGuid(ct.AzureSubscriptionId);
+ ct.ContainerTypeId = formatting.extractCsomGuid(ct.ContainerTypeId);
+ ct.OwningAppId = formatting.extractCsomGuid(ct.OwningAppId);
+ ct.OwningTenantId = formatting.extractCsomGuid(ct.OwningTenantId);
+ });
+
+ return containerTypes;
+ },
+
+ /**
+ * Get the ID of a container type by its name.
+ * @param spoAdminUrl SharePoint Online admin center URL (e.g. https://contoso-admin.sharepoint.com)
+ * @param name Name of the container type to search for
+ * @returns ID of the container type
+ */
+ async getContainerTypeIdByName(spoAdminUrl: string, name: string): Promise {
+ const allContainerTypes = await this.getAllContainerTypes(spoAdminUrl);
+ const containerTypes = allContainerTypes.filter(ct => ct.DisplayName.toLowerCase() === name!.toLowerCase());
+
+ if (containerTypes.length === 0) {
+ throw new Error(`The specified container type '${name}' does not exist.`);
+ }
+
+ if (containerTypes.length > 1) {
+ const containerTypeKeyValuePair = formatting.convertArrayToHashTable('ContainerTypeId', containerTypes);
+ const containerType = await cli.handleMultipleResultsFound(`Multiple container types with name '${name}' found.`, containerTypeKeyValuePair);
+ return containerType.ContainerTypeId;
+ }
+
+ return containerTypes[0].ContainerTypeId;
+ }
+};
\ No newline at end of file
diff --git a/src/utils/spo.spec.ts b/src/utils/spo.spec.ts
index 47ca260536d..bc5652b210b 100644
--- a/src/utils/spo.spec.ts
+++ b/src/utils/spo.spec.ts
@@ -88,37 +88,6 @@ const copyJobInfo = {
]
};
-const containerTypedata = [{
- "_ObjectType_": "Microsoft.Online.SharePoint.TenantAdministration.SPContainerTypeProperties",
- "ApplicationRedirectUrl": null,
- "AzureSubscriptionId": "/Guid(00000000-0000-0000-0000-000000000000)/",
- "ContainerTypeId": "/Guid(073269af-f1d2-042d-2ef5-5bdd6ac83115)/",
- "CreationDate": null,
- "DisplayName": "test1",
- "ExpiryDate": null,
- "IsBillingProfileRequired": true,
- "OwningAppId": "/Guid(df4085cc-9a38-4255-badc-5c5225610475)/",
- "OwningTenantId": "/Guid(00000000-0000-0000-0000-000000000000)/",
- "Region": null,
- "ResourceGroup": null,
- "SPContainerTypeBillingClassification": 0
-},
-{
- "_ObjectType_": "Microsoft.Online.SharePoint.TenantAdministration.SPContainerTypeProperties",
- "ApplicationRedirectUrl": null,
- "AzureSubscriptionId": "/Guid(00000000-0000-0000-0000-000000000000)/",
- "ContainerTypeId": "/Guid(880ab3bd-5b68-01d4-3744-01a7656cf2ba)/",
- "CreationDate": null,
- "DisplayName": "test1",
- "ExpiryDate": null,
- "IsBillingProfileRequired": true,
- "OwningAppId": "/Guid(50785fde-3082-47ac-a36d-06282ac5c7da)/",
- "OwningTenantId": "/Guid(00000000-0000-0000-0000-000000000000)/",
- "Region": null,
- "ResourceGroup": null,
- "SPContainerTypeBillingClassification": 0
-}];
-
describe('utils/spo', () => {
let logger: Logger;
let log: string[];
@@ -3358,59 +3327,6 @@ describe('utils/spo', () => {
assert.deepStrictEqual(postStub.firstCall.args[0].data, { url: 'https://contoso.sharepoint.com/sites/sales', includeDetail: true });
});
- it('retrieves list of Container Type', async () => {
- sinon.stub(spo, 'getSpoAdminUrl').resolves('https://contoso-admin.sharepoint.com');
- sinon.stub(spo, 'ensureFormDigest').resolves({ FormDigestValue: 'abc', FormDigestTimeoutSeconds: 1800, FormDigestExpiresAt: new Date(), WebFullUrl: 'https://contoso.sharepoint.com' });
-
- sinon.stub(request, 'post').callsFake(async (opts) => {
- if ((opts.url as string).indexOf(`/_vti_bin/client.svc/ProcessQuery`) > -1) {
- if (opts.headers &&
- opts.headers['X-RequestDigest'] &&
- opts.headers['X-RequestDigest'] === 'abc' &&
- opts.data === `1`) {
- return JSON.stringify([
- {
- "SchemaVersion": "15.0.0.0", "LibraryVersion": "16.0.24817.12005", "ErrorInfo": null, "TraceCorrelationId": "2d63d39f-3016-0000-a532-30514e76ae73"
- }, 46, {
- "IsNull": false
- }, 47, containerTypedata
- ]);
- }
- }
-
- throw 'Invalid request';
- });
-
- const containerTypeList = await spo.getAllContainerTypes('https://contoso-admin.sharepoint.com', logger, true);
- assert.deepEqual(containerTypeList, containerTypedata);
- });
-
- it('correctly throws error when retrieving container types', async () => {
- sinon.stub(spo, 'getSpoAdminUrl').resolves('https://contoso-admin.sharepoint.com');
- sinon.stub(spo, 'ensureFormDigest').resolves({ FormDigestValue: 'abc', FormDigestTimeoutSeconds: 1800, FormDigestExpiresAt: new Date(), WebFullUrl: 'https://contoso.sharepoint.com' });
-
- sinon.stub(request, 'post').callsFake(async (opts) => {
- if ((opts.url as string).indexOf(`/_vti_bin/client.svc/ProcessQuery`) > -1) {
- if (opts.headers &&
- opts.headers['X-RequestDigest'] &&
- opts.headers['X-RequestDigest'] === 'abc') {
-
- return JSON.stringify([
- {
- "SchemaVersion": "15.0.0.0", "LibraryVersion": "16.0.7324.1200", "ErrorInfo": {
- "ErrorMessage": "An error has occurred", "ErrorValue": null, "TraceCorrelationId": "e13c489e-2026-5000-8242-7ec96d02ba1d", "ErrorCode": -1, "ErrorTypeName": "SPException"
- }, "TraceCorrelationId": "e13c489e-2026-5000-8242-7ec96d02ba1d"
- }
- ]);
- }
- }
-
- throw 'Invalid request';
- });
-
- await assert.rejects(spo.getAllContainerTypes('https://contoso-admin.sharepoint.com', logger, true), 'An error occured');
- });
-
it('retrieves a roledefintion by its name', async () => {
sinon.stub(request, 'get').callsFake(async opts => {
if (opts.url === `https://contoso.sharepoint.com/sites/sales/_api/web/roledefinitions`) {
diff --git a/src/utils/spo.ts b/src/utils/spo.ts
index 55334c8d889..4d0a225593e 100644
--- a/src/utils/spo.ts
+++ b/src/utils/spo.ts
@@ -258,21 +258,6 @@ interface TenantSiteProperties {
WebsCount: number;
}
-export interface ContainerTypeProperties {
- _ObjectType_?: string;
- AzureSubscriptionId: string;
- ContainerTypeId: string;
- CreationDate: string;
- DisplayName: string;
- ExpiryDate: string;
- IsBillingProfileRequired: boolean;
- OwningAppId: string;
- OwningTenantId: string;
- Region?: string;
- ResourceGroup?: string;
- SPContainerTypeBillingClassification: string;
-}
-
export const spo = {
async getRequestDigest(siteUrl: string): Promise {
const requestOptions: CliRequestOptions = {
@@ -307,29 +292,6 @@ export const spo = {
return context;
},
- async getAllContainerTypes(spoAdminUrl: string, logger: Logger, verbose: boolean): Promise {
- const formDigestInfo: FormDigestInfo = await spo.ensureFormDigest(spoAdminUrl, logger, undefined, verbose);
-
- const requestOptions: CliRequestOptions = {
- url: `${spoAdminUrl}/_vti_bin/client.svc/ProcessQuery`,
- headers: {
- 'X-RequestDigest': formDigestInfo.FormDigestValue
- },
- data: `1`
- };
-
- const res: string = await request.post(requestOptions);
- const json: ClientSvcResponse = JSON.parse(res);
- const response: ClientSvcResponseContents = json[0];
-
- if (response.ErrorInfo) {
- throw new Error(response.ErrorInfo.ErrorMessage);
- }
-
- const containerTypes: ContainerTypeProperties[] = json[json.length - 1];
- return containerTypes;
- },
-
async waitUntilFinished({ operationId, siteUrl, logger, currentContext, debug, verbose }: { operationId: string, siteUrl: string, logger: Logger, currentContext: FormDigestInfo, debug: boolean, verbose: boolean }): Promise {
const resFormDigest = await spo.ensureFormDigest(siteUrl, logger, currentContext, debug);
currentContext = resFormDigest;