diff --git a/docs/docs/cmd/spe/container/container-get.mdx b/docs/docs/cmd/spe/container/container-get.mdx index c6f0078d004..68dafd5546f 100644 --- a/docs/docs/cmd/spe/container/container-get.mdx +++ b/docs/docs/cmd/spe/container/container-get.mdx @@ -15,8 +15,11 @@ m365 spe container get [options] ## Options ```md definition-list -`-i, --id ` -: The Id of the container instance. +`-i, --id [id]` +: The Id of the container instance. Specify either `id` or `name` but not both. + +`-n, --name [name]` +: Display name of the container. Specify either `id` or `name` but not both. ``` @@ -42,12 +45,18 @@ m365 spe container get [options] ## Examples -Gets a container of a specific type. +Gets a container of a specific type by id. ```sh m365 spe container get --id "b!ISJs1WRro0y0EWgkUYcktDa0mE8zSlFEqFzqRn70Zwp1CEtDEBZgQICPkRbil_5Z" ``` +Gets a container of a specific type by display name. + +```sh +m365 spe container get --name "My Application Storage Container" +``` + ## Response diff --git a/src/m365/spe/commands/container/container-get.spec.ts b/src/m365/spe/commands/container/container-get.spec.ts index 95d90f503de..edb27f083a4 100644 --- a/src/m365/spe/commands/container/container-get.spec.ts +++ b/src/m365/spe/commands/container/container-get.spec.ts @@ -1,5 +1,6 @@ import assert from 'assert'; import sinon from 'sinon'; +import { z } from 'zod'; import auth from '../../../../Auth.js'; import { Logger } from '../../../../cli/Logger.js'; import { CommandError } from '../../../../Command.js'; @@ -12,9 +13,24 @@ import commands from '../../commands.js'; import command from './container-get.js'; describe(commands.CONTAINER_GET, () => { + const containerId = 'eyJfdHlwZSI6Ikdyb3VwIiwiaWQiOiIxNTU1MjcwOTQyNzIifQ'; + const containerName = 'My Application Storage Container'; + const containerResponse = { + id: containerId, + displayName: containerName, + description: 'Description of My Application Storage Container', + containerTypeId: '91710488-5756-407f-9046-fbe5f0b4de73', + status: 'active', + createdDateTime: '2021-11-24T15:41:52.347Z', + settings: { + isOcrEnabled: false + } + }; let log: string[]; let logger: Logger; let loggerLogSpy: sinon.SinonSpy; + let loggerLogToStderrSpy: sinon.SinonSpy; + let schema: z.ZodTypeAny; before(() => { sinon.stub(auth, 'restoreAuth').resolves(); @@ -22,6 +38,7 @@ describe(commands.CONTAINER_GET, () => { sinon.stub(pid, 'getProcessName').returns(''); sinon.stub(session, 'getId').returns(''); auth.connection.active = true; + schema = command.getSchemaToParse()!; }); beforeEach(() => { @@ -38,12 +55,13 @@ describe(commands.CONTAINER_GET, () => { } }; loggerLogSpy = sinon.spy(logger, 'log'); + loggerLogToStderrSpy = sinon.spy(logger, 'logToStderr'); }); afterEach(() => { - sinonUtil.restore([ - request.get - ]); + loggerLogSpy.restore(); + loggerLogToStderrSpy.restore(); + sinonUtil.restore([request.get]); }); after(() => { @@ -72,28 +90,89 @@ describe(commands.CONTAINER_GET, () => { }); it('gets container by id', async () => { - const containerId = 'eyJfdHlwZSI6Ikdyb3VwIiwiaWQiOiIxNTU1MjcwOTQyNzIifQ'; - const response = { - id: containerId, - displayName: "My Application Storage Container", - description: "Description of My Application Storage Container", - containerTypeId: "91710488-5756-407f-9046-fbe5f0b4de73", - status: "active", - createdDateTime: "2021-11-24T15:41:52.347Z", - settings: { - isOcrEnabled: false - } - }; - sinon.stub(request, 'get').callsFake(async (opts) => { if (opts.url === `https://graph.microsoft.com/v1.0/storage/fileStorage/containers/${containerId}`) { - return response; + return containerResponse; } throw 'Invalid Request'; }); await command.action(logger, { options: { id: containerId } } as any); - assert.deepStrictEqual(loggerLogSpy.lastCall.args[0], response); + assert.deepStrictEqual(loggerLogSpy.lastCall.args[0], containerResponse); + }); + + it('gets container by name', async () => { + sinon.stub(request, 'get').onFirstCall().resolves({ + value: [containerResponse] + }).onSecondCall().callsFake(async (opts) => { + if (opts.url === `https://graph.microsoft.com/v1.0/storage/fileStorage/containers/${containerId}`) { + return containerResponse; + } + + throw 'Invalid Request'; + }); + + await command.action(logger, { options: { name: containerName } } as any); + assert.deepStrictEqual(loggerLogSpy.lastCall.args[0], containerResponse); + }); + + it('fails when container with specified name does not exist', async () => { + sinon.stub(request, 'get').resolves({ value: [] }); + + await assert.rejects( + command.action(logger, { options: { name: containerName } } as any), + new CommandError(`Container with name '${containerName}' not found.`) + ); + }); + + it('logs progress when resolving container id by name in verbose mode', async () => { + sinon.stub(request, 'get').onFirstCall().resolves({ + value: [containerResponse] + }).onSecondCall().callsFake(async (opts) => { + if (opts.url === `https://graph.microsoft.com/v1.0/storage/fileStorage/containers/${containerId}`) { + return containerResponse; + } + + throw 'Invalid Request'; + }); + + await command.action(logger, { options: { name: containerName, verbose: true } } as any); + assert(loggerLogToStderrSpy.calledWith(`Resolving container id from name '${containerName}'...`)); + }); + + it('throws received error when resolving container id fails with unexpected error', async () => { + const unexpectedError = new Error('Unexpected'); + sinon.stub(request, 'get').rejects(unexpectedError); + + try { + await command.action(logger, { options: { name: containerName } } as any); + assert.fail('Expected command to throw'); + } + catch (err: any) { + assert.strictEqual(err.message, unexpectedError.message); + } + }); + + it('fails validation when neither id nor name 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 id and name are specified', () => { + const result = schema.safeParse({ id: containerId, name: 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 id is specified', () => { + const result = schema.safeParse({ id: containerId }); + assert.strictEqual(result.success, true); + }); + + it('passes validation when only name is specified', () => { + const result = schema.safeParse({ name: containerName }); + assert.strictEqual(result.success, true); }); -}); \ No newline at end of file +}); diff --git a/src/m365/spe/commands/container/container-get.ts b/src/m365/spe/commands/container/container-get.ts index 15a4f597eaf..aed762862bc 100644 --- a/src/m365/spe/commands/container/container-get.ts +++ b/src/m365/spe/commands/container/container-get.ts @@ -1,18 +1,23 @@ -import GlobalOptions from '../../../../GlobalOptions.js'; +import { z } from 'zod'; import { Logger } from '../../../../cli/Logger.js'; +import { CommandError, globalOptionsZod } from '../../../../Command.js'; import GraphCommand from '../../../base/GraphCommand.js'; import commands from '../../commands.js'; import request, { CliRequestOptions } from '../../../../request.js'; import { SpeContainer } from '../../../../utils/spe.js'; +import { zod } from '../../../../utils/zod.js'; + +const options = globalOptionsZod.extend({ + id: zod.alias('i', z.string().optional()), + name: zod.alias('n', z.string().optional()) +}).strict(); + +type Options = z.infer; interface CommandArgs { options: Options; } -interface Options extends GlobalOptions { - id: string; -} - class SpeContainerGetCommand extends GraphCommand { public get name(): string { return commands.CONTAINER_GET; @@ -22,30 +27,55 @@ class SpeContainerGetCommand extends GraphCommand { return 'Gets a container of a specific container type'; } - constructor() { - super(); - - this.#initOptions(); - this.#initTypes(); + public get schema(): z.ZodTypeAny { + return options; } - #initOptions(): void { - this.options.unshift( - { option: '-i, --id ' } - ); + public getRefinedSchema(schema: z.ZodTypeAny): z.ZodEffects | undefined { + return schema.refine((opts: Options) => [opts.id, opts.name].filter(value => value !== undefined).length === 1, { + message: 'Specify either id or name, but not both.' + }); } - #initTypes(): void { - this.types.string.push('id'); + public async commandAction(logger: Logger, args: CommandArgs): Promise { + try { + const containerId = await this.resolveContainerId(args.options, logger); + + if (this.verbose) { + await logger.logToStderr(`Getting a container with id '${containerId}'...`); + } + + const requestOptions: CliRequestOptions = { + url: `${this.resource}/v1.0/storage/fileStorage/containers/${containerId}`, + headers: { + accept: 'application/json;odata.metadata=none' + }, + responseType: 'json' + }; + + const res = await request.get(requestOptions); + await logger.log(res); + } + catch (err: any) { + if (err instanceof CommandError) { + throw err; + } + + this.handleRejectedODataJsonPromise(err); + } } - public async commandAction(logger: Logger, args: CommandArgs): Promise { + private async resolveContainerId(options: Options, logger: Logger): Promise { + if (options.id) { + return options.id; + } + if (this.verbose) { - await logger.logToStderr(`Getting a container with id '${args.options.id}'...`); + await logger.logToStderr(`Resolving container id from name '${options.name}'...`); } const requestOptions: CliRequestOptions = { - url: `${this.resource}/v1.0/storage/fileStorage/containers/${args.options.id}`, + url: `${this.resource}/v1.0/storage/fileStorage/containers`, headers: { accept: 'application/json;odata.metadata=none' }, @@ -53,13 +83,23 @@ class SpeContainerGetCommand extends GraphCommand { }; try { - const res = await request.get(requestOptions); - await logger.log(res); + const response = await request.get<{ value?: SpeContainer[] }>(requestOptions); + const container = response.value?.find(item => item.displayName === options.name); + + if (!container) { + throw new CommandError(`Container with name '${options.name}' not found.`); + } + + return container.id; } - catch (err: any) { - this.handleRejectedODataJsonPromise(err); + catch (error: any) { + if (error instanceof CommandError) { + throw error; + } + + throw error; } } } -export default new SpeContainerGetCommand(); \ No newline at end of file +export default new SpeContainerGetCommand();