diff --git a/src/m365/spe/commands/container/container-activate.spec.ts b/src/m365/spe/commands/container/container-activate.spec.ts index 3c16b1a70f4..8420ae23e8e 100644 --- a/src/m365/spe/commands/container/container-activate.spec.ts +++ b/src/m365/spe/commands/container/container-activate.spec.ts @@ -8,13 +8,17 @@ 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-activate.js'; +import command, { options } from './container-activate.js'; import { CommandError } from '../../../../Command.js'; import { formatting } from '../../../../utils/formatting.js'; +import { CommandInfo } from '../../../../cli/CommandInfo.js'; +import { cli } from '../../../../cli/cli.js'; describe(commands.CONTAINER_ACTIVATE, () => { let log: string[]; let logger: Logger; + let commandInfo: CommandInfo; + let commandOptionsSchema: typeof options; const containerId = 'b!ISJs1WRro0y0EWgkUYcktDa0mE8zSlFEqFzqRn70Zwp1CEtDEBZgQICPkRbil_5Z'; @@ -24,6 +28,8 @@ describe(commands.CONTAINER_ACTIVATE, () => { sinon.stub(pid, 'getProcessName').returns(''); sinon.stub(session, 'getId').returns(''); auth.connection.active = true; + commandInfo = cli.getCommandInfo(command); + commandOptionsSchema = commandInfo.command.getSchemaToParse() as typeof options; }); beforeEach(() => { @@ -60,6 +66,21 @@ describe(commands.CONTAINER_ACTIVATE, () => { assert.notStrictEqual(command.description, null); }); + it('fails validation when id is not specified', () => { + const actual = commandOptionsSchema.safeParse({ + verbose: true + }); + assert.strictEqual(actual.success, false); + }); + + it('fails validation with unknown options', () => { + const actual = commandOptionsSchema.safeParse({ + id: containerId, + unknownOption: 'value' + }); + assert.strictEqual(actual.success, false); + }); + it('activates container by id', async () => { const postStub = sinon.stub(request, 'post').callsFake(async (opts) => { if (opts.url === `https://graph.microsoft.com/v1.0/storage/fileStorage/containers/${formatting.encodeQueryParameter(containerId)}/activate`) { @@ -69,7 +90,7 @@ describe(commands.CONTAINER_ACTIVATE, () => { throw 'Invalid request'; }); - await command.action(logger, { options: { id: containerId, verbose: true } }); + await command.action(logger, { options: commandOptionsSchema.parse({ id: containerId, verbose: true }) }); assert(postStub.calledOnce); }); @@ -94,7 +115,7 @@ describe(commands.CONTAINER_ACTIVATE, () => { throw 'Invalid request'; }); - await assert.rejects(command.action(logger, { options: { id: containerId, verbose: true } } as any), + await assert.rejects(command.action(logger, { options: commandOptionsSchema.parse({ id: containerId, verbose: true }) }), new CommandError(error.error.message)); }); }); \ No newline at end of file diff --git a/src/m365/spe/commands/container/container-activate.ts b/src/m365/spe/commands/container/container-activate.ts index 33102c2da02..ff74b1399d2 100644 --- a/src/m365/spe/commands/container/container-activate.ts +++ b/src/m365/spe/commands/container/container-activate.ts @@ -1,18 +1,22 @@ -import GlobalOptions from '../../../../GlobalOptions.js'; +import { z } from 'zod'; import { Logger } from '../../../../cli/Logger.js'; +import { globalOptionsZod } from '../../../../Command.js'; import GraphCommand from '../../../base/GraphCommand.js'; import commands from '../../commands.js'; import request, { CliRequestOptions } from '../../../../request.js'; import { formatting } from '../../../../utils/formatting.js'; +export const options = z.strictObject({ + ...globalOptionsZod.shape, + id: z.string().alias('i') +}); + +declare type Options = z.infer; + interface CommandArgs { options: Options; } -interface Options extends GlobalOptions { - id: string; -} - class SpeContainerActivateCommand extends GraphCommand { public get name(): string { return commands.CONTAINER_ACTIVATE; @@ -22,21 +26,8 @@ class SpeContainerActivateCommand extends GraphCommand { return 'Activates a container'; } - constructor() { - super(); - - this.#initOptions(); - this.#initTypes(); - } - - #initOptions(): void { - this.options.unshift( - { option: '-i, --id ' } - ); - } - - #initTypes(): void { - this.types.string.push('id'); + public get schema(): z.ZodTypeAny { + return options; } public async commandAction(logger: Logger, args: CommandArgs): Promise { diff --git a/src/m365/spe/commands/container/container-get.spec.ts b/src/m365/spe/commands/container/container-get.spec.ts index 83eb3f925e9..36479901853 100644 --- a/src/m365/spe/commands/container/container-get.spec.ts +++ b/src/m365/spe/commands/container/container-get.spec.ts @@ -170,7 +170,7 @@ describe(commands.CONTAINER_GET, () => { throw 'Invalid GET request: ' + opts.url; }); - await command.action(logger, { options: { name: containerName, containerTypeId: containerTypeId } }); + await command.action(logger, { options: commandOptionsSchema.parse({ name: containerName, containerTypeId: containerTypeId }) }); assert.deepStrictEqual(loggerLogSpy.lastCall.args[0], containerResponse); }); @@ -197,7 +197,7 @@ describe(commands.CONTAINER_GET, () => { throw 'Invalid GET request: ' + opts.url; }); - await command.action(logger, { options: { name: containerName, containerTypeName: containerTypeName, verbose: true } }); + await command.action(logger, { options: commandOptionsSchema.parse({ name: containerName, containerTypeName: containerTypeName, verbose: true }) }); assert.deepStrictEqual(loggerLogSpy.lastCall.args[0], containerResponse); }); @@ -206,7 +206,7 @@ describe(commands.CONTAINER_GET, () => { value: deletedContainersResponse }); - await assert.rejects(command.action(logger, { options: { name: 'Non-existing container', containerTypeId: containerTypeId } }), + await assert.rejects(command.action(logger, { options: commandOptionsSchema.parse({ name: 'Non-existing container', containerTypeId: containerTypeId }) }), new CommandError(`The specified container 'Non-existing container' does not exist.`)); }); @@ -232,7 +232,7 @@ describe(commands.CONTAINER_GET, () => { }); const stubMultiResults = sinon.stub(cli, 'handleMultipleResultsFound').resolves(deletedContainersResponse.find(c => c.id === containerId)!); - await command.action(logger, { options: { name: containerName, containerTypeId: containerTypeId } }); + await command.action(logger, { options: commandOptionsSchema.parse({ name: containerName, containerTypeId: containerTypeId }) }); assert(stubMultiResults.calledOnce); }); @@ -245,7 +245,7 @@ describe(commands.CONTAINER_GET, () => { } }); - await assert.rejects(command.action(logger, { options: { id: containerId } }), + await assert.rejects(command.action(logger, { options: commandOptionsSchema.parse({ id: containerId }) }), new CommandError(errorMessage)); }); }); diff --git a/src/m365/spe/commands/container/container-list.spec.ts b/src/m365/spe/commands/container/container-list.spec.ts index 38404f4054c..5930cf465ad 100644 --- a/src/m365/spe/commands/container/container-list.spec.ts +++ b/src/m365/spe/commands/container/container-list.spec.ts @@ -8,7 +8,7 @@ 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 command, { options } from './container-list.js'; import { CommandError } from '../../../../Command.js'; import { CommandInfo } from '../../../../cli/CommandInfo.js'; import { cli } from '../../../../cli/cli.js'; @@ -19,6 +19,7 @@ describe(commands.CONTAINER_LIST, () => { let logger: Logger; let loggerLogSpy: sinon.SinonSpy; let commandInfo: CommandInfo; + let commandOptionsSchema: typeof options; const containersList = [{ "id": "b!ISJs1WRro0y0EWgkUYcktDa0mE8zSlFEqFzqRn70Zwp1CEtDEBZgQICPkRbil_5Z", @@ -41,6 +42,7 @@ describe(commands.CONTAINER_LIST, () => { auth.connection.active = true; commandInfo = cli.getCommandInfo(command); + commandOptionsSchema = commandInfo.command.getSchemaToParse() as typeof options; }); beforeEach(() => { @@ -86,14 +88,35 @@ describe(commands.CONTAINER_LIST, () => { 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('fails validation if the containerTypeId is not a valid guid', () => { + const result = commandOptionsSchema.safeParse({ containerTypeId: 'abc' }); + assert.strictEqual(result.success, false); }); - 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('fails validation when neither containerTypeId nor containerTypeName is specified', () => { + const result = commandOptionsSchema.safeParse({}); + assert.strictEqual(result.success, false); + }); + + it('fails validation when both containerTypeId and containerTypeName are specified', () => { + const result = commandOptionsSchema.safeParse({ + containerTypeId: 'e2756c4d-fa33-4452-9c36-2325686e1082', + containerTypeName: 'standard container' + }); + assert.strictEqual(result.success, false); + }); + + it('fails validation when unknown option is specified', () => { + const result = commandOptionsSchema.safeParse({ + containerTypeId: 'e2756c4d-fa33-4452-9c36-2325686e1082', + unknownOption: 'value' + }); + assert.strictEqual(result.success, false); + }); + + it('passes validation if valid containerTypeId is specified', () => { + const result = commandOptionsSchema.safeParse({ containerTypeId: "e2756c4d-fa33-4452-9c36-2325686e1082" }); + assert.strictEqual(result.success, true); }); it('retrieves list of container type by id', async () => { @@ -105,7 +128,7 @@ describe(commands.CONTAINER_LIST, () => { throw 'Invalid request'; }); - await command.action(logger, { options: { containerTypeId: "e2756c4d-fa33-4452-9c36-2325686e1082", verbose: true } }); + await command.action(logger, { options: commandOptionsSchema.parse({ containerTypeId: "e2756c4d-fa33-4452-9c36-2325686e1082", verbose: true }) }); assert(loggerLogSpy.calledWith(containersList)); }); @@ -118,7 +141,7 @@ describe(commands.CONTAINER_LIST, () => { throw 'Invalid request'; }); - await command.action(logger, { options: { containerTypeName: "standard container", verbose: true } }); + await command.action(logger, { options: commandOptionsSchema.parse({ containerTypeName: "standard container", verbose: true }) }); assert(loggerLogSpy.calledWith(containersList)); }); @@ -128,10 +151,10 @@ describe(commands.CONTAINER_LIST, () => { sinon.stub(spe, 'getContainerTypeIdByName').rejects(new Error(error)); await assert.rejects(command.action(logger, { - options: { + options: commandOptionsSchema.parse({ containerTypeName: "nonexisting container", verbose: 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 index d01743d5d5f..5fae07e5130 100644 --- a/src/m365/spe/commands/container/container-list.ts +++ b/src/m365/spe/commands/container/container-list.ts @@ -1,82 +1,50 @@ +import { z } from 'zod'; import { Logger } from '../../../../cli/Logger.js'; -import GlobalOptions from '../../../../GlobalOptions.js'; +import { globalOptionsZod } from '../../../../Command.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 { SpeContainer, spe } from '../../../../utils/spe.js'; +export const options = z.strictObject({ + ...globalOptionsZod.shape, + containerTypeId: z.uuid().optional(), + containerTypeName: z.string().optional() +}); + +declare type Options = z.infer; + 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(); + return 'Lists containers of a specific Container Type'; } - #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]' - } - ); + public get schema(): z.ZodTypeAny { + return options; } - #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`; + public getRefinedSchema(schema: typeof options): z.ZodObject | undefined { + return schema + .refine(opts => [opts.containerTypeId, opts.containerTypeName].filter(o => o !== undefined).length === 1, { + message: 'Specify one of the following options: containerTypeId, containerTypeName.', + params: { + customCode: 'optionSet', + options: ['containerTypeId', 'containerTypeName'] } - - return true; - } - ); - } - - #initOptionSets(): void { - this.optionSets.push({ options: ['containerTypeId', 'containerTypeName'] }); + }); } - #initTypes(): void { - this.types.string.push('containerTypeId', 'containerTypeName'); + public defaultProperties(): string[] | undefined { + return ['id', 'displayName', 'containerTypeId', 'createdDateTime']; } public async commandAction(logger: Logger, args: CommandArgs): Promise { diff --git a/src/m365/spe/commands/containertype/containertype-list.spec.ts b/src/m365/spe/commands/containertype/containertype-list.spec.ts index 3f6008783c1..8bbe214a203 100644 --- a/src/m365/spe/commands/containertype/containertype-list.spec.ts +++ b/src/m365/spe/commands/containertype/containertype-list.spec.ts @@ -7,10 +7,12 @@ 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 './containertype-list.js'; +import command, { options } from './containertype-list.js'; import { CommandError } from '../../../../Command.js'; import { odata } from '../../../../utils/odata.js'; import { accessToken } from '../../../../utils/accessToken.js'; +import { cli } from '../../../../cli/cli.js'; +import { CommandInfo } from '../../../../cli/CommandInfo.js'; const containerTypeData = [ { @@ -57,6 +59,8 @@ describe(commands.CONTAINERTYPE_LIST, () => { let log: string[]; let logger: Logger; let loggerLogSpy: sinon.SinonSpy; + let commandInfo: CommandInfo; + let commandOptionsSchema: typeof options; before(() => { sinon.stub(auth, 'restoreAuth').resolves(); @@ -65,6 +69,8 @@ describe(commands.CONTAINERTYPE_LIST, () => { sinon.stub(session, 'getId').returns(''); sinon.stub(accessToken, 'assertAccessTokenType').withArgs('delegated').returns(); auth.connection.active = true; + commandInfo = cli.getCommandInfo(command); + commandOptionsSchema = commandInfo.command.getSchemaToParse() as typeof options; }); beforeEach(() => { @@ -115,7 +121,7 @@ describe(commands.CONTAINERTYPE_LIST, () => { throw 'Invalid GET request ' + url; }); - await command.action(logger, { options: { verbose: true } }); + await command.action(logger, { options: commandOptionsSchema.parse({ verbose: true }) }); assert(loggerLogSpy.calledOnceWith(containerTypeData)); }); @@ -124,9 +130,9 @@ describe(commands.CONTAINERTYPE_LIST, () => { sinon.stub(odata, 'getAllItems').rejects(new Error(error)); await assert.rejects(command.action(logger, { - options: { + options: commandOptionsSchema.parse({ verbose: true - } + }) }), 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 5582ae97017..f73ada51ca7 100644 --- a/src/m365/spe/commands/containertype/containertype-list.ts +++ b/src/m365/spe/commands/containertype/containertype-list.ts @@ -1,8 +1,12 @@ +import { z } from 'zod'; import { Logger } from '../../../../cli/Logger.js'; import commands from '../../commands.js'; +import { globalOptionsZod } from '../../../../Command.js'; import GraphDelegatedCommand from '../../../base/GraphDelegatedCommand.js'; import { odata } from '../../../../utils/odata.js'; +export const options = globalOptionsZod.strict(); + class SpeContainerTypeListCommand extends GraphDelegatedCommand { public get name(): string { @@ -13,6 +17,10 @@ class SpeContainerTypeListCommand extends GraphDelegatedCommand { return 'Lists all container types'; } + public get schema(): z.ZodTypeAny { + return options; + } + public defaultProperties(): string[] | undefined { return ['id', 'name', 'owningAppId']; }