diff --git a/docs/docs/cmd/spe/container/container-list.mdx b/docs/docs/cmd/spe/container/container-list.mdx new file mode 100644 index 00000000000..247e2534228 --- /dev/null +++ b/docs/docs/cmd/spe/container/container-list.mdx @@ -0,0 +1,97 @@ +import Global from '/docs/cmd/_global.mdx'; +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +# spe container list + +Lists containers of a specific Container Type + +## Usage + +```sh +m365 spe container 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 containers of a specific type by id. + +```sh +m365 spe container list --containerTypeId "91710488-5756-407f-9046-fbe5f0b4de73" +``` + +List containers of a specific type by name. + +```sh +m365 spe container list --containerTypeName "trial container" +``` + +## Response + + + + + ```json + [ + { + "id": "b!ISJs1WRro0y0EWgkUYcktDa0mE8zSlFEqFzqRn70Zwp1CEtDEBZgQICPkRbil_5Z", + "displayName": "My File Storage Container", + "containerTypeId": "e2756c4d-fa33-4452-9c36-2325686e1082", + "createdDateTime": "2021-11-24T15:41:52.347Z" + } + ] + ``` + + + + + ```text + id displayName containerTypeId createdDateTime + ------------------------------------------------------------------ ------------------------- ------------------------------------ ------------------------ + b!ISJs1WRro0y0EWgkUYcktDa0mE8zSlFEqFzqRn70Zwp1CEtDEBZgQICPkRbil_5Z My File Storage Container e2756c4d-fa33-4452-9c36-2325686e1082 2021-11-24T15:41:52.347Z + ``` + + + + + ```csv + id,displayName,containerTypeId,createdDateTime + b!ISJs1WRro0y0EWgkUYcktDa0mE8zSlFEqFzqRn70Zwp1CEtDEBZgQICPkRbil_5Z,My File Storage Container,e2756c4d-fa33-4452-9c36-2325686e1082,2021-11-24T15:41:52.347Z + ``` + + + + + ```md + # spe container list + + Date: 10/06/2024 + + ## My File Storage Container + + Property | Value + ---------|------- + id | b!ISJs1WRro0y0EWgkUYcktDa0mE8zSlFEqFzqRn70Zwp1CEtDEBZgQICPkRbil_5Z + displayName | My File Storage Container + containerTypeId | e2756c4d-fa33-4452-9c36-2325686e1082 + createdDateTime | 2021-11-24T15:41:52.347Z + ``` + + + + +## More information + +In SharePoint Embedded, all files and documents are stored in Containers. The calling app should be the owning app of the container type. diff --git a/docs/src/config/sidebars.ts b/docs/src/config/sidebars.ts index c995ffe5bdb..87bd48d5c66 100644 --- a/docs/src/config/sidebars.ts +++ b/docs/src/config/sidebars.ts @@ -1960,6 +1960,15 @@ const sidebars: SidebarsConfig = { }, { 'SharePoint Embedded (spe)': [ + { + container: [ + { + type: 'doc', + label: 'container list', + id: 'cmd/spe/container/container-list' + } + ] + }, { containertype: [ { diff --git a/src/m365/spe/ContainerProperties.ts b/src/m365/spe/ContainerProperties.ts new file mode 100644 index 00000000000..b7738cad160 --- /dev/null +++ b/src/m365/spe/ContainerProperties.ts @@ -0,0 +1,6 @@ +export interface ContainerProperties { + id: string; + displayName: string; + containerTypeId: string; + createdDateTime: string; +} \ No newline at end of file diff --git a/src/m365/spe/ContainerTypeProperties.ts b/src/m365/spe/ContainerTypeProperties.ts deleted file mode 100644 index 5b077ef73b7..00000000000 --- a/src/m365/spe/ContainerTypeProperties.ts +++ /dev/null @@ -1,14 +0,0 @@ -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; -} \ No newline at end of file diff --git a/src/m365/spe/commands.ts b/src/m365/spe/commands.ts index ecd05a6e954..ed8e096ffd7 100644 --- a/src/m365/spe/commands.ts +++ b/src/m365/spe/commands.ts @@ -1,6 +1,7 @@ const prefix: string = 'spe'; export default { + CONTAINER_LIST: `${prefix} container list`, CONTAINERTYPE_ADD: `${prefix} containertype add`, CONTAINERTYPE_LIST: `${prefix} containertype list` }; \ No newline at end of file diff --git a/src/m365/spe/commands/container/container-list.spec.ts b/src/m365/spe/commands/container/container-list.spec.ts new file mode 100644 index 00000000000..a5900faf2fa --- /dev/null +++ b/src/m365/spe/commands/container/container-list.spec.ts @@ -0,0 +1,182 @@ +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-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'; + +describe(commands.CONTAINER_LIST, () => { + let log: string[]; + let logger: Logger; + let loggerLogSpy: sinon.SinonSpy; + let commandInfo: CommandInfo; + + const adminUrl = 'https://contoso-admin.sharepoint.com'; + const containersList = [{ + "id": "b!ISJs1WRro0y0EWgkUYcktDa0mE8zSlFEqFzqRn70Zwp1CEtDEBZgQICPkRbil_5Z", + "displayName": "My File Storage Container", + "containerTypeId": "e2756c4d-fa33-4452-9c36-2325686e1082", + "createdDateTime": "2021-11-24T15:41:52.347Z" + }, + { + "id": "b!NdyMBAJ1FEWHB2hEx0DND2dYRB9gz4JOl4rzl7-DuyPG3Fidzm5TTKkyZW2beare", + "displayName": "Trial Container", + "containerTypeId": "e2756c4d-fa33-4452-9c36-2325686e1082", + "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').returns(); + 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); + }); + + 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, + request.post, + spo.getSpoAdminUrl, + spo.getAllContainerTypes + ]); + }); + + after(() => { + sinon.restore(); + auth.connection.active = false; + auth.connection.spoUrl = undefined; + }); + + it('has correct name', () => { + assert.strictEqual(command.name, commands.CONTAINER_LIST); + }); + + it('has a description', () => { + assert.notStrictEqual(command.description, null); + }); + + it('defines correct properties for the default output', () => { + assert.deepStrictEqual(command.defaultProperties(), ['id', 'displayName', 'containerTypeId', 'createdDateTime']); + }); + + it('fails validation if the containerTypeId is not a valid guid', async () => { + const actual = await command.validate({ options: { containerTypeId: 'abc' } }, commandInfo); + assert.notStrictEqual(actual, true); + }); + + it('passes validation if valid containerTypeId is specified', async () => { + const actual = await command.validate({ options: { containerTypeId: "e2756c4d-fa33-4452-9c36-2325686e1082" } }, commandInfo); + assert.strictEqual(actual, true); + }); + + it('retrieves list of container type by id', 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-2325686e1082') { + return { "value": containersList }; + } + + throw 'Invalid request'; + }); + + await command.action(logger, { options: { containerTypeId: "e2756c4d-fa33-4452-9c36-2325686e1082", debug: 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 }; + } + + throw 'Invalid request'; + }); + + await command.action(logger, { options: { containerTypeName: "standard container", debug: 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)); + + await assert.rejects(command.action(logger, { + options: { + debug: true + } + }), new CommandError('An error has occurred')); + }); +}); \ No newline at end of file diff --git a/src/m365/spe/commands/container/container-list.ts b/src/m365/spe/commands/container/container-list.ts new file mode 100644 index 00000000000..3149436ab36 --- /dev/null +++ b/src/m365/spe/commands/container/container-list.ts @@ -0,0 +1,118 @@ +import { Logger } from '../../../../cli/Logger.js'; +import GlobalOptions from '../../../../GlobalOptions.js'; +import { formatting } from '../../../../utils/formatting.js'; +import { odata } from '../../../../utils/odata.js'; +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'; + +interface CommandArgs { + options: Options; +} + +export interface Options extends GlobalOptions { + containerTypeId?: string; + containerTypeName?: string; +} + +class SpeContainerListCommand extends GraphCommand { + public get name(): string { + return commands.CONTAINER_LIST; + } + + public get description(): string { + return 'Lists all Container Types'; + } + + public defaultProperties(): string[] | undefined { + return ['id', 'displayName', 'containerTypeId', 'createdDateTime']; + } + + constructor() { + super(); + + this.#initTelemetry(); + this.#initOptions(); + this.#initValidators(); + this.#initOptionSets(); + this.#initTypes(); + } + + #initTelemetry(): void { + this.telemetry.push((args: CommandArgs) => { + Object.assign(this.telemetryProperties, { + containerTypeId: typeof args.options.containerTypeId !== 'undefined', + containerTypeName: typeof args.options.containerTypeName !== 'undefined' + }); + }); + } + + #initOptions(): void { + this.options.unshift( + { + option: '--containerTypeId [containerTypeId]' + }, + { + option: '--containerTypeName [containerTypeName]' + } + ); + } + + #initValidators(): void { + this.validators.push( + async (args: CommandArgs) => { + if (args.options.containerTypeId && !validation.isValidGuid(args.options.containerTypeId as string)) { + return `${args.options.containerTypeId} is not a valid GUID`; + } + + return true; + } + ); + } + + #initOptionSets(): void { + this.optionSets.push({ options: ['containerTypeId', 'containerTypeName'] }); + } + + #initTypes(): void { + this.types.string.push('containerTypeId', 'containerTypeName'); + } + + public async commandAction(logger: Logger, args: CommandArgs): Promise { + try { + if (this.verbose) { + await logger.logToStderr(`Retrieving list of Containers...`); + } + + const containerTypeId = await this.getContainerTypeId(logger, args.options); + const allContainers = await odata.getAllItems(`${this.resource}/v1.0/storage/fileStorage/containers?$filter=containerTypeId eq ${formatting.encodeQueryParameter(containerTypeId)}`); + await logger.log(allContainers); + } + catch (err: any) { + this.handleRejectedPromise(err); + } + } + + private async getContainerTypeId(logger: Logger, options: Options): Promise { + if (options.containerTypeId) { + return options.containerTypeId; + } + + 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(')')); + } +} + +export default new SpeContainerListCommand(); \ No newline at end of file diff --git a/src/m365/spe/commands/containertype/containertype-list.spec.ts b/src/m365/spe/commands/containertype/containertype-list.spec.ts index 1d161c62fe7..cb21ddc29ed 100644 --- a/src/m365/spe/commands/containertype/containertype-list.spec.ts +++ b/src/m365/spe/commands/containertype/containertype-list.spec.ts @@ -11,42 +11,38 @@ import commands from '../../commands.js'; import command from './containertype-list.js'; import { spo } from '../../../../utils/spo.js'; import { CommandError } from '../../../../Command.js'; -import config from '../../../../config.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" +}, +{ + "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" +}]; describe(commands.CONTAINERTYPE_LIST, () => { let log: string[]; let logger: Logger; let loggerLogSpy: sinon.SinonSpy; - 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 - }]; before(() => { sinon.stub(auth, 'restoreAuth').resolves(); @@ -76,7 +72,8 @@ describe(commands.CONTAINERTYPE_LIST, () => { afterEach(() => { sinonUtil.restore([ - request.post + request.post, + spo.getAllContainerTypes ]); }); @@ -98,81 +95,20 @@ describe(commands.CONTAINERTYPE_LIST, () => { assert.deepStrictEqual(command.defaultProperties(), ['ContainerTypeId', 'DisplayName', 'OwningAppId']); }); - it('correctly handles random API error', async () => { - sinon.stub(request, 'post').rejects(new Error('An error has occurred')); - - await assert.rejects(command.action(logger, { options: { debug: true } } as any), new CommandError("An error has occurred")); - }); - - it('retrieves list of Container Type', 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([ - { - "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'; - }); - - await command.action(logger, { options: {} }); - assert(loggerLogSpy.calledWith(containerTypedata)); - }); - - it('retrieves list of Container Type (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 === `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'; - }); - + it('retrieves list of container type', async () => { + sinon.stub(spo, 'getAllContainerTypes').resolves(containerTypedata); await command.action(logger, { options: { debug: true } }); assert(loggerLogSpy.calledWith(containerTypedata)); }); - it('correctly handles error when retrieving Container Types', 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') { + it('correctly handles error when retrieving container types', async () => { + const error = 'An error has occurred'; + sinon.stub(spo, 'getAllContainerTypes').rejects(new Error(error)); - 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" - } - ]); - } + await assert.rejects(command.action(logger, { + options: { + debug: true } - - throw 'Invalid request'; - }); - - await assert.rejects(command.action(logger, { options: { debug: true } } as any), - new CommandError("An error has occurred.")); + }), new CommandError('An error has occurred')); }); -}); +}); \ No newline at end of file diff --git a/src/m365/spe/commands/containertype/containertype-list.ts b/src/m365/spe/commands/containertype/containertype-list.ts index b55527b9a9e..97e0f573a20 100644 --- a/src/m365/spe/commands/containertype/containertype-list.ts +++ b/src/m365/spe/commands/containertype/containertype-list.ts @@ -1,10 +1,7 @@ import { Logger } from '../../../../cli/Logger.js'; -import config from '../../../../config.js'; -import request, { CliRequestOptions } from '../../../../request.js'; -import { ClientSvcResponse, ClientSvcResponseContents, FormDigestInfo, spo } from '../../../../utils/spo.js'; import SpoCommand from '../../../base/SpoCommand.js'; import commands from '../../commands.js'; -import { ContainerTypeProperties } from '../../ContainerTypeProperties.js'; +import { ContainerTypeProperties, spo } from '../../../../utils/spo.js'; class SpeContainertypeListCommand extends SpoCommand { @@ -28,36 +25,13 @@ class SpeContainertypeListCommand extends SpoCommand { await logger.logToStderr(`Retrieving list of Container types...`); } - const allContainerTypes = await this.getAllContainerTypes(spoAdminUrl, logger); + const allContainerTypes: ContainerTypeProperties[] = await spo.getAllContainerTypes(spoAdminUrl, logger, this.debug); await logger.log(allContainerTypes); } catch (err: any) { this.handleRejectedPromise(err); } } - - private async getAllContainerTypes(spoAdminUrl: string, logger: Logger): Promise { - const formDigestInfo: FormDigestInfo = await spo.ensureFormDigest(spoAdminUrl, logger, undefined, this.debug); - - 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 response.ErrorInfo.ErrorMessage; - } - - const containerTypes: ContainerTypeProperties[] = json[json.length - 1]; - return containerTypes; - } } export default new SpeContainertypeListCommand(); \ No newline at end of file diff --git a/src/utils/spo.spec.ts b/src/utils/spo.spec.ts index f9b7b914885..adbf41a71d0 100644 --- a/src/utils/spo.spec.ts +++ b/src/utils/spo.spec.ts @@ -88,6 +88,37 @@ 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[]; @@ -3137,7 +3168,6 @@ describe('utils/spo', () => { }); await spo.getSiteAdminPropertiesByUrl('https://contoso.sharepoint.com/sites/sales', false, logger, true); - assert.deepStrictEqual(postStub.firstCall.args[0].data, { url: 'https://contoso.sharepoint.com/sites/sales', includeDetail: false }); }); @@ -3154,7 +3184,59 @@ describe('utils/spo', () => { }); await spo.getSiteAdminPropertiesByUrl('https://contoso.sharepoint.com/sites/sales', true, logger, true); - 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'); + }); }); \ No newline at end of file diff --git a/src/utils/spo.ts b/src/utils/spo.ts index 9256d3cce4e..e0d7637d2cd 100644 --- a/src/utils/spo.ts +++ b/src/utils/spo.ts @@ -89,6 +89,21 @@ export interface User { UserPrincipalName: string | null; } +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; +} + interface CreateFileCopyJobsOptions { nameConflictBehavior?: CreateFileCopyJobsNameConflictBehavior; newName?: string; @@ -293,6 +308,29 @@ 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;