From 538e321119702d7a479bd9a244f1975ed867a60e Mon Sep 17 00:00:00 2001 From: Waldek Mastykarz Date: Thu, 28 May 2026 14:32:17 +0200 Subject: [PATCH] Migrate entra approleassignment and enterpriseapp commands to Zod Migrates the following commands from legacy options/validators to Zod schemas: - entra approleassignment add - entra approleassignment list - entra approleassignment remove - entra enterpriseapp add - entra enterpriseapp get - entra enterpriseapp list - entra enterpriseapp remove Closes pnp/cli-microsoft365#7295 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- npm-shrinkwrap.json | 7 + .../approleassignment-add.spec.ts | 100 ++++---------- .../approleassignment-add.ts | 87 ++++--------- .../approleassignment-list.spec.ts | 87 +++---------- .../approleassignment-list.ts | 77 +++-------- .../approleassignment-remove.spec.ts | 112 ++++------------ .../approleassignment-remove.ts | 93 ++++--------- .../enterpriseapp/enterpriseapp-add.spec.ts | 123 +++++------------- .../enterpriseapp/enterpriseapp-add.ts | 77 +++-------- .../enterpriseapp/enterpriseapp-get.spec.ts | 106 ++++----------- .../enterpriseapp/enterpriseapp-get.ts | 77 +++-------- .../enterpriseapp/enterpriseapp-list.ts | 43 ++---- .../enterpriseapp-remove.spec.ts | 52 ++++---- .../enterpriseapp/enterpriseapp-remove.ts | 89 ++++--------- 14 files changed, 304 insertions(+), 826 deletions(-) diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index d76c64cf835..acbe534d0bb 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -902,6 +902,7 @@ "node_modules/@opentelemetry/api": { "version": "1.9.1", "license": "Apache-2.0", + "peer": true, "engines": { "node": ">=8.0.0" } @@ -1750,6 +1751,7 @@ "node_modules/@types/node": { "version": "24.12.2", "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -1859,6 +1861,7 @@ "version": "8.59.2", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.59.2", "@typescript-eslint/types": "8.59.2", @@ -2388,6 +2391,7 @@ "node_modules/acorn": { "version": "8.16.0", "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -3144,6 +3148,7 @@ "node_modules/diagnostic-channel": { "version": "1.1.1", "license": "MIT", + "peer": true, "dependencies": { "semver": "^7.5.3" } @@ -3302,6 +3307,7 @@ "version": "10.2.1", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", @@ -5900,6 +5906,7 @@ "node_modules/typescript": { "version": "5.9.3", "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/src/m365/entra/commands/approleassignment/approleassignment-add.spec.ts b/src/m365/entra/commands/approleassignment/approleassignment-add.spec.ts index 21609859bdf..cd0621b8802 100644 --- a/src/m365/entra/commands/approleassignment/approleassignment-add.spec.ts +++ b/src/m365/entra/commands/approleassignment/approleassignment-add.spec.ts @@ -12,7 +12,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 './approleassignment-add.js'; +import command, { options } from './approleassignment-add.js'; import { settingsNames } from '../../../../settingsNames.js'; describe(commands.APPROLEASSIGNMENT_ADD, () => { @@ -20,6 +20,7 @@ describe(commands.APPROLEASSIGNMENT_ADD, () => { let logger: Logger; let loggerLogSpy: sinon.SinonSpy; let commandInfo: CommandInfo; + let commandOptionsSchema: typeof options; const getRequestStub = (): sinon.SinonStub => { return sinon.stub(request, 'get').callsFake(async (opts: any) => { @@ -48,6 +49,7 @@ describe(commands.APPROLEASSIGNMENT_ADD, () => { sinon.stub(session, 'getId').returns(''); auth.connection.active = true; commandInfo = cli.getCommandInfo(command); + commandOptionsSchema = commandInfo.command.getSchemaToParse()! as typeof options; }); beforeEach(() => { @@ -274,93 +276,39 @@ describe(commands.APPROLEASSIGNMENT_ADD, () => { new CommandError(`Resource '' does not exist or one of its queried reference-property objects are not present`)); }); - it('fails validation if neither appId, objectId nor displayName are not specified', async () => { - sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => { - if (settingName === settingsNames.prompt) { - return false; - } - - return defaultValue; - }); - - const actual = await command.validate({ options: { resource: 'abc', scopes: 'abc' } }, commandInfo); - assert.notStrictEqual(actual, true); - }); - - it('fails validation if the appId is not a valid GUID', async () => { - const actual = await command.validate({ options: { appId: '123', resource: 'abc', scopes: 'abc' } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation if neither appId, objectId nor displayName are not specified', () => { + const actual = commandOptionsSchema.safeParse({ resource: 'abc', scopes: 'abc' }); + assert.strictEqual(actual.success, false); }); - it('fails validation if the objectId is not a valid GUID', async () => { - const actual = await command.validate({ options: { appObjectId: '123', resource: 'abc', scopes: 'abc' } }, commandInfo); - assert.notStrictEqual(actual, true); - }); - - it('fails validation if both appId and appDisplayName are specified', async () => { - sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => { - if (settingName === settingsNames.prompt) { - return false; - } - - return defaultValue; - }); - - const actual = await command.validate({ options: { appId: '123', appDisplayName: 'abc', resource: 'abc', scopes: 'abc' } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation if the appId is not a valid GUID', () => { + const actual = commandOptionsSchema.safeParse({ appId: '123', resource: 'abc', scopes: 'abc' }); + assert.strictEqual(actual.success, false); }); - it('fails validation if both appObjectId and appDisplayName are specified', async () => { - sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => { - if (settingName === settingsNames.prompt) { - return false; - } - - return defaultValue; - }); - - const actual = await command.validate({ options: { appObjectId: '123', appDisplayName: 'abc', resource: 'abc', scopes: 'abc' } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation if the objectId is not a valid GUID', () => { + const actual = commandOptionsSchema.safeParse({ appObjectId: '123', resource: 'abc', scopes: 'abc' }); + assert.strictEqual(actual.success, false); }); - it('fails validation if both appObjectId, appId and appDisplayName are specified', async () => { - sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => { - if (settingName === settingsNames.prompt) { - return false; - } - - return defaultValue; - }); - - const actual = await command.validate({ options: { appId: '123', appObjectId: '123', appDisplayName: 'abc', resource: 'abc', scopes: 'abc' } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation if both appId and appDisplayName are specified', () => { + const actual = commandOptionsSchema.safeParse({ appId: '57907bf8-73fa-43a6-89a5-1f603e29e452', appDisplayName: 'abc', resource: 'abc', scopes: 'abc' }); + assert.strictEqual(actual.success, false); }); - it('passes validation when the appId option specified', async () => { - const actual = await command.validate({ options: { appId: '57907bf8-73fa-43a6-89a5-1f603e29e452', resource: 'abc', scopes: 'abc' } }, commandInfo); - assert.strictEqual(actual, true); + it('fails validation if both appObjectId and appDisplayName are specified', () => { + const actual = commandOptionsSchema.safeParse({ appObjectId: '57907bf8-73fa-43a6-89a5-1f603e29e452', appDisplayName: 'abc', resource: 'abc', scopes: 'abc' }); + assert.strictEqual(actual.success, false); }); - it('supports specifying appId', () => { - const options = command.options; - let containsOption = false; - options.forEach(o => { - if (o.option.indexOf('--appId') > -1) { - containsOption = true; - } - }); - assert(containsOption); + it('fails validation if both appObjectId, appId and appDisplayName are specified', () => { + const actual = commandOptionsSchema.safeParse({ appId: '57907bf8-73fa-43a6-89a5-1f603e29e452', appObjectId: '57907bf8-73fa-43a6-89a5-1f603e29e452', appDisplayName: 'abc', resource: 'abc', scopes: 'abc' }); + assert.strictEqual(actual.success, false); }); - it('supports specifying appDisplayName', () => { - const options = command.options; - let containsOption = false; - options.forEach(o => { - if (o.option.indexOf('--appDisplayName') > -1) { - containsOption = true; - } - }); - assert(containsOption); + it('passes validation when the appId option specified', () => { + const actual = commandOptionsSchema.safeParse({ appId: '57907bf8-73fa-43a6-89a5-1f603e29e452', resource: 'abc', scopes: 'abc' }); + assert.strictEqual(actual.success, true); }); }); diff --git a/src/m365/entra/commands/approleassignment/approleassignment-add.ts b/src/m365/entra/commands/approleassignment/approleassignment-add.ts index eb04a4a3a9c..d2140e6b80f 100644 --- a/src/m365/entra/commands/approleassignment/approleassignment-add.ts +++ b/src/m365/entra/commands/approleassignment/approleassignment-add.ts @@ -1,6 +1,7 @@ import os from 'os'; +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 { validation } from '../../../../utils/validation.js'; @@ -15,18 +16,21 @@ interface AppRole { resourceId: string; } +export const options = z.strictObject({ + ...globalOptionsZod.shape, + appId: z.uuid().optional(), + appObjectId: z.uuid().optional(), + appDisplayName: z.string().optional(), + resource: z.string().alias('r'), + scopes: z.string().alias('s') +}); + +declare type Options = z.infer; + interface CommandArgs { options: Options; } -interface Options extends GlobalOptions { - appId?: string; - appObjectId?: string; - appDisplayName?: string; - resource: string; - scopes: string; -} - class EntraAppRoleAssignmentAddCommand extends GraphCommand { public get name(): string { return commands.APPROLEASSIGNMENT_ADD; @@ -36,64 +40,19 @@ class EntraAppRoleAssignmentAddCommand extends GraphCommand { return 'Adds service principal permissions also known as scopes and app role assignments for specified Microsoft Entra application registration'; } - constructor() { - super(); - - this.#initTelemetry(); - this.#initOptions(); - this.#initValidators(); - this.#initOptionSets(); - } - - #initTelemetry(): void { - this.telemetry.push((args: CommandArgs) => { - Object.assign(this.telemetryProperties, { - appId: typeof args.options.appId !== 'undefined', - appObjectId: typeof args.options.appObjectId !== 'undefined', - appDisplayName: typeof args.options.appDisplayName !== 'undefined' - }); - }); - } - - #initOptions(): void { - this.options.unshift( - { - option: '--appId [appId]' - }, - { - option: '--appObjectId [appObjectId]' - }, - { - option: '--appDisplayName [appDisplayName]' - }, - { - option: '-r, --resource ', - autocomplete: ['Microsoft Graph', 'SharePoint', 'OneNote', 'Exchange', 'Microsoft Forms', 'Azure Active Directory Graph', 'Skype for Business'] - }, - { - option: '-s, --scopes ' - } - ); + public get schema(): z.ZodType | undefined { + return options; } - #initValidators(): void { - this.validators.push( - async (args: CommandArgs) => { - if (args.options.appId && !validation.isValidGuid(args.options.appId)) { - return `${args.options.appId} is not a valid GUID`; - } - - if (args.options.appObjectId && !validation.isValidGuid(args.options.appObjectId)) { - return `${args.options.appObjectId} is not a valid GUID`; + public getRefinedSchema(schema: typeof options): z.ZodObject | undefined { + return schema + .refine(options => [options.appId, options.appObjectId, options.appDisplayName].filter(o => o !== undefined).length === 1, { + error: 'Specify either appId, appObjectId, or appDisplayName', + params: { + customCode: 'optionSet', + options: ['appId', 'appObjectId', 'appDisplayName'] } - - return true; - } - ); - } - - #initOptionSets(): void { - this.optionSets.push({ options: ['appId', 'appObjectId', 'appDisplayName'] }); + }); } public async commandAction(logger: Logger, args: CommandArgs): Promise { diff --git a/src/m365/entra/commands/approleassignment/approleassignment-list.spec.ts b/src/m365/entra/commands/approleassignment/approleassignment-list.spec.ts index 0ada242c656..a23dde155bb 100644 --- a/src/m365/entra/commands/approleassignment/approleassignment-list.spec.ts +++ b/src/m365/entra/commands/approleassignment/approleassignment-list.spec.ts @@ -12,8 +12,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 './approleassignment-list.js'; -import { settingsNames } from '../../../../settingsNames.js'; +import command, { options } from './approleassignment-list.js'; class ServicePrincipalAppRoleAssignments { private static AppRoleAssignments: any = { @@ -414,6 +413,7 @@ describe(commands.APPROLEASSIGNMENT_LIST, () => { let logger: Logger; let loggerLogSpy: sinon.SinonSpy; let commandInfo: CommandInfo; + let commandOptionsSchema: typeof options; const jsonOutput = [ { @@ -443,6 +443,7 @@ describe(commands.APPROLEASSIGNMENT_LIST, () => { sinon.stub(session, 'getId').returns(''); auth.connection.active = true; commandInfo = cli.getCommandInfo(command); + commandOptionsSchema = commandInfo.command.getSchemaToParse()! as typeof options; }); beforeEach(() => { @@ -546,80 +547,34 @@ describe(commands.APPROLEASSIGNMENT_LIST, () => { await assert.rejects(command.action(logger, { options: { appObjectId: '021d971f-779d-439b-8006-9f084423f344' } } as any), new CommandError(`Resource '' does not exist or one of its queried reference-property objects are not present`)); }); - it('fails validation if neither appId nor appDisplayName are not specified', async () => { - sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => { - if (settingName === settingsNames.prompt) { - return false; - } - - return defaultValue; - }); - - const actual = await command.validate({ options: {} }, commandInfo); - assert.notStrictEqual(actual, true); - }); - - it('fails validation if the appId is not a valid GUID', async () => { - const actual = await command.validate({ options: { appId: '123' } }, commandInfo); - assert.notStrictEqual(actual, true); - }); - - it('fails validation if the appObjectId is not a valid GUID', async () => { - const actual = await command.validate({ options: { appObjectId: '123' } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation if neither appId nor appDisplayName are not specified', () => { + const actual = commandOptionsSchema.safeParse({}); + assert.strictEqual(actual.success, false); }); - it('fails validation if both appId and appDisplayName are specified', async () => { - sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => { - if (settingName === settingsNames.prompt) { - return false; - } - - return defaultValue; - }); - - const actual = await command.validate({ options: { appId: CommandActionParameters.appIdWithNoRoleAssignments, appDisplayName: CommandActionParameters.appNameWithRoleAssignments } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation if the appId is not a valid GUID', () => { + const actual = commandOptionsSchema.safeParse({ appId: '123' }); + assert.strictEqual(actual.success, false); }); - it('fails validation if appObjectId and appDisplayName are specified', async () => { - sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => { - if (settingName === settingsNames.prompt) { - return false; - } - - return defaultValue; - }); - - const actual = await command.validate({ options: { appDisplayName: CommandActionParameters.appNameWithRoleAssignments, appObjectId: CommandActionParameters.objectIdWithRoleAssignments } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation if the appObjectId is not a valid GUID', () => { + const actual = commandOptionsSchema.safeParse({ appObjectId: '123' }); + assert.strictEqual(actual.success, false); }); - it('passes validation when the appId option specified', async () => { - const actual = await command.validate({ options: { appId: CommandActionParameters.appIdWithNoRoleAssignments } }, commandInfo); - assert.strictEqual(actual, true); + it('fails validation if both appId and appDisplayName are specified', () => { + const actual = commandOptionsSchema.safeParse({ appId: CommandActionParameters.appIdWithNoRoleAssignments, appDisplayName: CommandActionParameters.appNameWithRoleAssignments }); + assert.strictEqual(actual.success, false); }); - it('supports specifying appId', () => { - const options = command.options; - let containsOption = false; - options.forEach(o => { - if (o.option.indexOf('--appId') > -1) { - containsOption = true; - } - }); - assert(containsOption); + it('fails validation if appObjectId and appDisplayName are specified', () => { + const actual = commandOptionsSchema.safeParse({ appDisplayName: CommandActionParameters.appNameWithRoleAssignments, appObjectId: CommandActionParameters.objectIdWithRoleAssignments }); + assert.strictEqual(actual.success, false); }); - it('supports specifying appDisplayName', () => { - const options = command.options; - let containsOption = false; - options.forEach(o => { - if (o.option.indexOf('--appDisplayName') > -1) { - containsOption = true; - } - }); - assert(containsOption); + it('passes validation when the appId option specified', () => { + const actual = commandOptionsSchema.safeParse({ appId: CommandActionParameters.appIdWithNoRoleAssignments }); + assert.strictEqual(actual.success, true); }); }); diff --git a/src/m365/entra/commands/approleassignment/approleassignment-list.ts b/src/m365/entra/commands/approleassignment/approleassignment-list.ts index 3d82c94fac6..4d766dfec43 100644 --- a/src/m365/entra/commands/approleassignment/approleassignment-list.ts +++ b/src/m365/entra/commands/approleassignment/approleassignment-list.ts @@ -1,22 +1,25 @@ import { AppRole, AppRoleAssignment, ServicePrincipal } from '@microsoft/microsoft-graph-types'; +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 { validation } from '../../../../utils/validation.js'; import GraphCommand from '../../../base/GraphCommand.js'; import commands from '../../commands.js'; +export const options = z.strictObject({ + ...globalOptionsZod.shape, + appId: z.uuid().optional().alias('i'), + appDisplayName: z.string().optional().alias('n'), + appObjectId: z.uuid().optional() +}); + +declare type Options = z.infer; + interface CommandArgs { options: Options; } -interface Options extends GlobalOptions { - appId?: string; - appDisplayName?: string; - appObjectId?: string; -} - class EntraAppRoleAssignmentListCommand extends GraphCommand { public get name(): string { return commands.APPROLEASSIGNMENT_LIST; @@ -26,57 +29,19 @@ class EntraAppRoleAssignmentListCommand extends GraphCommand { return 'Lists app role assignments for the specified application registration'; } - constructor() { - super(); - - this.#initTelemetry(); - this.#initOptions(); - this.#initValidators(); - this.#initOptionSets(); - } - - #initTelemetry(): void { - this.telemetry.push((args: CommandArgs) => { - Object.assign(this.telemetryProperties, { - appId: typeof args.options.appId !== 'undefined', - appDisplayName: typeof args.options.appDisplayName !== 'undefined', - appObjectId: typeof args.options.appObjectId !== 'undefined' - }); - }); - } - - #initOptions(): void { - this.options.unshift( - { - option: '-i, --appId [appId]' - }, - { - option: '-n, --appDisplayName [appDisplayName]' - }, - { - option: '--appObjectId [appObjectId]' - } - ); + public get schema(): z.ZodType | undefined { + return options; } - #initValidators(): void { - this.validators.push( - async (args: CommandArgs) => { - if (args.options.appId && !validation.isValidGuid(args.options.appId)) { - return `${args.options.appId} is not a valid GUID`; - } - - if (args.options.appObjectId && !validation.isValidGuid(args.options.appObjectId)) { - return `${args.options.appObjectId} is not a valid GUID`; + public getRefinedSchema(schema: typeof options): z.ZodObject | undefined { + return schema + .refine(options => [options.appId, options.appObjectId, options.appDisplayName].filter(o => o !== undefined).length === 1, { + error: 'Specify either appId, appObjectId, or appDisplayName', + params: { + customCode: 'optionSet', + options: ['appId', 'appObjectId', 'appDisplayName'] } - - return true; - } - ); - } - - #initOptionSets(): void { - this.optionSets.push({ options: ['appId', 'appObjectId', 'appDisplayName'] }); + }); } public defaultProperties(): string[] | undefined { diff --git a/src/m365/entra/commands/approleassignment/approleassignment-remove.spec.ts b/src/m365/entra/commands/approleassignment/approleassignment-remove.spec.ts index a847d386f51..825916b5c92 100644 --- a/src/m365/entra/commands/approleassignment/approleassignment-remove.spec.ts +++ b/src/m365/entra/commands/approleassignment/approleassignment-remove.spec.ts @@ -12,13 +12,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 './approleassignment-remove.js'; -import { settingsNames } from '../../../../settingsNames.js'; +import command, { options } from './approleassignment-remove.js'; describe(commands.APPROLEASSIGNMENT_REMOVE, () => { let log: string[]; let logger: Logger; let commandInfo: CommandInfo; + let commandOptionsSchema: typeof options; let promptIssued: boolean = false; let deleteRequestStub: sinon.SinonStub; @@ -29,6 +29,7 @@ describe(commands.APPROLEASSIGNMENT_REMOVE, () => { sinon.stub(session, 'getId').returns(''); auth.connection.active = true; commandInfo = cli.getCommandInfo(command); + commandOptionsSchema = commandInfo.command.getSchemaToParse()! as typeof options; }); beforeEach(() => { @@ -243,104 +244,39 @@ describe(commands.APPROLEASSIGNMENT_REMOVE, () => { new CommandError(`Resource '' does not exist or one of its queried reference-property objects are not present`)); }); - it('fails validation if neither appId, appObjectId nor appDisplayName are not specified', async () => { - sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => { - if (settingName === settingsNames.prompt) { - return false; - } - - return defaultValue; - }); - - const actual = await command.validate({ options: { resource: 'abc', scopes: 'abc' } }, commandInfo); - assert.notStrictEqual(actual, true); - }); - - it('fails validation if the appId is not a valid GUID', async () => { - const actual = await command.validate({ options: { appId: '123', resource: 'abc', scopes: 'abc' } }, commandInfo); - assert.notStrictEqual(actual, true); - }); - - it('fails validation if the appObjectId is not a valid GUID', async () => { - const actual = await command.validate({ options: { appObjectId: '123', resource: 'abc', scopes: 'abc' } }, commandInfo); - assert.notStrictEqual(actual, true); - }); - - it('fails validation if both appId and appDisplayName are specified', async () => { - sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => { - if (settingName === settingsNames.prompt) { - return false; - } - - return defaultValue; - }); - - const actual = await command.validate({ options: { appId: '123', appDisplayName: 'abc', resource: 'abc', scopes: 'abc' } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation if neither appId, appObjectId nor appDisplayName are not specified', () => { + const actual = commandOptionsSchema.safeParse({ resource: 'abc', scopes: 'abc' }); + assert.strictEqual(actual.success, false); }); - it('fails validation if both appObjectId and appDisplayName are specified', async () => { - sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => { - if (settingName === settingsNames.prompt) { - return false; - } - - return defaultValue; - }); - - const actual = await command.validate({ options: { appObjectId: '123', appDisplayName: 'abc', resource: 'abc', scopes: 'abc' } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation if the appId is not a valid GUID', () => { + const actual = commandOptionsSchema.safeParse({ appId: '123', resource: 'abc', scopes: 'abc' }); + assert.strictEqual(actual.success, false); }); - it('fails validation if both appObjectId, appId and appDisplayName are specified', async () => { - sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => { - if (settingName === settingsNames.prompt) { - return false; - } - - return defaultValue; - }); - - const actual = await command.validate({ options: { appId: '123', appObjectId: '123', appDisplayName: 'abc', resource: 'abc', scopes: 'abc' } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation if the appObjectId is not a valid GUID', () => { + const actual = commandOptionsSchema.safeParse({ appObjectId: '123', resource: 'abc', scopes: 'abc' }); + assert.strictEqual(actual.success, false); }); - it('passes validation when the appId option specified', async () => { - const actual = await command.validate({ options: { appId: '57907bf8-73fa-43a6-89a5-1f603e29e452', resource: 'abc', scopes: 'abc' } }, commandInfo); - assert.strictEqual(actual, true); + it('fails validation if both appId and appDisplayName are specified', () => { + const actual = commandOptionsSchema.safeParse({ appId: '57907bf8-73fa-43a6-89a5-1f603e29e452', appDisplayName: 'abc', resource: 'abc', scopes: 'abc' }); + assert.strictEqual(actual.success, false); }); - it('supports specifying appId', () => { - const options = command.options; - let containsOption = false; - options.forEach(o => { - if (o.option.indexOf('--appId') > -1) { - containsOption = true; - } - }); - assert(containsOption); + it('fails validation if both appObjectId and appDisplayName are specified', () => { + const actual = commandOptionsSchema.safeParse({ appObjectId: '57907bf8-73fa-43a6-89a5-1f603e29e452', appDisplayName: 'abc', resource: 'abc', scopes: 'abc' }); + assert.strictEqual(actual.success, false); }); - it('supports specifying appDisplayName', () => { - const options = command.options; - let containsOption = false; - options.forEach(o => { - if (o.option.indexOf('--appDisplayName') > -1) { - containsOption = true; - } - }); - assert(containsOption); + it('fails validation if both appObjectId, appId and appDisplayName are specified', () => { + const actual = commandOptionsSchema.safeParse({ appId: '57907bf8-73fa-43a6-89a5-1f603e29e452', appObjectId: '57907bf8-73fa-43a6-89a5-1f603e29e452', appDisplayName: 'abc', resource: 'abc', scopes: 'abc' }); + assert.strictEqual(actual.success, false); }); - 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); + it('passes validation when the appId option specified', () => { + const actual = commandOptionsSchema.safeParse({ appId: '57907bf8-73fa-43a6-89a5-1f603e29e452', resource: 'abc', scopes: 'abc' }); + assert.strictEqual(actual.success, true); }); }); diff --git a/src/m365/entra/commands/approleassignment/approleassignment-remove.ts b/src/m365/entra/commands/approleassignment/approleassignment-remove.ts index 362f2ece74a..d0e4ab86e2a 100644 --- a/src/m365/entra/commands/approleassignment/approleassignment-remove.ts +++ b/src/m365/entra/commands/approleassignment/approleassignment-remove.ts @@ -1,7 +1,8 @@ import os from 'os'; +import { z } from 'zod'; import { cli } from '../../../../cli/cli.js'; import { Logger } from '../../../../cli/Logger.js'; -import GlobalOptions from '../../../../GlobalOptions.js'; +import { globalOptionsZod } from '../../../../Command.js'; import request, { CliRequestOptions } from '../../../../request.js'; import { formatting } from '../../../../utils/formatting.js'; import { validation } from '../../../../utils/validation.js'; @@ -9,19 +10,22 @@ import GraphCommand from '../../../base/GraphCommand.js'; import commands from '../../commands.js'; import { AppRole, AppRoleAssignment, ServicePrincipal } from '@microsoft/microsoft-graph-types'; +export const options = z.strictObject({ + ...globalOptionsZod.shape, + appId: z.uuid().optional(), + appObjectId: z.uuid().optional(), + appDisplayName: z.string().optional(), + resource: z.string().alias('r'), + scopes: z.string().alias('s'), + force: z.boolean().optional().alias('f') +}); + +declare type Options = z.infer; + interface CommandArgs { options: Options; } -interface Options extends GlobalOptions { - appId?: string; - appDisplayName?: string; - appObjectId?: string; - resource: string; - scopes: string; - force?: boolean; -} - class EntraAppRoleAssignmentRemoveCommand extends GraphCommand { public get name(): string { return commands.APPROLEASSIGNMENT_REMOVE; @@ -31,68 +35,19 @@ class EntraAppRoleAssignmentRemoveCommand extends GraphCommand { return 'Deletes an app role assignment for the specified Entra Application Registration'; } - constructor() { - super(); - - this.#initTelemetry(); - this.#initOptions(); - this.#initValidators(); - this.#initOptionSets(); - } - - #initTelemetry(): void { - this.telemetry.push((args: CommandArgs) => { - Object.assign(this.telemetryProperties, { - appId: typeof args.options.appId !== 'undefined', - appDisplayName: typeof args.options.appDisplayName !== 'undefined', - appObjectId: typeof args.options.appObjectId !== 'undefined', - force: (!!args.options.force).toString() - }); - }); + public get schema(): z.ZodType | undefined { + return options; } - #initOptions(): void { - this.options.unshift( - { - option: '--appId [appId]' - }, - { - option: '--appObjectId [appObjectId]' - }, - { - option: '--appDisplayName [appDisplayName]' - }, - { - option: '-r, --resource ', - autocomplete: ['Microsoft Graph', 'SharePoint', 'OneNote', 'Exchange', 'Microsoft Forms', 'Azure Active Directory Graph', 'Skype for Business'] - }, - { - option: '-s, --scopes ' - }, - { - option: '-f, --force' - } - ); - } - - #initValidators(): void { - this.validators.push( - async (args: CommandArgs) => { - if (args.options.appId && !validation.isValidGuid(args.options.appId)) { - return `${args.options.appId} is not a valid GUID`; - } - - if (args.options.appObjectId && !validation.isValidGuid(args.options.appObjectId)) { - return `${args.options.appObjectId} is not a valid GUID`; + public getRefinedSchema(schema: typeof options): z.ZodObject | undefined { + return schema + .refine(options => [options.appId, options.appObjectId, options.appDisplayName].filter(o => o !== undefined).length === 1, { + error: 'Specify either appId, appObjectId, or appDisplayName', + params: { + customCode: 'optionSet', + options: ['appId', 'appObjectId', 'appDisplayName'] } - - return true; - } - ); - } - - #initOptionSets(): void { - this.optionSets.push({ options: ['appId', 'appObjectId', 'appDisplayName'] }); + }); } public async commandAction(logger: Logger, args: CommandArgs): Promise { diff --git a/src/m365/entra/commands/enterpriseapp/enterpriseapp-add.spec.ts b/src/m365/entra/commands/enterpriseapp/enterpriseapp-add.spec.ts index 86209ea93cf..ab5abd0b540 100644 --- a/src/m365/entra/commands/enterpriseapp/enterpriseapp-add.spec.ts +++ b/src/m365/entra/commands/enterpriseapp/enterpriseapp-add.spec.ts @@ -11,7 +11,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 './enterpriseapp-add.js'; +import command, { options } from './enterpriseapp-add.js'; import { settingsNames } from '../../../../settingsNames.js'; describe(commands.ENTERPRISEAPP_ADD, () => { @@ -19,6 +19,7 @@ describe(commands.ENTERPRISEAPP_ADD, () => { let logger: Logger; let loggerLogSpy: sinon.SinonSpy; let commandInfo: CommandInfo; + let commandOptionsSchema: typeof options; before(() => { sinon.stub(auth, 'restoreAuth').resolves(); @@ -27,6 +28,7 @@ describe(commands.ENTERPRISEAPP_ADD, () => { sinon.stub(session, 'getId').returns(''); auth.connection.active = true; commandInfo = cli.getCommandInfo(command); + commandOptionsSchema = commandInfo.command.getSchemaToParse()! as typeof options; }); beforeEach(() => { @@ -73,114 +75,49 @@ describe(commands.ENTERPRISEAPP_ADD, () => { assert.deepStrictEqual(alias, [commands.SP_ADD]); }); - it('fails validation if neither the id, displayName, nor objectId option specified', async () => { - sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => { - if (settingName === settingsNames.prompt) { - return false; - } - - return defaultValue; - }); - - const actual = await command.validate({ options: {} }, commandInfo); - assert.notStrictEqual(actual, true); - }); - - it('fails validation if the id is not a valid GUID', async () => { - const actual = await command.validate({ options: { id: '123' } }, commandInfo); - assert.notStrictEqual(actual, true); - }); - - it('fails validation if the objectId is not a valid GUID', async () => { - const actual = await command.validate({ options: { objectId: '123' } }, commandInfo); - assert.notStrictEqual(actual, true); - }); - - it('fails validation if both id and displayName are specified', async () => { - sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => { - if (settingName === settingsNames.prompt) { - return false; - } - - return defaultValue; - }); - - const actual = await command.validate({ options: { id: '00000000-0000-0000-0000-000000000000', displayName: 'abc' } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation if neither the id, displayName, nor objectId option specified', () => { + const actual = commandOptionsSchema.safeParse({}); + assert.strictEqual(actual.success, false); }); - it('fails validation if both displayName and objectId are specified', async () => { - sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => { - if (settingName === settingsNames.prompt) { - return false; - } - - return defaultValue; - }); - - const actual = await command.validate({ options: { displayName: 'abc', objectId: '123' } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation if the id is not a valid GUID', () => { + const actual = commandOptionsSchema.safeParse({ id: '123' }); + assert.strictEqual(actual.success, false); }); - it('fails validation if both id and objectId are specified', async () => { - sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => { - if (settingName === settingsNames.prompt) { - return false; - } - - return defaultValue; - }); - - const actual = await command.validate({ options: { id: '00000000-0000-0000-0000-000000000000', objectId: '00000000-0000-0000-0000-000000000000' } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation if the objectId is not a valid GUID', () => { + const actual = commandOptionsSchema.safeParse({ objectId: '123' }); + assert.strictEqual(actual.success, false); }); - it('passes validation when the id option specified', async () => { - const actual = await command.validate({ options: { id: '00000000-0000-0000-0000-000000000000' } }, commandInfo); - assert.strictEqual(actual, true); + it('fails validation if both id and displayName are specified', () => { + const actual = commandOptionsSchema.safeParse({ id: '00000000-0000-0000-0000-000000000000', displayName: 'abc' }); + assert.strictEqual(actual.success, false); }); - it('passes validation when the displayName option specified', async () => { - const actual = await command.validate({ options: { displayName: 'abc' } }, commandInfo); - assert.strictEqual(actual, true); + it('fails validation if both displayName and objectId are specified', () => { + const actual = commandOptionsSchema.safeParse({ displayName: 'abc', objectId: '00000000-0000-0000-0000-000000000000' }); + assert.strictEqual(actual.success, false); }); - it('passes validation when the objectId option specified', async () => { - const actual = await command.validate({ options: { objectId: '00000000-0000-0000-0000-000000000000' } }, commandInfo); - assert.strictEqual(actual, true); + it('fails validation if both id and objectId are specified', () => { + const actual = commandOptionsSchema.safeParse({ id: '00000000-0000-0000-0000-000000000000', objectId: '00000000-0000-0000-0000-000000000000' }); + assert.strictEqual(actual.success, false); }); - it('supports specifying id', () => { - const options = command.options; - let containsOption = false; - options.forEach(o => { - if (o.option.indexOf('--id') > -1) { - containsOption = true; - } - }); - assert(containsOption); + it('passes validation when the id option specified', () => { + const actual = commandOptionsSchema.safeParse({ id: '00000000-0000-0000-0000-000000000000' }); + assert.strictEqual(actual.success, true); }); - it('supports specifying displayName', () => { - const options = command.options; - let containsOption = false; - options.forEach(o => { - if (o.option.indexOf('--displayName') > -1) { - containsOption = true; - } - }); - assert(containsOption); + it('passes validation when the displayName option specified', () => { + const actual = commandOptionsSchema.safeParse({ displayName: 'abc' }); + assert.strictEqual(actual.success, true); }); - it('supports specifying objectId', () => { - const options = command.options; - let containsOption = false; - options.forEach(o => { - if (o.option.indexOf('--objectId') > -1) { - containsOption = true; - } - }); - assert(containsOption); + it('passes validation when the objectId option specified', () => { + const actual = commandOptionsSchema.safeParse({ objectId: '00000000-0000-0000-0000-000000000000' }); + assert.strictEqual(actual.success, true); }); it('correctly handles API OData error', async () => { diff --git a/src/m365/entra/commands/enterpriseapp/enterpriseapp-add.ts b/src/m365/entra/commands/enterpriseapp/enterpriseapp-add.ts index db3f5bf5138..b4665feab5d 100644 --- a/src/m365/entra/commands/enterpriseapp/enterpriseapp-add.ts +++ b/src/m365/entra/commands/enterpriseapp/enterpriseapp-add.ts @@ -1,22 +1,25 @@ +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 { validation } from '../../../../utils/validation.js'; import GraphCommand from '../../../base/GraphCommand.js'; import commands from '../../commands.js'; import { cli } from '../../../../cli/cli.js'; +export const options = z.strictObject({ + ...globalOptionsZod.shape, + id: z.uuid().optional().alias('i'), + displayName: z.string().optional().alias('n'), + objectId: z.uuid().optional() +}); + +declare type Options = z.infer; + interface CommandArgs { options: Options; } -interface Options extends GlobalOptions { - id?: string; - displayName?: string; - objectId?: string; -} - class EntraEnterpriseAppAddCommand extends GraphCommand { public get name(): string { return commands.ENTERPRISEAPP_ADD; @@ -30,57 +33,19 @@ class EntraEnterpriseAppAddCommand extends GraphCommand { return [commands.SP_ADD]; } - constructor() { - super(); - - this.#initTelemetry(); - this.#initOptions(); - this.#initValidators(); - this.#initOptionSets(); - } - - #initTelemetry(): void { - this.telemetry.push((args: CommandArgs) => { - Object.assign(this.telemetryProperties, { - id: (!(!args.options.id)).toString(), - displayName: (!(!args.options.displayName)).toString(), - objectId: (!(!args.options.objectId)).toString() - }); - }); - } - - #initOptions(): void { - this.options.unshift( - { - option: '-i, --id [id]' - }, - { - option: '-n, --displayName [displayName]' - }, - { - option: '--objectId [objectId]' - } - ); + public get schema(): z.ZodType | undefined { + return options; } - #initValidators(): void { - this.validators.push( - async (args: CommandArgs) => { - if (args.options.id && !validation.isValidGuid(args.options.id)) { - return `${args.options.id} is not a valid GUID`; - } - - if (args.options.objectId && !validation.isValidGuid(args.options.objectId)) { - return `${args.options.objectId} is not a valid GUID`; + public getRefinedSchema(schema: typeof options): z.ZodObject | undefined { + return schema + .refine(options => [options.id, options.displayName, options.objectId].filter(o => o !== undefined).length === 1, { + error: 'Specify either id, displayName, or objectId', + params: { + customCode: 'optionSet', + options: ['id', 'displayName', 'objectId'] } - - return true; - } - ); - } - - #initOptionSets(): void { - this.optionSets.push({ options: ['id', 'displayName', 'objectId'] }); + }); } private async getAppId(args: CommandArgs): Promise { diff --git a/src/m365/entra/commands/enterpriseapp/enterpriseapp-get.spec.ts b/src/m365/entra/commands/enterpriseapp/enterpriseapp-get.spec.ts index 47c9dd78c36..06764d9a5be 100644 --- a/src/m365/entra/commands/enterpriseapp/enterpriseapp-get.spec.ts +++ b/src/m365/entra/commands/enterpriseapp/enterpriseapp-get.spec.ts @@ -11,7 +11,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 './enterpriseapp-get.js'; +import command, { options } from './enterpriseapp-get.js'; import { settingsNames } from '../../../../settingsNames.js'; describe(commands.ENTERPRISEAPP_GET, () => { @@ -19,6 +19,7 @@ describe(commands.ENTERPRISEAPP_GET, () => { let logger: Logger; let loggerLogSpy: sinon.SinonSpy; let commandInfo: CommandInfo; + let commandOptionsSchema: typeof options; const spAppInfo = { "value": [ @@ -43,6 +44,7 @@ describe(commands.ENTERPRISEAPP_GET, () => { sinon.stub(session, 'getId').returns(''); auth.connection.active = true; commandInfo = cli.getCommandInfo(command); + commandOptionsSchema = commandInfo.command.getSchemaToParse()! as typeof options; }); beforeEach(() => { @@ -270,97 +272,43 @@ describe(commands.ENTERPRISEAPP_GET, () => { }), new CommandError(`The specified Entra app does not exist`)); }); - it('fails validation if neither the id nor the displayName option specified', async () => { - sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => { - if (settingName === settingsNames.prompt) { - return false; - } - - return defaultValue; - }); - - const actual = await command.validate({ options: {} }, commandInfo); - assert.notStrictEqual(actual, true); - }); - - it('fails validation if the id is not a valid GUID', async () => { - const actual = await command.validate({ options: { id: '123' } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation if neither the id nor the displayName option specified', () => { + const actual = commandOptionsSchema.safeParse({}); + assert.strictEqual(actual.success, false); }); - it('passes validation when the id option specified', async () => { - const actual = await command.validate({ options: { id: '6a7b1395-d313-4682-8ed4-65a6265a6320' } }, commandInfo); - assert.strictEqual(actual, true); + it('fails validation if the id is not a valid GUID', () => { + const actual = commandOptionsSchema.safeParse({ id: '123' }); + assert.strictEqual(actual.success, false); }); - it('passes validation when the displayName option specified', async () => { - const actual = await command.validate({ options: { displayName: 'Microsoft Graph' } }, commandInfo); - assert.strictEqual(actual, true); - }); - - it('fails validation when both the id and displayName are specified', async () => { - sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => { - if (settingName === settingsNames.prompt) { - return false; - } - - return defaultValue; - }); - - const actual = await command.validate({ options: { id: '6a7b1395-d313-4682-8ed4-65a6265a6320', displayName: 'Microsoft Graph' } }, commandInfo); - assert.notStrictEqual(actual, true); + it('passes validation when the id option specified', () => { + const actual = commandOptionsSchema.safeParse({ id: '6a7b1395-d313-4682-8ed4-65a6265a6320' }); + assert.strictEqual(actual.success, true); }); - it('fails validation if the objectId is not a valid GUID', async () => { - const actual = await command.validate({ options: { objectId: '123' } }, commandInfo); - assert.notStrictEqual(actual, true); + it('passes validation when the displayName option specified', () => { + const actual = commandOptionsSchema.safeParse({ displayName: 'Microsoft Graph' }); + assert.strictEqual(actual.success, true); }); - it('fails validation if both id and displayName are specified', async () => { - sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => { - if (settingName === settingsNames.prompt) { - return false; - } - - return defaultValue; - }); - - const actual = await command.validate({ options: { id: '123', displayName: 'abc' } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation when both the id and displayName are specified', () => { + const actual = commandOptionsSchema.safeParse({ id: '6a7b1395-d313-4682-8ed4-65a6265a6320', displayName: 'Microsoft Graph' }); + assert.strictEqual(actual.success, false); }); - it('fails validation if objectId and displayName are specified', async () => { - sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => { - if (settingName === settingsNames.prompt) { - return false; - } - - return defaultValue; - }); - - const actual = await command.validate({ options: { displayName: 'abc', objectId: '123' } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation if the objectId is not a valid GUID', () => { + const actual = commandOptionsSchema.safeParse({ objectId: '123' }); + assert.strictEqual(actual.success, false); }); - it('supports specifying id', () => { - const options = command.options; - let containsOption = false; - options.forEach(o => { - if (o.option.indexOf('--id') > -1) { - containsOption = true; - } - }); - assert(containsOption); + it('fails validation if both id and displayName are specified', () => { + const actual = commandOptionsSchema.safeParse({ id: '6a7b1395-d313-4682-8ed4-65a6265a6320', displayName: 'abc' }); + assert.strictEqual(actual.success, false); }); - it('supports specifying displayName', () => { - const options = command.options; - let containsOption = false; - options.forEach(o => { - if (o.option.indexOf('--displayName') > -1) { - containsOption = true; - } - }); - assert(containsOption); + it('fails validation if objectId and displayName are specified', () => { + const actual = commandOptionsSchema.safeParse({ displayName: 'abc', objectId: '6a7b1395-d313-4682-8ed4-65a6265a6320' }); + assert.strictEqual(actual.success, false); }); }); diff --git a/src/m365/entra/commands/enterpriseapp/enterpriseapp-get.ts b/src/m365/entra/commands/enterpriseapp/enterpriseapp-get.ts index 48798595b9a..e517eff54c1 100644 --- a/src/m365/entra/commands/enterpriseapp/enterpriseapp-get.ts +++ b/src/m365/entra/commands/enterpriseapp/enterpriseapp-get.ts @@ -1,22 +1,25 @@ +import { z } from 'zod'; import { cli } from '../../../../cli/cli.js'; import { Logger } from '../../../../cli/Logger.js'; -import GlobalOptions from '../../../../GlobalOptions.js'; +import { globalOptionsZod } from '../../../../Command.js'; import request, { CliRequestOptions } from '../../../../request.js'; import { formatting } from '../../../../utils/formatting.js'; -import { validation } from '../../../../utils/validation.js'; import GraphCommand from '../../../base/GraphCommand.js'; import commands from '../../commands.js'; +export const options = z.strictObject({ + ...globalOptionsZod.shape, + id: z.uuid().optional().alias('i'), + displayName: z.string().optional().alias('n'), + objectId: z.uuid().optional() +}); + +declare type Options = z.infer; + interface CommandArgs { options: Options; } -interface Options extends GlobalOptions { - id?: string; - displayName?: string; - objectId?: string; -} - class EntraEnterpriseAppGetCommand extends GraphCommand { public get name(): string { return commands.ENTERPRISEAPP_GET; @@ -30,57 +33,19 @@ class EntraEnterpriseAppGetCommand extends GraphCommand { return [commands.SP_GET]; } - constructor() { - super(); - - this.#initTelemetry(); - this.#initOptions(); - this.#initValidators(); - this.#initOptionSets(); - } - - #initTelemetry(): void { - this.telemetry.push((args: CommandArgs) => { - Object.assign(this.telemetryProperties, { - id: (!(!args.options.id)).toString(), - displayName: (!(!args.options.displayName)).toString(), - objectId: (!(!args.options.objectId)).toString() - }); - }); - } - - #initOptions(): void { - this.options.unshift( - { - option: '-i, --id [id]' - }, - { - option: '-n, --displayName [displayName]' - }, - { - option: '--objectId [objectId]' - } - ); + public get schema(): z.ZodType | undefined { + return options; } - #initValidators(): void { - this.validators.push( - async (args: CommandArgs) => { - if (args.options.id && !validation.isValidGuid(args.options.id)) { - return `${args.options.id} is not a valid GUID`; - } - - if (args.options.objectId && !validation.isValidGuid(args.options.objectId)) { - return `${args.options.objectId} is not a valid GUID`; + public getRefinedSchema(schema: typeof options): z.ZodObject | undefined { + return schema + .refine(options => [options.id, options.displayName, options.objectId].filter(o => o !== undefined).length === 1, { + error: 'Specify either id, displayName, or objectId', + params: { + customCode: 'optionSet', + options: ['id', 'displayName', 'objectId'] } - - return true; - } - ); - } - - #initOptionSets(): void { - this.optionSets.push({ options: ['id', 'displayName', 'objectId'] }); + }); } private async getSpId(args: CommandArgs): Promise { diff --git a/src/m365/entra/commands/enterpriseapp/enterpriseapp-list.ts b/src/m365/entra/commands/enterpriseapp/enterpriseapp-list.ts index 3995679b25f..b89e206d009 100644 --- a/src/m365/entra/commands/enterpriseapp/enterpriseapp-list.ts +++ b/src/m365/entra/commands/enterpriseapp/enterpriseapp-list.ts @@ -1,18 +1,22 @@ +import { z } from 'zod'; +import { globalOptionsZod } from '../../../../Command.js'; import { Logger } from '../../../../cli/Logger.js'; -import GlobalOptions from '../../../../GlobalOptions.js'; import { odata } from '../../../../utils/odata.js'; import GraphCommand from '../../../base/GraphCommand.js'; import commands from '../../commands.js'; +export const options = z.strictObject({ + ...globalOptionsZod.shape, + displayName: z.string().optional().alias('n'), + tag: z.string().optional() +}); + +declare type Options = z.infer; + interface CommandArgs { options: Options; } -interface Options extends GlobalOptions { - displayName?: string; - tag?: string; -} - class EntraEnterpriseAppListCommand extends GraphCommand { public get name(): string { return commands.ENTERPRISEAPP_LIST; @@ -30,31 +34,8 @@ class EntraEnterpriseAppListCommand extends GraphCommand { return [commands.SP_LIST]; } - constructor() { - super(); - - this.#initTelemetry(); - this.#initOptions(); - } - - #initTelemetry(): void { - this.telemetry.push((args: CommandArgs) => { - Object.assign(this.telemetryProperties, { - displayName: typeof args.options.displayName !== 'undefined', - tag: typeof args.options.tag !== 'undefined' - }); - }); - } - - #initOptions(): void { - this.options.unshift( - { - option: '-n, --displayName [displayName]' - }, - { - option: '--tag [tag]' - } - ); + public get schema(): z.ZodType | undefined { + return options; } public async commandAction(logger: Logger, args: CommandArgs): Promise { diff --git a/src/m365/entra/commands/enterpriseapp/enterpriseapp-remove.spec.ts b/src/m365/entra/commands/enterpriseapp/enterpriseapp-remove.spec.ts index 69ae6b6614c..043f52c6f1c 100644 --- a/src/m365/entra/commands/enterpriseapp/enterpriseapp-remove.spec.ts +++ b/src/m365/entra/commands/enterpriseapp/enterpriseapp-remove.spec.ts @@ -11,13 +11,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 './enterpriseapp-remove.js'; +import command, { options } from './enterpriseapp-remove.js'; import { settingsNames } from '../../../../settingsNames.js'; describe(commands.ENTERPRISEAPP_REMOVE, () => { let log: string[]; let logger: Logger; let commandInfo: CommandInfo; + let commandOptionsSchema: typeof options; let promptIssued: boolean = false; const spAppInfo = { @@ -55,6 +56,7 @@ describe(commands.ENTERPRISEAPP_REMOVE, () => { sinon.stub(session, 'getId').returns(''); auth.connection.active = true; commandInfo = cli.getCommandInfo(command); + commandOptionsSchema = commandInfo.command.getSchemaToParse()! as typeof options; sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => settingName === settingsNames.prompt ? false : defaultValue); }); @@ -126,44 +128,44 @@ describe(commands.ENTERPRISEAPP_REMOVE, () => { }), new CommandError(`The specified enterprise application does not exist.`)); }); - it('fails validation if neither the id nor the displayName option is specified', async () => { - const actual = await command.validate({ options: {} }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation if neither the id nor the displayName option is specified', () => { + const actual = commandOptionsSchema.safeParse({}); + assert.strictEqual(actual.success, false); }); - it('fails validation if the id is not a valid GUID', async () => { - const actual = await command.validate({ options: { id: '123' } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation if the id is not a valid GUID', () => { + const actual = commandOptionsSchema.safeParse({ id: '123' }); + assert.strictEqual(actual.success, false); }); - it('passes validation when the id option is specified', async () => { - const actual = await command.validate({ options: { id: '6a7b1395-d313-4682-8ed4-65a6265a6320' } }, commandInfo); - assert.strictEqual(actual, true); + it('passes validation when the id option is specified', () => { + const actual = commandOptionsSchema.safeParse({ id: '6a7b1395-d313-4682-8ed4-65a6265a6320' }); + assert.strictEqual(actual.success, true); }); - it('passes validation when the displayName option is specified', async () => { - const actual = await command.validate({ options: { displayName: 'Contoso app' } }, commandInfo); - assert.strictEqual(actual, true); + it('passes validation when the displayName option is specified', () => { + const actual = commandOptionsSchema.safeParse({ displayName: 'Contoso app' }); + assert.strictEqual(actual.success, true); }); - it('fails validation when both the id and displayName are specified', async () => { - const actual = await command.validate({ options: { id: '6a7b1395-d313-4682-8ed4-65a6265a6320', displayName: 'Contoso app' } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation when both the id and displayName are specified', () => { + const actual = commandOptionsSchema.safeParse({ id: '6a7b1395-d313-4682-8ed4-65a6265a6320', displayName: 'Contoso app' }); + assert.strictEqual(actual.success, false); }); - it('fails validation if the objectId is not a valid GUID', async () => { - const actual = await command.validate({ options: { objectId: '123' } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation if the objectId is not a valid GUID', () => { + const actual = commandOptionsSchema.safeParse({ objectId: '123' }); + assert.strictEqual(actual.success, false); }); - it('fails validation if both id and displayName are specified', async () => { - const actual = await command.validate({ options: { id: '123', displayName: 'abc' } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation if both id and displayName are specified', () => { + const actual = commandOptionsSchema.safeParse({ id: '6a7b1395-d313-4682-8ed4-65a6265a6320', displayName: 'abc' }); + assert.strictEqual(actual.success, false); }); - it('fails validation if objectId and displayName are specified', async () => { - const actual = await command.validate({ options: { displayName: 'abc', objectId: '123' } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation if objectId and displayName are specified', () => { + const actual = commandOptionsSchema.safeParse({ displayName: 'abc', objectId: '6a7b1395-d313-4682-8ed4-65a6265a6320' }); + assert.strictEqual(actual.success, false); }); it('prompts before removing the enterprise application when force option not passed', async () => { diff --git a/src/m365/entra/commands/enterpriseapp/enterpriseapp-remove.ts b/src/m365/entra/commands/enterpriseapp/enterpriseapp-remove.ts index 1cfae670c48..290117f9086 100644 --- a/src/m365/entra/commands/enterpriseapp/enterpriseapp-remove.ts +++ b/src/m365/entra/commands/enterpriseapp/enterpriseapp-remove.ts @@ -1,24 +1,27 @@ +import { z } from 'zod'; import { cli } from '../../../../cli/cli.js'; import { Logger } from '../../../../cli/Logger.js'; -import GlobalOptions from '../../../../GlobalOptions.js'; +import { globalOptionsZod } from '../../../../Command.js'; import request, { CliRequestOptions } from '../../../../request.js'; import { odata } from '../../../../utils/odata.js'; import { formatting } from '../../../../utils/formatting.js'; -import { validation } from '../../../../utils/validation.js'; import GraphCommand from '../../../base/GraphCommand.js'; import commands from '../../commands.js'; +export const options = z.strictObject({ + ...globalOptionsZod.shape, + id: z.uuid().optional().alias('i'), + displayName: z.string().optional().alias('n'), + objectId: z.uuid().optional(), + force: z.boolean().optional().alias('f') +}); + +declare type Options = z.infer; + interface CommandArgs { options: Options; } -interface Options extends GlobalOptions { - id?: string; - displayName?: string; - objectId?: string; - force?: boolean; -} - class EntraEnterpriseAppRemoveCommand extends GraphCommand { public get name(): string { return commands.ENTERPRISEAPP_REMOVE; @@ -32,67 +35,19 @@ class EntraEnterpriseAppRemoveCommand extends GraphCommand { return [commands.SP_REMOVE]; } - constructor() { - super(); - - this.#initTelemetry(); - this.#initOptions(); - this.#initValidators(); - this.#initOptionSets(); - this.#initTypes(); - } - - #initTelemetry(): void { - this.telemetry.push((args: CommandArgs) => { - Object.assign(this.telemetryProperties, { - id: typeof args.options.id !== 'undefined', - displayName: typeof args.options.displayName !== 'undefined', - objectId: typeof args.options.objectId !== 'undefined', - force: !!args.options.force - }); - }); - } - - #initOptions(): void { - this.options.unshift( - { - option: '-i, --id [id]' - }, - { - option: '-n, --displayName [displayName]' - }, - { - option: '--objectId [objectId]' - }, - { - option: '-f, --force' - } - ); + public get schema(): z.ZodType | undefined { + return options; } - #initValidators(): void { - this.validators.push( - async (args: CommandArgs) => { - if (args.options.id && !validation.isValidGuid(args.options.id)) { - return `The option 'id' with value '${args.options.id}' is not a valid GUID.`; - } - - if (args.options.objectId && !validation.isValidGuid(args.options.objectId)) { - return `The option 'objectId' with value '${args.options.objectId}' is not a valid GUID.`; + public getRefinedSchema(schema: typeof options): z.ZodObject | undefined { + return schema + .refine(options => [options.id, options.displayName, options.objectId].filter(o => o !== undefined).length === 1, { + error: 'Specify either id, displayName, or objectId', + params: { + customCode: 'optionSet', + options: ['id', 'displayName', 'objectId'] } - - return true; - } - ); - } - - #initOptionSets(): void { - this.optionSets.push({ options: ['id', 'displayName', 'objectId'] }); - } - - #initTypes(): void { - this.types.string.push('id', 'displayName', 'objectId'); - this.types.boolean.push('force'); + }); } public async commandAction(logger: Logger, args: CommandArgs): Promise {