diff --git a/docs/docs/cmd/spe/container/container-get.mdx b/docs/docs/cmd/spe/container/container-get.mdx index d9e118ea3e1..054b5b3b1d4 100644 --- a/docs/docs/cmd/spe/container/container-get.mdx +++ b/docs/docs/cmd/spe/container/container-get.mdx @@ -20,6 +20,12 @@ m365 spe container get [options] `-n, --name [name]` : Display name of the container. Specify either `id` or `name` but not both. + +`--containerTypeId [containerTypeId]` +: The container type ID of the container instance. Specify either `containerTypeId` or `containerTypeName` when using `name` but not both. + +`--containerTypeName [containerTypeName]` +: The container type name of the container instance. Specify either `containerTypeId` or `containerTypeName` when using `name` but not both. ``` @@ -54,7 +60,13 @@ m365 spe container get --id "b!ISJs1WRro0y0EWgkUYcktDa0mE8zSlFEqFzqRn70Zwp1CEtDE Gets a container of a specific type by display name. ```sh -m365 spe container get --name "My Application Storage Container" +m365 spe container get --name "My Application Storage Container" --containerTypeId "91710488-5756-407f-9046-fbe5f0b4de73" +``` + +Gets container by using its name and container type name. + +```sh +m365 spe container get --name "Invoices" --containerTypeName "My container type name" ``` ## Response diff --git a/docs/docs/cmd/spe/container/container-permission-list.mdx b/docs/docs/cmd/spe/container/container-permission-list.mdx index ad7a25bc701..427e2d880d7 100644 --- a/docs/docs/cmd/spe/container/container-permission-list.mdx +++ b/docs/docs/cmd/spe/container/container-permission-list.mdx @@ -15,8 +15,17 @@ m365 spe container permission list [options] ## Options ```md definition-list -`-i, --containerId ` -: The ID of the SharePoint Embedded Container. +`-i, --containerId [id]` +: The ID of the SharePoint Embedded Container. Specify either `containerId` or `containerName` but not both. + +`-n, --containerName [containerName]` +: Display name of the SharePoint Embedded Container. Specify either `containerId` or `containerName` but not both. + +`--containerTypeId [containerTypeId]` +: The container type ID of the container instance. Specify either `containerTypeId` or `containerTypeName` when using `containerName` but not both. + +`--containerTypeName [containerTypeName]` +: The container type name of the container instance. Specify either `containerTypeId` or `containerTypeName` when using `containerName` but not both. ``` @@ -42,12 +51,24 @@ m365 spe container permission list [options] ## Examples -Lists Container permissions. +Lists Container permissions by id. ```sh m365 spe container permission list --containerId "b!ISJs1WRro0y0EWgkUYcktDa0mE8zSlFEqFzqRn70Zwp1CEtDEBZgQICPkRbil_5Z" ``` +Lists Container permissions by display name. + +```sh +m365 spe container permission list --containerName "My Application Storage Container" --containerTypeId "91710488-5756-407f-9046-fbe5f0b4de73" +``` + +Lists Container permissions by display name and container type name. + +```sh +m365 spe container permission list --containerName "My Application Storage Container" --containerTypeName "My container type name" +``` + ## Response diff --git a/src/m365/spe/commands/container/container-permission-list.spec.ts b/src/m365/spe/commands/container/container-permission-list.spec.ts index 32cf0695669..82c1f41d8e8 100644 --- a/src/m365/spe/commands/container/container-permission-list.spec.ts +++ b/src/m365/spe/commands/container/container-permission-list.spec.ts @@ -3,20 +3,26 @@ import sinon from 'sinon'; import auth from '../../../../Auth.js'; import { Logger } from '../../../../cli/Logger.js'; import { CommandError } from '../../../../Command.js'; -import request from '../../../../request.js'; import { telemetry } from '../../../../telemetry.js'; import { pid } from '../../../../utils/pid.js'; import { sinonUtil } from '../../../../utils/sinonUtil.js'; import commands from '../../commands.js'; import command from './container-permission-list.js'; -import { formatting } from '../../../../utils/formatting.js'; +import { z } from 'zod'; +import { spe } from '../../../../utils/spe.js'; +import { odata } from '../../../../utils/odata.js'; +import { cli } from '../../../../cli/cli.js'; describe(commands.CONTAINER_PERMISSION_LIST, () => { let log: string[]; let logger: Logger; let loggerLogSpy: sinon.SinonSpy; + let loggerLogToStderrSpy: sinon.SinonSpy; + let schema: z.ZodTypeAny; const containerId = "b!ISJs1WRro0y0EWgkUYcktDa0mE8zSlFEqFzqRn70Zwp1CEtDEBZgQICPkRbil_5Z"; + const containerName = 'My Application Storage Container'; + const containerTypeId = 'b2e2cef4-9ac1-4b3b-b4a5-2a2e3a2e2a2e'; const containerPermissionResponse = { "value": [ { @@ -66,6 +72,7 @@ describe(commands.CONTAINER_PERMISSION_LIST, () => { sinon.stub(telemetry, 'trackEvent').resolves(); sinon.stub(pid, 'getProcessName').returns(''); auth.connection.active = true; + schema = command.getSchemaToParse()!; }); beforeEach(() => { @@ -82,12 +89,18 @@ describe(commands.CONTAINER_PERMISSION_LIST, () => { } }; loggerLogSpy = sinon.spy(logger, 'log'); + loggerLogToStderrSpy = sinon.spy(logger, 'logToStderr'); }); afterEach(() => { sinonUtil.restore([ - request.get + spe.getContainerIdByName, + spe.getContainerTypeIdByName, + odata.getAllItems, + cli.handleMultipleResultsFound ]); + loggerLogSpy.restore(); + loggerLogToStderrSpy.restore(); }); after(() => { @@ -108,13 +121,7 @@ describe(commands.CONTAINER_PERMISSION_LIST, () => { }); it('correctly lists permissions of a SharePoint Embedded Container', async () => { - sinon.stub(request, 'get').callsFake(async (opts) => { - if (opts.url === `https://graph.microsoft.com/v1.0/storage/fileStorage/containers/${formatting.encodeQueryParameter(containerId)}/permissions`) { - return containerPermissionResponse; - } - - throw 'Invalid request'; - }); + sinon.stub(odata, 'getAllItems').resolves(containerPermissionResponse.value); await command.action(logger, { options: { @@ -127,13 +134,7 @@ describe(commands.CONTAINER_PERMISSION_LIST, () => { }); it('correctly lists permissions of a SharePoint Embedded Container (TEXT)', async () => { - sinon.stub(request, 'get').callsFake(async (opts) => { - if (opts.url === `https://graph.microsoft.com/v1.0/storage/fileStorage/containers/${formatting.encodeQueryParameter(containerId)}/permissions`) { - return containerPermissionResponse; - } - - throw 'Invalid request'; - }); + sinon.stub(odata, 'getAllItems').resolves(containerPermissionResponse.value); await command.action(logger, { options: { @@ -146,8 +147,114 @@ describe(commands.CONTAINER_PERMISSION_LIST, () => { assert(loggerLogSpy.calledWith(textOutput)); }); + it('correctly lists permissions of a SharePoint Embedded Container by name', async () => { + sinon.stub(odata, 'getAllItems').onFirstCall().resolves([ + { + id: containerId, + displayName: containerName + } + ]).onSecondCall().resolves(containerPermissionResponse.value); + + await command.action(logger, { + options: { + containerName, + containerTypeId, + debug: true + } + }); + + assert(loggerLogSpy.calledWith(containerPermissionResponse.value)); + }); + + it('logs progress when resolving container id by name in verbose mode', async () => { + sinon.stub(odata, 'getAllItems').onFirstCall().resolves([ + { + id: containerId, + displayName: containerName + } + ]).onSecondCall().resolves(containerPermissionResponse.value); + + await command.action(logger, { + options: { + containerName, + containerTypeId, + verbose: true + } + }); + + assert(loggerLogToStderrSpy.calledWith(`Resolving container id from name '${containerName}'...`)); + }); + + it('fails when container with specified name does not exist', async () => { + sinon.stub(odata, 'getAllItems').resolves([]); + + await assert.rejects( + command.action(logger, { + options: { + containerName, + containerTypeId + } + }), + new CommandError(`The specified container '${containerName}' does not exist.`) + ); + }); + + it('handles multiple containers with same name when resolving id', async () => { + sinon.stub(odata, 'getAllItems').onFirstCall().resolves([ + { + id: '1', + displayName: containerName + }, + { + id: containerId, + displayName: containerName + } + ]).onSecondCall().resolves(containerPermissionResponse.value); + sinon.stub(cli, 'handleMultipleResultsFound').resolves({ + id: containerId + }); + + await command.action(logger, { + options: { + containerName, + containerTypeId + } + }); + + assert(loggerLogSpy.calledWith(containerPermissionResponse.value)); + }); + + it('rethrows unexpected errors when resolving container id by name', async () => { + sinon.stub(odata, 'getAllItems').rejects({ + error: { + 'odata.error': { + message: { + value: 'unexpected error' + } + } + } + }); + + await assert.rejects(command.action(logger, { + options: { + containerName, + containerTypeId + } + }), new CommandError('unexpected error')); + }); + + it('rethrows CommandError thrown during command execution', async () => { + sinon.stub(odata, 'getAllItems').rejects(new CommandError('command error')); + + await assert.rejects(command.action(logger, { + options: { + containerId + } + }), new CommandError('command error')); + }); + it('correctly handles error when SharePoint Embedded Container is not found', async () => { - sinon.stub(request, 'get').rejects({ + sinon.stub(odata, 'getAllItems').rejects({ error: { 'odata.error': { message: { value: 'Item Not Found.' } } } }); @@ -157,7 +264,7 @@ describe(commands.CONTAINER_PERMISSION_LIST, () => { it('correctly handles error when retrieving permissions of a SharePoint Embedded Container', async () => { const error = 'An error has occurred'; - sinon.stub(request, 'get').rejects(new Error(error)); + sinon.stub(odata, 'getAllItems').rejects(new Error(error)); await assert.rejects(command.action(logger, { options: { @@ -165,4 +272,70 @@ describe(commands.CONTAINER_PERMISSION_LIST, () => { } }), new CommandError(error)); }); -}); \ No newline at end of file + + it('fails validation when neither containerId nor containerName is specified', () => { + const result = schema.safeParse({}); + assert.strictEqual(result.success, false); + assert(result.error?.issues.some(issue => issue.message.includes('Specify either id or name'))); + }); + + it('fails validation when both containerId and containerName are specified', () => { + const result = schema.safeParse({ + containerId, + containerName + }); + assert.strictEqual(result.success, false); + assert(result.error?.issues.some(issue => issue.message.includes('Specify either id or name'))); + }); + + it('passes validation when only containerId is specified', () => { + const result = schema.safeParse({ containerId }); + assert.strictEqual(result.success, true); + }); + + it('passes validation when containerName and containerTypeId are specified', () => { + const result = schema.safeParse({ containerName, containerTypeId }); + assert.strictEqual(result.success, true); + }); + + it('correctly lists permissions of a SharePoint Embedded Container by containerTypeName', async () => { + const containerTypeName = 'My Container Type'; + sinon.stub(spe, 'getContainerTypeIdByName').resolves(containerTypeId); + sinon.stub(odata, 'getAllItems').onFirstCall().resolves([ + { + id: containerId, + displayName: containerName + } + ]).onSecondCall().resolves(containerPermissionResponse.value); + + await command.action(logger, { + options: { + containerName, + containerTypeName + } + }); + + assert(loggerLogSpy.calledWith(containerPermissionResponse.value)); + }); + + it('logs progress when getting container type by name in verbose mode', async () => { + const containerTypeName = 'My Container Type'; + sinon.stub(spe, 'getContainerTypeIdByName').resolves(containerTypeId); + sinon.stub(odata, 'getAllItems').onFirstCall().resolves([ + { + id: containerId, + displayName: containerName + } + ]).onSecondCall().resolves(containerPermissionResponse.value); + + await command.action(logger, { + options: { + containerName, + containerTypeName, + verbose: true + } + }); + + assert(loggerLogToStderrSpy.calledWith(`Getting container type with name '${containerTypeName}'...`)); + }); +}); diff --git a/src/m365/spe/commands/container/container-permission-list.ts b/src/m365/spe/commands/container/container-permission-list.ts index 63111bab0ae..6a8c4497b7a 100644 --- a/src/m365/spe/commands/container/container-permission-list.ts +++ b/src/m365/spe/commands/container/container-permission-list.ts @@ -1,16 +1,21 @@ import { cli } from '../../../../cli/cli.js'; import { z } from 'zod'; import { Logger } from '../../../../cli/Logger.js'; -import { globalOptionsZod } from '../../../../Command.js'; +import { CommandError, globalOptionsZod } from '../../../../Command.js'; import commands from '../../commands.js'; import GraphCommand from '../../../base/GraphCommand.js'; import { odata } from '../../../../utils/odata.js'; import { formatting } from '../../../../utils/formatting.js'; +import { spe } from '../../../../utils/spe.js'; export const options = z.strictObject({ ...globalOptionsZod.shape, - containerId: z.string().alias('i') + containerId: z.string().alias('i').optional(), + containerName: z.string().alias('n').optional(), + containerTypeId: z.uuid().optional(), + containerTypeName: z.string().optional() }); + declare type Options = z.infer; interface CommandArgs { @@ -34,13 +39,28 @@ class SpeContainerPermissionListCommand extends GraphCommand { return options; } + public getRefinedSchema(schema: typeof options): z.ZodObject | undefined { + return schema + .refine((opts: Options) => [opts.containerId, opts.containerName].filter(value => value !== undefined).length === 1, { + message: 'Specify either id or name, but not both.' + }) + .refine((options: Options) => !options.containerName || [options.containerTypeId, options.containerTypeName].filter(o => o !== undefined).length === 1, { + error: 'Use one of the following options when specifying the container name: containerTypeId or containerTypeName.' + }) + .refine((options: Options) => options.containerName || [options.containerTypeId, options.containerTypeName].filter(o => o !== undefined).length === 0, { + error: 'Options containerTypeId and containerTypeName are only required when retrieving a container by name.' + }); + } + public async commandAction(logger: Logger, args: CommandArgs): Promise { try { + const containerId = await this.resolveContainerId(args.options, logger); + if (this.verbose) { - await logger.logToStderr(`Retrieving permissions of a SharePoint Embedded Container with id '${args.options.containerId}'...`); + await logger.logToStderr(`Retrieving permissions of a SharePoint Embedded Container with id '${containerId}'...`); } - const containerPermission = await odata.getAllItems(`${this.resource}/v1.0/storage/fileStorage/containers/${formatting.encodeQueryParameter(args.options.containerId)}/permissions`); + const containerPermission = await odata.getAllItems(`${this.resource}/v1.0/storage/fileStorage/containers/${formatting.encodeQueryParameter(containerId)}/permissions`); if (!cli.shouldTrimOutput(args.options.output)) { await logger.log(containerPermission); @@ -56,9 +76,37 @@ class SpeContainerPermissionListCommand extends GraphCommand { } } catch (err: any) { + if (err instanceof CommandError) { + throw err; + } this.handleRejectedODataJsonPromise(err); } } + + private async resolveContainerId(options: Options, logger: Logger): Promise { + if (options.containerId) { + return options.containerId; + } + + if (this.verbose) { + await logger.logToStderr(`Resolving container id from name '${options.containerName}'...`); + } + + const containerTypeId = await this.getContainerTypeId(options, logger); + return spe.getContainerIdByName(containerTypeId, options.containerName!); + } + + private async getContainerTypeId(options: Options, logger: Logger): Promise { + if (options.containerTypeId) { + return options.containerTypeId; + } + + if (this.verbose) { + await logger.logToStderr(`Getting container type with name '${options.containerTypeName}'...`); + } + + return spe.getContainerTypeIdByName(options.containerTypeName!); + } } -export default new SpeContainerPermissionListCommand(); \ No newline at end of file +export default new SpeContainerPermissionListCommand();