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;