diff --git a/src/m365/entra/commands/multitenant/multitenant-get.spec.ts b/src/m365/entra/commands/multitenant/multitenant-get.spec.ts index 4a1e5f7735f..3ed05bf3de0 100644 --- a/src/m365/entra/commands/multitenant/multitenant-get.spec.ts +++ b/src/m365/entra/commands/multitenant/multitenant-get.spec.ts @@ -1,20 +1,22 @@ import assert from 'assert'; import sinon from 'sinon'; import auth from '../../../../Auth.js'; -import request from '../../../../request.js'; +import { cli } from '../../../../cli/cli.js'; +import { Logger } from '../../../../cli/Logger.js'; import commands from '../../commands.js'; +import command, { options } from './multitenant-get.js'; import { telemetry } from '../../../../telemetry.js'; import { pid } from '../../../../utils/pid.js'; import { session } from '../../../../utils/session.js'; -import { Logger } from '../../../../cli/Logger.js'; +import request from '../../../../request.js'; import { sinonUtil } from '../../../../utils/sinonUtil.js'; -import command from './multitenant-get.js'; import { CommandError } from '../../../../Command.js'; describe(commands.MULTITENANT_GET, () => { let log: string[]; let logger: Logger; let loggerLogSpy: sinon.SinonSpy; + let commandOptionsSchema: typeof options; const response = { "id": "ab217953-e37f-4691-97b8-dbb8a0a3bcaf", "createdDateTime": "2024-05-05T05:05:05", @@ -29,6 +31,8 @@ describe(commands.MULTITENANT_GET, () => { sinon.stub(pid, 'getProcessName').returns(''); sinon.stub(session, 'getId').returns(''); auth.connection.active = true; + const commandInfo = cli.getCommandInfo(command); + commandOptionsSchema = commandInfo.command.getSchemaToParse() as typeof options; }); beforeEach(() => { @@ -75,7 +79,7 @@ describe(commands.MULTITENANT_GET, () => { throw 'Invalid request'; }); - await command.action(logger, { options: { verbose: true } }); + await command.action(logger, { options: commandOptionsSchema.parse({ verbose: true }) }); assert(loggerLogSpy.calledOnceWithExactly(response)); }); @@ -93,6 +97,6 @@ describe(commands.MULTITENANT_GET, () => { }; sinon.stub(request, 'get').rejects(error); - await assert.rejects(command.action(logger, { options: {} }), new CommandError(error.error.message)); + await assert.rejects(command.action(logger, { options: commandOptionsSchema.parse({}) }), new CommandError(error.error.message)); }); }); \ No newline at end of file diff --git a/src/m365/entra/commands/multitenant/multitenant-get.ts b/src/m365/entra/commands/multitenant/multitenant-get.ts index d30613594d9..d3b2d955567 100644 --- a/src/m365/entra/commands/multitenant/multitenant-get.ts +++ b/src/m365/entra/commands/multitenant/multitenant-get.ts @@ -1,9 +1,20 @@ +import { z } from 'zod'; +import { globalOptionsZod } from '../../../../Command.js'; import { Logger } from '../../../../cli/Logger.js'; import request, { CliRequestOptions } from '../../../../request.js'; import GraphCommand from '../../../base/GraphCommand.js'; import commands from '../../commands.js'; import { MultitenantOrganization } from './MultitenantOrganization.js'; +export const options = globalOptionsZod + .extend({}) + .strict(); +declare type Options = z.infer; + +interface CommandArgs { + options: Options; +} + class EntraMultitenantGetCommand extends GraphCommand { public get name(): string { return commands.MULTITENANT_GET; @@ -13,7 +24,11 @@ class EntraMultitenantGetCommand extends GraphCommand { return 'Gets properties of the multitenant organization'; } - public async commandAction(logger: Logger): Promise { + public get schema(): z.ZodTypeAny | undefined { + return options; + } + + public async commandAction(logger: Logger, _args: CommandArgs): Promise { const requestOptions: CliRequestOptions = { url: `${this.resource}/v1.0/tenantRelationships/multiTenantOrganization`, diff --git a/src/m365/entra/commands/multitenant/multitenant-remove.spec.ts b/src/m365/entra/commands/multitenant/multitenant-remove.spec.ts index 4dfae86d255..0ebddc13f5c 100644 --- a/src/m365/entra/commands/multitenant/multitenant-remove.spec.ts +++ b/src/m365/entra/commands/multitenant/multitenant-remove.spec.ts @@ -6,7 +6,7 @@ import commands from '../../commands.js'; import { cli } from '../../../../cli/cli.js'; import request from '../../../../request.js'; import { sinonUtil } from '../../../../utils/sinonUtil.js'; -import command from './multitenant-remove.js'; +import command, { options } from './multitenant-remove.js'; import { telemetry } from '../../../../telemetry.js'; import { pid } from '../../../../utils/pid.js'; import { session } from '../../../../utils/session.js'; @@ -28,6 +28,7 @@ describe(commands.MULTITENANT_REMOVE, () => { let log: string[]; let logger: Logger; let promptIssued: boolean; + let commandOptionsSchema: typeof options; before(() => { sinon.stub(auth, 'restoreAuth').resolves(); @@ -35,6 +36,8 @@ describe(commands.MULTITENANT_REMOVE, () => { sinon.stub(pid, 'getProcessName').returns(''); sinon.stub(session, 'getId').returns(''); auth.connection.active = true; + const commandInfo = cli.getCommandInfo(command); + commandOptionsSchema = commandInfo.command.getSchemaToParse() as typeof options; }); beforeEach(() => { @@ -121,7 +124,7 @@ describe(commands.MULTITENANT_REMOVE, () => { return {} as any; }); - await command.action(logger, { options: { force: true, verbose: true } }); + await command.action(logger, { options: commandOptionsSchema.parse({ force: true, verbose: true }) }); assert(deleteRequestStub.calledTwice); }); @@ -167,12 +170,12 @@ describe(commands.MULTITENANT_REMOVE, () => { sinonUtil.restore(cli.promptForConfirmation); sinon.stub(cli, 'promptForConfirmation').resolves(true); - await command.action(logger, { options: {} }); + await command.action(logger, { options: commandOptionsSchema.parse({}) }); assert(deleteRequestStub.calledTwice); }); it('prompts before removing the multitenant organization when prompt option not passed', async () => { - await command.action(logger, { options: {} }); + await command.action(logger, { options: commandOptionsSchema.parse({}) }); assert(promptIssued); }); @@ -180,7 +183,7 @@ describe(commands.MULTITENANT_REMOVE, () => { it('aborts removing the multitenant organization when prompt not confirmed', async () => { const deleteSpy = sinon.stub(request, 'delete').resolves(); - await command.action(logger, { options: {} }); + await command.action(logger, { options: commandOptionsSchema.parse({}) }); assert(deleteSpy.notCalled); }); @@ -223,7 +226,7 @@ describe(commands.MULTITENANT_REMOVE, () => { throw 'Invalid request'; }); - await assert.rejects(command.action(logger, { options: { force: true } }), + await assert.rejects(command.action(logger, { options: commandOptionsSchema.parse({ force: true }) }), new CommandError(error.error.message)); }); }); \ No newline at end of file diff --git a/src/m365/entra/commands/multitenant/multitenant-remove.ts b/src/m365/entra/commands/multitenant/multitenant-remove.ts index f09ca963433..143e166dd21 100644 --- a/src/m365/entra/commands/multitenant/multitenant-remove.ts +++ b/src/m365/entra/commands/multitenant/multitenant-remove.ts @@ -1,4 +1,5 @@ -import GlobalOptions from '../../../../GlobalOptions.js'; +import { z } from 'zod'; +import { globalOptionsZod } from '../../../../Command.js'; import { Organization } from '@microsoft/microsoft-graph-types'; import { Logger } from '../../../../cli/Logger.js'; import request, { CliRequestOptions } from '../../../../request.js'; @@ -11,14 +12,16 @@ interface MultitenantOrganizationMember { tenantId?: string; } +export const options = globalOptionsZod + .extend({ + force: z.boolean().optional().alias('f') + }).strict(); +declare type Options = z.infer; + interface CommandArgs { options: Options; } -interface Options extends GlobalOptions { - force?: boolean; -} - class EntraMultitenantRemoveCommand extends GraphCommand { public get name(): string { return commands.MULTITENANT_REMOVE; @@ -28,27 +31,8 @@ class EntraMultitenantRemoveCommand extends GraphCommand { return 'Removes a multitenant organization'; } - constructor() { - super(); - - this.#initTelemetry(); - this.#initOptions(); - } - - #initTelemetry(): void { - this.telemetry.push((args: CommandArgs) => { - Object.assign(this.telemetryProperties, { - force: !!args.options.force - }); - }); - } - - #initOptions(): void { - this.options.unshift( - { - option: '-f, --force' - } - ); + public get schema(): z.ZodTypeAny | undefined { + return options; } public async commandAction(logger: Logger, args: CommandArgs): Promise { diff --git a/src/m365/entra/commands/multitenant/multitenant-set.spec.ts b/src/m365/entra/commands/multitenant/multitenant-set.spec.ts index 04f35ccc58c..170f62ea4ec 100644 --- a/src/m365/entra/commands/multitenant/multitenant-set.spec.ts +++ b/src/m365/entra/commands/multitenant/multitenant-set.spec.ts @@ -2,9 +2,8 @@ import assert from 'assert'; import sinon from 'sinon'; import auth from '../../../../Auth.js'; import { cli } from '../../../../cli/cli.js'; -import { CommandInfo } from '../../../../cli/CommandInfo.js'; import commands from '../../commands.js'; -import command from './multitenant-set.js'; +import command, { options } from './multitenant-set.js'; import { telemetry } from '../../../../telemetry.js'; import { pid } from '../../../../utils/pid.js'; import { session } from '../../../../utils/session.js'; @@ -16,7 +15,7 @@ import { CommandError } from '../../../../Command.js'; describe(commands.MULTITENANT_SET, () => { let log: string[]; let logger: Logger; - let commandInfo: CommandInfo; + let commandOptionsSchema: typeof options; before(() => { sinon.stub(auth, 'restoreAuth').resolves(); @@ -24,7 +23,8 @@ describe(commands.MULTITENANT_SET, () => { sinon.stub(pid, 'getProcessName').returns(''); sinon.stub(session, 'getId').returns(''); auth.connection.active = true; - commandInfo = cli.getCommandInfo(command); + const commandInfo = cli.getCommandInfo(command); + commandOptionsSchema = commandInfo.command.getSchemaToParse() as typeof options; }); beforeEach(() => { @@ -63,23 +63,23 @@ describe(commands.MULTITENANT_SET, () => { }); it('passes validation when only displayName is specified', async () => { - const actual = await command.validate({ options: { displayName: 'Contoso organization' } }, commandInfo); - assert.strictEqual(actual, true); + const parseResult = commandOptionsSchema.safeParse({ displayName: 'Contoso organization' }); + assert.strictEqual(parseResult.success, true); }); it('passes validation when only description is specified', async () => { - const actual = await command.validate({ options: { description: 'Contoso and partners' } }, commandInfo); - assert.strictEqual(actual, true); + const parseResult = commandOptionsSchema.safeParse({ description: 'Contoso and partners' }); + assert.strictEqual(parseResult.success, true); }); it('passes validation when the displayName and description are specified', async () => { - const actual = await command.validate({ options: { displayName: 'Contoso organization', description: 'Contoso and partners' } }, commandInfo); - assert.strictEqual(actual, true); + const parseResult = commandOptionsSchema.safeParse({ displayName: 'Contoso organization', description: 'Contoso and partners' }); + assert.strictEqual(parseResult.success, true); }); it('fails validation when no option is specified', async () => { - const actual = await command.validate({ options: {} }, commandInfo); - assert.notStrictEqual(actual, true); + const parseResult = commandOptionsSchema.safeParse({}); + assert.strictEqual(parseResult.success, false); }); it('updates a displayName of a multitenant organization', async () => { @@ -91,7 +91,7 @@ describe(commands.MULTITENANT_SET, () => { throw 'Invalid request'; }); - await command.action(logger, { options: { displayName: 'Contoso organization' } }); + await command.action(logger, { options: commandOptionsSchema.parse({ displayName: 'Contoso organization' }) }); assert(patchRequestStub.called); }); @@ -104,7 +104,7 @@ describe(commands.MULTITENANT_SET, () => { throw 'Invalid request'; }); - await command.action(logger, { options: { description: 'Contoso and partners', verbose: true } }); + await command.action(logger, { options: commandOptionsSchema.parse({ description: 'Contoso and partners', verbose: true }) }); assert(patchRequestStub.called); }); @@ -117,7 +117,7 @@ describe(commands.MULTITENANT_SET, () => { throw 'Invalid request'; }); - await command.action(logger, { options: { displayName: 'Contoso organization', description: 'Contoso and partners' } }); + await command.action(logger, { options: commandOptionsSchema.parse({ displayName: 'Contoso organization', description: 'Contoso and partners' }) }); assert(patchRequestStub.called); }); @@ -133,6 +133,6 @@ describe(commands.MULTITENANT_SET, () => { } }); - await assert.rejects(command.action(logger, { options: {} }), new CommandError('Invalid request')); + await assert.rejects(command.action(logger, { options: commandOptionsSchema.parse({ displayName: 'test' }) }), new CommandError('Invalid request')); }); }); \ No newline at end of file diff --git a/src/m365/entra/commands/multitenant/multitenant-set.ts b/src/m365/entra/commands/multitenant/multitenant-set.ts index fe7aaa8c783..f879dc35ce2 100644 --- a/src/m365/entra/commands/multitenant/multitenant-set.ts +++ b/src/m365/entra/commands/multitenant/multitenant-set.ts @@ -1,19 +1,22 @@ -import GlobalOptions from '../../../../GlobalOptions.js'; +import { z } from 'zod'; +import { globalOptionsZod } from '../../../../Command.js'; import { Logger } from '../../../../cli/Logger.js'; import request, { CliRequestOptions } from '../../../../request.js'; import GraphCommand from '../../../base/GraphCommand.js'; import commands from '../../commands.js'; import { MultitenantOrganization } from './MultitenantOrganization.js'; +export const options = globalOptionsZod + .extend({ + displayName: z.string().optional().alias('n'), + description: z.string().optional().alias('d') + }).strict(); +declare type Options = z.infer; + interface CommandArgs { options: Options; } -interface Options extends GlobalOptions { - displayName?: string; - description?: string; -} - class EntraMultitenantSetCommand extends GraphCommand { public get name(): string { return commands.MULTITENANT_SET; @@ -23,49 +26,15 @@ class EntraMultitenantSetCommand extends GraphCommand { return 'Updates the properties of a multitenant organization'; } - constructor() { - super(); - - this.#initTelemetry(); - this.#initOptions(); - this.#initValidators(); - this.#initTypes(); + public get schema(): z.ZodTypeAny | undefined { + return options; } - #initTelemetry(): void { - this.telemetry.push((args: CommandArgs) => { - Object.assign(this.telemetryProperties, { - displayName: typeof args.options.displayName !== 'undefined', - description: typeof args.options.description !== 'undefined' + public getRefinedSchema(schema: typeof options): z.ZodObject | undefined { + return schema + .refine(options => options.displayName || options.description, { + error: 'Specify either displayName or description or both' }); - }); - } - - #initOptions(): void { - this.options.unshift( - { - option: '-n, --displayName [displayName]' - }, - { - option: '-d, --description [description]' - } - ); - } - - #initValidators(): void { - this.validators.push( - async (args: CommandArgs) => { - if (!args.options.displayName && !args.options.description) { - return 'Specify either displayName or description or both.'; - } - - return true; - } - ); - } - - #initTypes(): void { - this.types.string.push('displayName', 'description'); } public async commandAction(logger: Logger, args: CommandArgs): Promise { diff --git a/src/m365/entra/commands/oauth2grant/oauth2grant-add.spec.ts b/src/m365/entra/commands/oauth2grant/oauth2grant-add.spec.ts index 551b91cb676..6f2d81b7030 100644 --- a/src/m365/entra/commands/oauth2grant/oauth2grant-add.spec.ts +++ b/src/m365/entra/commands/oauth2grant/oauth2grant-add.spec.ts @@ -2,7 +2,6 @@ import assert from 'assert'; import sinon from 'sinon'; import auth from '../../../../Auth.js'; import { cli } from '../../../../cli/cli.js'; -import { CommandInfo } from '../../../../cli/CommandInfo.js'; import { Logger } from '../../../../cli/Logger.js'; import { CommandError } from '../../../../Command.js'; import request from '../../../../request.js'; @@ -11,14 +10,14 @@ 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 './oauth2grant-add.js'; +import command, { options } from './oauth2grant-add.js'; describe(commands.OAUTH2GRANT_ADD, () => { let log: string[]; let logger: Logger; let loggerLogSpy: sinon.SinonSpy; - let commandInfo: CommandInfo; let loggerLogToStderrSpy: sinon.SinonSpy; + let commandOptionsSchema: typeof options; before(() => { sinon.stub(auth, 'restoreAuth').resolves(); @@ -26,7 +25,8 @@ describe(commands.OAUTH2GRANT_ADD, () => { sinon.stub(pid, 'getProcessName').returns(''); sinon.stub(session, 'getId').returns(''); auth.connection.active = true; - commandInfo = cli.getCommandInfo(command); + const commandInfo = cli.getCommandInfo(command); + commandOptionsSchema = commandInfo.command.getSchemaToParse() as typeof options; }); beforeEach(() => { @@ -81,7 +81,7 @@ describe(commands.OAUTH2GRANT_ADD, () => { throw 'Invalid request'; }); - await command.action(logger, { options: { debug: true, clientId: '6a7b1395-d313-4682-8ed4-65a6265a6320', resourceId: '6a7b1395-d313-4682-8ed4-65a6265a6321', scope: 'user_impersonation' } } as any); + await command.action(logger, { options: commandOptionsSchema.parse({ debug: true, clientId: '6a7b1395-d313-4682-8ed4-65a6265a6320', resourceId: '6a7b1395-d313-4682-8ed4-65a6265a6321', scope: 'user_impersonation' }) }); assert(loggerLogToStderrSpy.called); }); @@ -101,7 +101,7 @@ describe(commands.OAUTH2GRANT_ADD, () => { throw 'Invalid request'; }); - await command.action(logger, { options: { clientId: '6a7b1395-d313-4682-8ed4-65a6265a6320', resourceId: '6a7b1395-d313-4682-8ed4-65a6265a6321', scope: 'user_impersonation' } }); + await command.action(logger, { options: commandOptionsSchema.parse({ clientId: '6a7b1395-d313-4682-8ed4-65a6265a6320', resourceId: '6a7b1395-d313-4682-8ed4-65a6265a6321', scope: 'user_impersonation' }) }); assert(loggerLogSpy.notCalled); }); @@ -117,55 +117,22 @@ describe(commands.OAUTH2GRANT_ADD, () => { } }); - await assert.rejects(command.action(logger, { options: { clientId: '6a7b1395-d313-4682-8ed4-65a6265a6320', resourceId: '6a7b1395-d313-4682-8ed4-65a6265a6320', scope: 'user_impersonation' } } as any), + await assert.rejects(command.action(logger, { options: commandOptionsSchema.parse({ clientId: '6a7b1395-d313-4682-8ed4-65a6265a6320', resourceId: '6a7b1395-d313-4682-8ed4-65a6265a6320', scope: 'user_impersonation' }) }), new CommandError('An error has occurred')); }); it('fails validation if the clientId is not a valid GUID', async () => { - const actual = await command.validate({ options: { clientId: '123', resourceId: '6a7b1395-d313-4682-8ed4-65a6265a6320', scope: 'user_impersonation' } }, commandInfo); - assert.notStrictEqual(actual, true); + const parseResult = commandOptionsSchema.safeParse({ clientId: '123', resourceId: '6a7b1395-d313-4682-8ed4-65a6265a6320', scope: 'user_impersonation' }); + assert.strictEqual(parseResult.success, false); }); it('fails validation if the resourceId is not a valid GUID', async () => { - const actual = await command.validate({ options: { clientId: '6a7b1395-d313-4682-8ed4-65a6265a6320', resourceId: '123', scope: 'user_impersonation' } }, commandInfo); - assert.notStrictEqual(actual, true); + const parseResult = commandOptionsSchema.safeParse({ clientId: '6a7b1395-d313-4682-8ed4-65a6265a6320', resourceId: '123', scope: 'user_impersonation' }); + assert.strictEqual(parseResult.success, false); }); it('passes validation when clientId, resourceId and scope are specified', async () => { - const actual = await command.validate({ options: { clientId: '6a7b1395-d313-4682-8ed4-65a6265a6320', resourceId: '6a7b1395-d313-4682-8ed4-65a6265a6320', scope: 'user_impersonation' } }, commandInfo); - assert.strictEqual(actual, true); - }); - - it('supports specifying clientId', () => { - const options = command.options; - let containsOption = false; - options.forEach(o => { - if (o.option.indexOf('--clientId') > -1) { - containsOption = true; - } - }); - assert(containsOption); - }); - - it('supports specifying resourceId', () => { - const options = command.options; - let containsOption = false; - options.forEach(o => { - if (o.option.indexOf('--resourceId') > -1) { - containsOption = true; - } - }); - assert(containsOption); - }); - - it('supports specifying scope', () => { - const options = command.options; - let containsOption = false; - options.forEach(o => { - if (o.option.indexOf('--scope') > -1) { - containsOption = true; - } - }); - assert(containsOption); + const parseResult = commandOptionsSchema.safeParse({ clientId: '6a7b1395-d313-4682-8ed4-65a6265a6320', resourceId: '6a7b1395-d313-4682-8ed4-65a6265a6320', scope: 'user_impersonation' }); + assert.strictEqual(parseResult.success, true); }); }); diff --git a/src/m365/entra/commands/oauth2grant/oauth2grant-add.ts b/src/m365/entra/commands/oauth2grant/oauth2grant-add.ts index fd2bd08a12a..ceddb420127 100644 --- a/src/m365/entra/commands/oauth2grant/oauth2grant-add.ts +++ b/src/m365/entra/commands/oauth2grant/oauth2grant-add.ts @@ -1,20 +1,22 @@ +import { z } from 'zod'; +import { globalOptionsZod } from '../../../../Command.js'; import { Logger } from '../../../../cli/Logger.js'; -import GlobalOptions from '../../../../GlobalOptions.js'; import request, { CliRequestOptions } from '../../../../request.js'; -import { validation } from '../../../../utils/validation.js'; import GraphCommand from '../../../base/GraphCommand.js'; import commands from '../../commands.js'; +export const options = globalOptionsZod + .extend({ + clientId: z.uuid().alias('i'), + resourceId: z.uuid().alias('r'), + scope: z.string().alias('s') + }).strict(); +declare type Options = z.infer; + interface CommandArgs { options: Options; } -interface Options extends GlobalOptions { - clientId: string; - resourceId: string; - scope: string; -} - class EntraOAuth2GrantAddCommand extends GraphCommand { public get name(): string { return commands.OAUTH2GRANT_ADD; @@ -24,41 +26,8 @@ class EntraOAuth2GrantAddCommand extends GraphCommand { return 'Grant the specified service principal OAuth2 permissions to the specified resource'; } - constructor() { - super(); - - this.#initOptions(); - this.#initValidators(); - } - - #initOptions(): void { - this.options.unshift( - { - option: '-i, --clientId ' - }, - { - option: '-r, --resourceId ' - }, - { - option: '-s, --scope ' - } - ); - } - - #initValidators(): void { - this.validators.push( - async (args: CommandArgs) => { - if (!validation.isValidGuid(args.options.clientId)) { - return `${args.options.clientId} is not a valid GUID`; - } - - if (!validation.isValidGuid(args.options.resourceId)) { - return `${args.options.resourceId} is not a valid GUID`; - } - - return true; - } - ); + public get schema(): z.ZodTypeAny | undefined { + return options; } public async commandAction(logger: Logger, args: CommandArgs): Promise { diff --git a/src/m365/entra/commands/oauth2grant/oauth2grant-list.spec.ts b/src/m365/entra/commands/oauth2grant/oauth2grant-list.spec.ts index 562eef15590..0e0f9e59d98 100644 --- a/src/m365/entra/commands/oauth2grant/oauth2grant-list.spec.ts +++ b/src/m365/entra/commands/oauth2grant/oauth2grant-list.spec.ts @@ -2,7 +2,6 @@ import assert from 'assert'; import sinon from 'sinon'; import auth from '../../../../Auth.js'; import { cli } from '../../../../cli/cli.js'; -import { CommandInfo } from '../../../../cli/CommandInfo.js'; import { Logger } from '../../../../cli/Logger.js'; import { CommandError } from '../../../../Command.js'; import request from '../../../../request.js'; @@ -11,13 +10,13 @@ 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 './oauth2grant-list.js'; +import command, { options } from './oauth2grant-list.js'; describe(commands.OAUTH2GRANT_LIST, () => { let log: string[]; let logger: Logger; let loggerLogSpy: sinon.SinonSpy; - let commandInfo: CommandInfo; + let commandOptionsSchema: typeof options; before(() => { sinon.stub(auth, 'restoreAuth').resolves(); @@ -25,7 +24,8 @@ describe(commands.OAUTH2GRANT_LIST, () => { sinon.stub(pid, 'getProcessName').returns(''); sinon.stub(session, 'getId').returns(''); auth.connection.active = true; - commandInfo = cli.getCommandInfo(command); + const commandInfo = cli.getCommandInfo(command); + commandOptionsSchema = commandInfo.command.getSchemaToParse() as typeof options; }); beforeEach(() => { @@ -92,7 +92,7 @@ describe(commands.OAUTH2GRANT_LIST, () => { throw 'Invalid request'; }); - await command.action(logger, { options: { debug: true, spObjectId: '141f7648-0c71-4752-9cdb-c7d5305b7e68' } }); + await command.action(logger, { options: commandOptionsSchema.parse({ debug: true, spObjectId: '141f7648-0c71-4752-9cdb-c7d5305b7e68' }) }); assert(loggerLogSpy.calledWith([{ "clientId": "cd4043e7-b749-420b-bd07-aa7c3912ed22", "consentType": "AllPrincipals", @@ -139,7 +139,7 @@ describe(commands.OAUTH2GRANT_LIST, () => { throw 'Invalid request'; }); - await command.action(logger, { options: { spObjectId: '141f7648-0c71-4752-9cdb-c7d5305b7e68' } }); + await command.action(logger, { options: commandOptionsSchema.parse({ spObjectId: '141f7648-0c71-4752-9cdb-c7d5305b7e68' }) }); assert(loggerLogSpy.calledWith([{ "clientId": "cd4043e7-b749-420b-bd07-aa7c3912ed22", "consentType": "AllPrincipals", @@ -192,7 +192,7 @@ describe(commands.OAUTH2GRANT_LIST, () => { throw 'Invalid request'; }); - await command.action(logger, { options: { spObjectId: '141f7648-0c71-4752-9cdb-c7d5305b7e68', output: 'json' } }); + await command.action(logger, { options: commandOptionsSchema.parse({ spObjectId: '141f7648-0c71-4752-9cdb-c7d5305b7e68', output: 'json' }) }); assert(loggerLogSpy.calledOnceWithExactly([{ "clientId": "cd4043e7-b749-420b-bd07-aa7c3912ed22", "consentType": "AllPrincipals", @@ -224,7 +224,7 @@ describe(commands.OAUTH2GRANT_LIST, () => { throw 'Invalid request'; }); - await command.action(logger, { options: { spObjectId: '141f7648-0c71-4752-9cdb-c7d5305b7e68' } }); + await command.action(logger, { options: commandOptionsSchema.parse({ spObjectId: '141f7648-0c71-4752-9cdb-c7d5305b7e68' }) }); assert(loggerLogSpy.calledOnceWithExactly([])); }); @@ -240,28 +240,17 @@ describe(commands.OAUTH2GRANT_LIST, () => { } }); - await assert.rejects(command.action(logger, { options: { spObjectId: 'b2307a39-e878-458b-bc90-03bc578531d6' } } as any), + await assert.rejects(command.action(logger, { options: commandOptionsSchema.parse({ spObjectId: 'b2307a39-e878-458b-bc90-03bc578531d6' }) }), new CommandError(`Resource '' does not exist or one of its queried reference-property objects are not present`)); }); it('fails validation if the spObjectId is not a valid GUID', async () => { - const actual = await command.validate({ options: { spObjectId: '123' } }, commandInfo); - assert.notStrictEqual(actual, true); + const parseResult = commandOptionsSchema.safeParse({ spObjectId: '123' }); + assert.strictEqual(parseResult.success, false); }); it('passes validation when the spObjectId option specified', async () => { - const actual = await command.validate({ options: { spObjectId: '6a7b1395-d313-4682-8ed4-65a6265a6320' } }, commandInfo); - assert.strictEqual(actual, true); - }); - - it('supports specifying spObjectId', () => { - const options = command.options; - let containsOption = false; - options.forEach(o => { - if (o.option.indexOf('--spObjectId') > -1) { - containsOption = true; - } - }); - assert(containsOption); + const parseResult = commandOptionsSchema.safeParse({ spObjectId: '6a7b1395-d313-4682-8ed4-65a6265a6320' }); + assert.strictEqual(parseResult.success, true); }); }); diff --git a/src/m365/entra/commands/oauth2grant/oauth2grant-list.ts b/src/m365/entra/commands/oauth2grant/oauth2grant-list.ts index 3d0edbd7738..79020eb8d26 100644 --- a/src/m365/entra/commands/oauth2grant/oauth2grant-list.ts +++ b/src/m365/entra/commands/oauth2grant/oauth2grant-list.ts @@ -1,19 +1,21 @@ +import { z } from 'zod'; +import { globalOptionsZod } from '../../../../Command.js'; 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'; +export const options = globalOptionsZod + .extend({ + spObjectId: z.uuid().alias('i') + }).strict(); +declare type Options = z.infer; + interface CommandArgs { options: Options; } -interface Options extends GlobalOptions { - spObjectId: string; -} - class EntraOAuth2GrantListCommand extends GraphCommand { public get name(): string { return commands.OAUTH2GRANT_LIST; @@ -27,31 +29,8 @@ class EntraOAuth2GrantListCommand extends GraphCommand { return ['objectId', 'resourceId', 'scope']; } - constructor() { - super(); - - this.#initOptions(); - this.#initValidators(); - } - - #initOptions(): void { - this.options.unshift( - { - option: '-i, --spObjectId ' - } - ); - } - - #initValidators(): void { - this.validators.push( - async (args: CommandArgs) => { - if (!validation.isValidGuid(args.options.spObjectId)) { - return `${args.options.spObjectId} is not a valid GUID`; - } - - return true; - } - ); + public get schema(): z.ZodTypeAny | undefined { + return options; } public async commandAction(logger: Logger, args: CommandArgs): Promise { diff --git a/src/m365/entra/commands/oauth2grant/oauth2grant-remove.spec.ts b/src/m365/entra/commands/oauth2grant/oauth2grant-remove.spec.ts index cd78ef2f334..13dc2386547 100644 --- a/src/m365/entra/commands/oauth2grant/oauth2grant-remove.spec.ts +++ b/src/m365/entra/commands/oauth2grant/oauth2grant-remove.spec.ts @@ -10,7 +10,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 './oauth2grant-remove.js'; +import command, { options } from './oauth2grant-remove.js'; describe(commands.OAUTH2GRANT_REMOVE, () => { let log: string[]; @@ -18,6 +18,7 @@ describe(commands.OAUTH2GRANT_REMOVE, () => { let loggerLogSpy: sinon.SinonSpy; let loggerLogToStderrSpy: sinon.SinonSpy; let promptIssued: boolean = false; + let commandOptionsSchema: typeof options; before(() => { sinon.stub(auth, 'restoreAuth').resolves(); @@ -25,6 +26,8 @@ describe(commands.OAUTH2GRANT_REMOVE, () => { sinon.stub(pid, 'getProcessName').returns(''); sinon.stub(session, 'getId').returns(''); auth.connection.active = true; + const commandInfo = cli.getCommandInfo(command); + commandOptionsSchema = commandInfo.command.getSchemaToParse() as typeof options; }); beforeEach(() => { @@ -82,7 +85,7 @@ describe(commands.OAUTH2GRANT_REMOVE, () => { sinonUtil.restore(cli.promptForConfirmation); sinon.stub(cli, 'promptForConfirmation').resolves(true); - await command.action(logger, { options: { debug: true, grantId: 'YgA60KYa4UOPSdc-lpxYEnQkr8KVLDpCsOXkiV8i-ek' } }); + await command.action(logger, { options: commandOptionsSchema.parse({ debug: true, grantId: 'YgA60KYa4UOPSdc-lpxYEnQkr8KVLDpCsOXkiV8i-ek' }) }); assert(loggerLogToStderrSpy.called); assert(deleteRequestStub.called); }); @@ -99,7 +102,7 @@ describe(commands.OAUTH2GRANT_REMOVE, () => { sinonUtil.restore(cli.promptForConfirmation); sinon.stub(cli, 'promptForConfirmation').resolves(true); - await command.action(logger, { options: { grantId: 'YgA60KYa4UOPSdc-lpxYEnQkr8KVLDpCsOXkiV8i-ek' } }); + await command.action(logger, { options: commandOptionsSchema.parse({ grantId: 'YgA60KYa4UOPSdc-lpxYEnQkr8KVLDpCsOXkiV8i-ek' }) }); assert(loggerLogSpy.notCalled); assert(deleteRequestStub.called); }); @@ -113,19 +116,19 @@ describe(commands.OAUTH2GRANT_REMOVE, () => { throw 'Invalid request'; }); - await command.action(logger, { options: { grantId: 'YgA60KYa4UOPSdc-lpxYEnQkr8KVLDpCsOXkiV8i-ek', force: true } }); + await command.action(logger, { options: commandOptionsSchema.parse({ grantId: 'YgA60KYa4UOPSdc-lpxYEnQkr8KVLDpCsOXkiV8i-ek', force: true }) }); assert(loggerLogSpy.notCalled); assert(deleteRequestStub.called); }); it('prompts before removing OAuth2 permission grant when force option not passed', async () => { - await command.action(logger, { options: { grantId: 'YgA60KYa4UOPSdc-lpxYEnQkr8KVLDpCsOXkiV8i-ek' } }); + await command.action(logger, { options: commandOptionsSchema.parse({ grantId: 'YgA60KYa4UOPSdc-lpxYEnQkr8KVLDpCsOXkiV8i-ek' }) }); assert(promptIssued); }); it('prompts before removing OAuth2 permission grant when force option not passed (debug)', async () => { - await command.action(logger, { options: { debug: true, grantId: 'YgA60KYa4UOPSdc-lpxYEnQkr8KVLDpCsOXkiV8i-ek' } }); + await command.action(logger, { options: commandOptionsSchema.parse({ debug: true, grantId: 'YgA60KYa4UOPSdc-lpxYEnQkr8KVLDpCsOXkiV8i-ek' }) }); assert(promptIssued); }); @@ -136,7 +139,7 @@ describe(commands.OAUTH2GRANT_REMOVE, () => { sinonUtil.restore(cli.promptForConfirmation); sinon.stub(cli, 'promptForConfirmation').resolves(false); - await command.action(logger, { options: { grantId: 'YgA60KYa4UOPSdc-lpxYEnQkr8KVLDpCsOXkiV8i-ek' } }); + await command.action(logger, { options: commandOptionsSchema.parse({ grantId: 'YgA60KYa4UOPSdc-lpxYEnQkr8KVLDpCsOXkiV8i-ek' }) }); assert(deleteSpy.notCalled); }); @@ -146,7 +149,7 @@ describe(commands.OAUTH2GRANT_REMOVE, () => { sinonUtil.restore(cli.promptForConfirmation); sinon.stub(cli, 'promptForConfirmation').resolves(false); - await command.action(logger, { options: { debug: true, grantId: 'YgA60KYa4UOPSdc-lpxYEnQkr8KVLDpCsOXkiV8i-ek' } }); + await command.action(logger, { options: commandOptionsSchema.parse({ debug: true, grantId: 'YgA60KYa4UOPSdc-lpxYEnQkr8KVLDpCsOXkiV8i-ek' }) }); assert(deleteSpy.notCalled); }); @@ -164,29 +167,7 @@ describe(commands.OAUTH2GRANT_REMOVE, () => { }); }); - await assert.rejects(command.action(logger, { options: { force: true, grantId: 'YgA60KYa4UOPSdc-lpxYEnQkr8KVLDpCsOXkiV8i-ek' } } as any), + await assert.rejects(command.action(logger, { options: commandOptionsSchema.parse({ force: true, grantId: 'YgA60KYa4UOPSdc-lpxYEnQkr8KVLDpCsOXkiV8i-ek' }) }), new CommandError('An error has occurred')); }); - - it('supports specifying grantId', () => { - const options = command.options; - let containsOption = false; - options.forEach(o => { - if (o.option.indexOf('--grantId') > -1) { - containsOption = true; - } - }); - assert(containsOption); - }); - - it('supports specifying confirmation flag', () => { - const options = command.options; - let containsOption = false; - options.forEach(o => { - if (o.option.indexOf('--force') > -1) { - containsOption = true; - } - }); - assert(containsOption); - }); }); diff --git a/src/m365/entra/commands/oauth2grant/oauth2grant-remove.ts b/src/m365/entra/commands/oauth2grant/oauth2grant-remove.ts index 567b8d53b9f..67571104b49 100644 --- a/src/m365/entra/commands/oauth2grant/oauth2grant-remove.ts +++ b/src/m365/entra/commands/oauth2grant/oauth2grant-remove.ts @@ -1,19 +1,23 @@ +import { z } from 'zod'; +import { globalOptionsZod } from '../../../../Command.js'; import { cli } from '../../../../cli/cli.js'; import { Logger } from '../../../../cli/Logger.js'; -import GlobalOptions from '../../../../GlobalOptions.js'; import request, { CliRequestOptions } from '../../../../request.js'; import { formatting } from '../../../../utils/formatting.js'; import GraphCommand from '../../../base/GraphCommand.js'; import commands from '../../commands.js'; +export const options = globalOptionsZod + .extend({ + grantId: z.string().alias('i'), + force: z.boolean().optional().alias('f') + }).strict(); +declare type Options = z.infer; + interface CommandArgs { options: Options; } -interface Options extends GlobalOptions { - grantId: string; -} - class EntraOAuth2GrantRemoveCommand extends GraphCommand { public get name(): string { return commands.OAUTH2GRANT_REMOVE; @@ -23,21 +27,8 @@ class EntraOAuth2GrantRemoveCommand extends GraphCommand { return 'Remove specified service principal OAuth2 permissions'; } - constructor() { - super(); - - this.#initOptions(); - } - - #initOptions(): void { - this.options.unshift( - { - option: '-i, --grantId ' - }, - { - option: '-f, --force' - } - ); + public get schema(): z.ZodTypeAny | undefined { + return options; } public async commandAction(logger: Logger, args: CommandArgs): Promise { diff --git a/src/m365/entra/commands/oauth2grant/oauth2grant-set.spec.ts b/src/m365/entra/commands/oauth2grant/oauth2grant-set.spec.ts index 5e2825ad0cc..914308c4a13 100644 --- a/src/m365/entra/commands/oauth2grant/oauth2grant-set.spec.ts +++ b/src/m365/entra/commands/oauth2grant/oauth2grant-set.spec.ts @@ -1,6 +1,7 @@ import assert from 'assert'; import sinon from 'sinon'; import auth from '../../../../Auth.js'; +import { cli } from '../../../../cli/cli.js'; import { Logger } from '../../../../cli/Logger.js'; import { CommandError } from '../../../../Command.js'; import request from '../../../../request.js'; @@ -9,13 +10,14 @@ 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 './oauth2grant-set.js'; +import command, { options } from './oauth2grant-set.js'; describe(commands.OAUTH2GRANT_SET, () => { let log: string[]; let logger: Logger; let loggerLogSpy: sinon.SinonSpy; let loggerLogToStderrSpy: sinon.SinonSpy; + let commandOptionsSchema: typeof options; before(() => { sinon.stub(auth, 'restoreAuth').resolves(); @@ -23,6 +25,8 @@ describe(commands.OAUTH2GRANT_SET, () => { sinon.stub(pid, 'getProcessName').returns(''); sinon.stub(session, 'getId').returns(''); auth.connection.active = true; + const commandInfo = cli.getCommandInfo(command); + commandOptionsSchema = commandInfo.command.getSchemaToParse() as typeof options; }); beforeEach(() => { @@ -75,7 +79,7 @@ describe(commands.OAUTH2GRANT_SET, () => { throw 'Invalid request'; }); - await command.action(logger, { options: { debug: true, grantId: 'YgA60KYa4UOPSdc-lpxYEnQkr8KVLDpCsOXkiV8i-ek', scope: 'user_impersonation' } }); + await command.action(logger, { options: commandOptionsSchema.parse({ debug: true, grantId: 'YgA60KYa4UOPSdc-lpxYEnQkr8KVLDpCsOXkiV8i-ek', scope: 'user_impersonation' }) }); assert(loggerLogToStderrSpy.called); }); @@ -93,7 +97,7 @@ describe(commands.OAUTH2GRANT_SET, () => { throw 'Invalid request'; }); - await command.action(logger, { options: { grantId: 'YgA60KYa4UOPSdc-lpxYEnQkr8KVLDpCsOXkiV8i-ek', scope: 'user_impersonation' } }); + await command.action(logger, { options: commandOptionsSchema.parse({ grantId: 'YgA60KYa4UOPSdc-lpxYEnQkr8KVLDpCsOXkiV8i-ek', scope: 'user_impersonation' }) }); assert(loggerLogSpy.notCalled); }); @@ -109,29 +113,7 @@ describe(commands.OAUTH2GRANT_SET, () => { } }); - await assert.rejects(command.action(logger, { options: { clientId: '6a7b1395-d313-4682-8ed4-65a6265a6320', resourceId: '6a7b1395-d313-4682-8ed4-65a6265a6320', scope: 'user_impersonation' } } as any), + await assert.rejects(command.action(logger, { options: commandOptionsSchema.parse({ grantId: '6a7b1395-d313-4682-8ed4-65a6265a6320', scope: 'user_impersonation' }) }), new CommandError('An error has occurred')); }); - - it('supports specifying grantId', () => { - const options = command.options; - let containsOption = false; - options.forEach(o => { - if (o.option.indexOf('--grantId') > -1) { - containsOption = true; - } - }); - assert(containsOption); - }); - - it('supports specifying scope', () => { - const options = command.options; - let containsOption = false; - options.forEach(o => { - if (o.option.indexOf('--scope') > -1) { - containsOption = true; - } - }); - assert(containsOption); - }); }); diff --git a/src/m365/entra/commands/oauth2grant/oauth2grant-set.ts b/src/m365/entra/commands/oauth2grant/oauth2grant-set.ts index 14c27647eb7..c59ed16f384 100644 --- a/src/m365/entra/commands/oauth2grant/oauth2grant-set.ts +++ b/src/m365/entra/commands/oauth2grant/oauth2grant-set.ts @@ -1,19 +1,22 @@ +import { z } from 'zod'; +import { globalOptionsZod } from '../../../../Command.js'; import { Logger } from '../../../../cli/Logger.js'; -import GlobalOptions from '../../../../GlobalOptions.js'; import request, { CliRequestOptions } from '../../../../request.js'; import { formatting } from '../../../../utils/formatting.js'; import GraphCommand from '../../../base/GraphCommand.js'; import commands from '../../commands.js'; +export const options = globalOptionsZod + .extend({ + grantId: z.string().alias('i'), + scope: z.string().alias('s') + }).strict(); +declare type Options = z.infer; + interface CommandArgs { options: Options; } -interface Options extends GlobalOptions { - grantId: string; - scope: string; -} - class EntraOAuth2GrantSetCommand extends GraphCommand { public get name(): string { return commands.OAUTH2GRANT_SET; @@ -23,21 +26,8 @@ class EntraOAuth2GrantSetCommand extends GraphCommand { return 'Update OAuth2 permissions for the service principal'; } - constructor() { - super(); - - this.#initOptions(); - } - - #initOptions(): void { - this.options.unshift( - { - option: '-i, --grantId ' - }, - { - option: '-s, --scope ' - } - ); + public get schema(): z.ZodTypeAny | undefined { + return options; } public async commandAction(logger: Logger, args: CommandArgs): Promise {