From 538e321119702d7a479bd9a244f1975ed867a60e Mon Sep 17 00:00:00 2001 From: Waldek Mastykarz Date: Thu, 28 May 2026 14:32:17 +0200 Subject: [PATCH 1/3] 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 { From de037505dcd9fea1fdfa8d40155eec15ef510b61 Mon Sep 17 00:00:00 2001 From: Waldek Mastykarz Date: Fri, 5 Jun 2026 09:11:22 +0200 Subject: [PATCH 2/3] Fix misleading test names in validation tests Updates test descriptions to accurately reflect all supported options (appId/appObjectId/appDisplayName and id/displayName/objectId) instead of mentioning only a subset. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../commands/approleassignment/approleassignment-add.spec.ts | 2 +- .../commands/approleassignment/approleassignment-list.spec.ts | 2 +- .../commands/approleassignment/approleassignment-remove.spec.ts | 2 +- src/m365/entra/commands/enterpriseapp/enterpriseapp-get.spec.ts | 2 +- .../entra/commands/enterpriseapp/enterpriseapp-remove.spec.ts | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/m365/entra/commands/approleassignment/approleassignment-add.spec.ts b/src/m365/entra/commands/approleassignment/approleassignment-add.spec.ts index cd0621b8802..ea9c2bbf512 100644 --- a/src/m365/entra/commands/approleassignment/approleassignment-add.spec.ts +++ b/src/m365/entra/commands/approleassignment/approleassignment-add.spec.ts @@ -276,7 +276,7 @@ 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', () => { + it('fails validation if neither appId, appObjectId, nor appDisplayName are specified', () => { const actual = commandOptionsSchema.safeParse({ resource: 'abc', scopes: 'abc' }); assert.strictEqual(actual.success, false); }); diff --git a/src/m365/entra/commands/approleassignment/approleassignment-list.spec.ts b/src/m365/entra/commands/approleassignment/approleassignment-list.spec.ts index a23dde155bb..81227021834 100644 --- a/src/m365/entra/commands/approleassignment/approleassignment-list.spec.ts +++ b/src/m365/entra/commands/approleassignment/approleassignment-list.spec.ts @@ -547,7 +547,7 @@ 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', () => { + it('fails validation if neither appId, appObjectId, nor appDisplayName are specified', () => { const actual = commandOptionsSchema.safeParse({}); assert.strictEqual(actual.success, false); }); diff --git a/src/m365/entra/commands/approleassignment/approleassignment-remove.spec.ts b/src/m365/entra/commands/approleassignment/approleassignment-remove.spec.ts index 825916b5c92..88642feab0d 100644 --- a/src/m365/entra/commands/approleassignment/approleassignment-remove.spec.ts +++ b/src/m365/entra/commands/approleassignment/approleassignment-remove.spec.ts @@ -244,7 +244,7 @@ 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', () => { + it('fails validation if neither appId, appObjectId, nor appDisplayName are specified', () => { const actual = commandOptionsSchema.safeParse({ resource: 'abc', scopes: 'abc' }); assert.strictEqual(actual.success, false); }); diff --git a/src/m365/entra/commands/enterpriseapp/enterpriseapp-get.spec.ts b/src/m365/entra/commands/enterpriseapp/enterpriseapp-get.spec.ts index 06764d9a5be..2b9dc315f7b 100644 --- a/src/m365/entra/commands/enterpriseapp/enterpriseapp-get.spec.ts +++ b/src/m365/entra/commands/enterpriseapp/enterpriseapp-get.spec.ts @@ -272,7 +272,7 @@ 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', () => { + it('fails validation if neither the id, displayName, nor objectId option specified', () => { const actual = commandOptionsSchema.safeParse({}); assert.strictEqual(actual.success, false); }); diff --git a/src/m365/entra/commands/enterpriseapp/enterpriseapp-remove.spec.ts b/src/m365/entra/commands/enterpriseapp/enterpriseapp-remove.spec.ts index 043f52c6f1c..98d46977bcd 100644 --- a/src/m365/entra/commands/enterpriseapp/enterpriseapp-remove.spec.ts +++ b/src/m365/entra/commands/enterpriseapp/enterpriseapp-remove.spec.ts @@ -128,7 +128,7 @@ 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', () => { + it('fails validation if neither the id, displayName, nor objectId option is specified', () => { const actual = commandOptionsSchema.safeParse({}); assert.strictEqual(actual.success, false); }); From 1677be848422b1516663e1c39eacc13ff62e6a3b Mon Sep 17 00:00:00 2001 From: waldekmastykarz Date: Sun, 7 Jun 2026 19:08:26 +0200 Subject: [PATCH 3/3] Address PR review comments for Zod migration - Add scopes transform to split comma-separated values into arrays - Update command code to iterate scopes directly (no manual split) - Remove non-null assertion from getSchemaToParse() calls - Use commandOptionsSchema.parse() for action test options - Migrate enterpriseapp-list.spec.ts to Zod pattern - Add validation test for id + objectId combination in enterpriseapp-get Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../approleassignment-add.spec.ts | 26 +++++++-------- .../approleassignment-add.ts | 4 +-- .../approleassignment-list.spec.ts | 16 +++++----- .../approleassignment-remove.spec.ts | 32 +++++++++---------- .../approleassignment-remove.ts | 4 +-- .../enterpriseapp/enterpriseapp-get.spec.ts | 17 ++++++---- .../enterpriseapp/enterpriseapp-list.spec.ts | 16 +++++++--- .../enterpriseapp-remove.spec.ts | 16 +++++----- 8 files changed, 71 insertions(+), 60 deletions(-) diff --git a/src/m365/entra/commands/approleassignment/approleassignment-add.spec.ts b/src/m365/entra/commands/approleassignment/approleassignment-add.spec.ts index ea9c2bbf512..5cfbb0e8e83 100644 --- a/src/m365/entra/commands/approleassignment/approleassignment-add.spec.ts +++ b/src/m365/entra/commands/approleassignment/approleassignment-add.spec.ts @@ -49,7 +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; + commandOptionsSchema = commandInfo.command.getSchemaToParse() as typeof options; }); beforeEach(() => { @@ -94,7 +94,7 @@ describe(commands.APPROLEASSIGNMENT_ADD, () => { getRequestStub(); postRequestStub(); - await command.action(logger, { options: { appDisplayName: 'myapp', resource: 'SharePoint', scopes: 'Sites.Read.All' } }); + await command.action(logger, { options: commandOptionsSchema.parse({ appDisplayName: 'myapp', resource: 'SharePoint', scopes: 'Sites.Read.All' }) }); assert.strictEqual(loggerLogSpy.lastCall.args[0][0].objectId, 'nI5EJPrQ0UOh3eJ5cglpoLL3KmM12wZPom8Zw6AEypw'); assert.strictEqual(loggerLogSpy.lastCall.args[0][0].principalDisplayName, 'myapp'); assert.strictEqual(loggerLogSpy.lastCall.args[0][0].resourceDisplayName, 'Office 365 SharePoint Online'); @@ -104,7 +104,7 @@ describe(commands.APPROLEASSIGNMENT_ADD, () => { getRequestStub(); postRequestStub(); - await command.action(logger, { options: { appObjectId: '24448e9c-d0fa-43d1-a1dd-e279720969a0', resource: 'SharePoint', scopes: 'Sites.Read.All,Sites.ReadWrite.All' } }); + await command.action(logger, { options: commandOptionsSchema.parse({ appObjectId: '24448e9c-d0fa-43d1-a1dd-e279720969a0', resource: 'SharePoint', scopes: 'Sites.Read.All,Sites.ReadWrite.All' }) }); assert.strictEqual(loggerLogSpy.lastCall.args[0][0].objectId, 'nI5EJPrQ0UOh3eJ5cglpoLL3KmM12wZPom8Zw6AEypw'); assert.strictEqual(loggerLogSpy.lastCall.args[0][0].principalDisplayName, 'myapp'); assert.strictEqual(loggerLogSpy.lastCall.args[0][0].resourceDisplayName, 'Office 365 SharePoint Online'); @@ -117,7 +117,7 @@ describe(commands.APPROLEASSIGNMENT_ADD, () => { getRequestStub(); postRequestStub(); - await command.action(logger, { options: { appDisplayName: 'myapp', resource: 'SharePoint', scopes: 'Sites.Read.All', output: 'json' } }); + await command.action(logger, { options: commandOptionsSchema.parse({ appDisplayName: 'myapp', resource: 'SharePoint', scopes: 'Sites.Read.All', output: 'json' }) }); assert.strictEqual(loggerLogSpy.lastCall.args[0][0].id, 'nI5EJPrQ0UOh3eJ5cglpoLL3KmM12wZPom8Zw6AEypw'); assert.strictEqual(loggerLogSpy.lastCall.args[0][0].principalDisplayName, 'myapp'); assert.strictEqual(loggerLogSpy.lastCall.args[0][0].resourceDisplayName, 'Office 365 SharePoint Online'); @@ -130,28 +130,28 @@ describe(commands.APPROLEASSIGNMENT_ADD, () => { getRequestStub(); postRequestStub(); - await command.action(logger, { options: { debug: true, appId: '26e49d05-4227-4ace-ae52-9b8f08f37184', resource: 'SharePoint', scopes: 'Sites.Read.All' } }); + await command.action(logger, { options: commandOptionsSchema.parse({ debug: true, appId: '26e49d05-4227-4ace-ae52-9b8f08f37184', resource: 'SharePoint', scopes: 'Sites.Read.All' }) }); }); it('handles intune alias for the resource option value', async () => { getRequestStub(); postRequestStub(); - await command.action(logger, { options: { debug: true, appId: '26e49d05-4227-4ace-ae52-9b8f08f37184', resource: 'intune', scopes: 'Sites.Read.All' } }); + await command.action(logger, { options: commandOptionsSchema.parse({ debug: true, appId: '26e49d05-4227-4ace-ae52-9b8f08f37184', resource: 'intune', scopes: 'Sites.Read.All' }) }); }); it('handles exchange alias for the resource option value', async () => { getRequestStub(); postRequestStub(); - await command.action(logger, { options: { debug: true, appId: '26e49d05-4227-4ace-ae52-9b8f08f37184', resource: 'exchange', scopes: 'Sites.Read.All' } }); + await command.action(logger, { options: commandOptionsSchema.parse({ debug: true, appId: '26e49d05-4227-4ace-ae52-9b8f08f37184', resource: 'exchange', scopes: 'Sites.Read.All' }) }); }); it('handles appId for the resource option value', async () => { getRequestStub(); postRequestStub(); - await command.action(logger, { options: { debug: true, appId: '26e49d05-4227-4ace-ae52-9b8f08f37184', resource: 'fff194f1-7dce-4428-8301-1badb5518201', scopes: 'Sites.Read.All' } }); + await command.action(logger, { options: commandOptionsSchema.parse({ debug: true, appId: '26e49d05-4227-4ace-ae52-9b8f08f37184', resource: 'fff194f1-7dce-4428-8301-1badb5518201', scopes: 'Sites.Read.All' }) }); }); it('rejects if app roles are not found for the specified resource option value', async () => { @@ -168,7 +168,7 @@ describe(commands.APPROLEASSIGNMENT_ADD, () => { throw 'Invalid request'; }); - await assert.rejects(command.action(logger, { options: { debug: true, appId: '26e49d05-4227-4ace-ae52-9b8f08f37184', resource: 'SharePoint', scopes: 'Sites.Read.All' } } as any), + await assert.rejects(command.action(logger, { options: commandOptionsSchema.parse({ debug: true, appId: '26e49d05-4227-4ace-ae52-9b8f08f37184', resource: 'SharePoint', scopes: 'Sites.Read.All' }) }), new CommandError(`The resource 'SharePoint' does not have any application permissions available.`)); }); @@ -186,7 +186,7 @@ describe(commands.APPROLEASSIGNMENT_ADD, () => { throw 'Invalid request'; }); - await assert.rejects(command.action(logger, { options: { debug: true, appId: '26e49d05-4227-4ace-ae52-9b8f08f37184', resource: 'SharePoint', scopes: 'Sites.Read.All' } } as any), + await assert.rejects(command.action(logger, { options: commandOptionsSchema.parse({ debug: true, appId: '26e49d05-4227-4ace-ae52-9b8f08f37184', resource: 'SharePoint', scopes: 'Sites.Read.All' }) }), new CommandError(`The scope value 'Sites.Read.All' you have specified does not exist for SharePoint. ${os.EOL}Available scopes (application permissions) are: ${os.EOL}Scope1${os.EOL}Scope2`)); }); @@ -204,7 +204,7 @@ describe(commands.APPROLEASSIGNMENT_ADD, () => { throw 'Invalid request'; }); - await assert.rejects(command.action(logger, { options: { debug: true, appId: '26e49d05-4227-4ace-ae52-9b8f08f37184', resource: 'SharePoint', scopes: 'Sites.Read.All' } } as any), + await assert.rejects(command.action(logger, { options: commandOptionsSchema.parse({ debug: true, appId: '26e49d05-4227-4ace-ae52-9b8f08f37184', resource: 'SharePoint', scopes: 'Sites.Read.All' }) }), new CommandError("The specified service principal doesn't exist")); }); @@ -230,7 +230,7 @@ describe(commands.APPROLEASSIGNMENT_ADD, () => { throw 'Invalid request'; }); - await assert.rejects(command.action(logger, { options: { debug: true, appId: '26e49d05-4227-4ace-ae52-9b8f08f37184', resource: 'SharePoint', scopes: 'Sites.Read.All' } } as any), + await assert.rejects(command.action(logger, { options: commandOptionsSchema.parse({ debug: true, appId: '26e49d05-4227-4ace-ae52-9b8f08f37184', resource: 'SharePoint', scopes: 'Sites.Read.All' }) }), new CommandError("Multiple service principal found. Found: 24448e9c-d0fa-43d1-a1dd-e279720969a0.")); }); @@ -250,7 +250,7 @@ describe(commands.APPROLEASSIGNMENT_ADD, () => { sinon.stub(cli, 'handleMultipleResultsFound').resolves({ id: '24448e9c-d0fa-43d1-a1dd-e279720969a0' }); - await command.action(logger, { options: { debug: true, appDisplayName: 'test', resource: 'SharePoint', scopes: 'Scope1' } }); + await command.action(logger, { options: commandOptionsSchema.parse({ debug: true, appDisplayName: 'test', resource: 'SharePoint', scopes: 'Scope1' }) }); assert.deepEqual(loggerLogSpy.lastCall.args[0][0], { objectId: 'nI5EJPrQ0UOh3eJ5cglpoLL3KmM12wZPom8Zw6AEypw', principalDisplayName: 'myapp', diff --git a/src/m365/entra/commands/approleassignment/approleassignment-add.ts b/src/m365/entra/commands/approleassignment/approleassignment-add.ts index d2140e6b80f..b77314d889a 100644 --- a/src/m365/entra/commands/approleassignment/approleassignment-add.ts +++ b/src/m365/entra/commands/approleassignment/approleassignment-add.ts @@ -22,7 +22,7 @@ export const options = z.strictObject({ appObjectId: z.uuid().optional(), appDisplayName: z.string().optional(), resource: z.string().alias('r'), - scopes: z.string().alias('s') + scopes: z.string().transform((value) => value.split(',').map(String)).alias('s') }); declare type Options = z.infer; @@ -144,7 +144,7 @@ class EntraAppRoleAssignmentAddCommand extends GraphCommand { } // search for match between the found app roles and the specified scopes option value - for (const scope of args.options.scopes.split(',')) { + for (const scope of args.options.scopes) { const existingRoles = appRolesFound.filter((role: AppRole) => { return role.value.toLocaleLowerCase() === scope.toLocaleLowerCase().trim(); }); diff --git a/src/m365/entra/commands/approleassignment/approleassignment-list.spec.ts b/src/m365/entra/commands/approleassignment/approleassignment-list.spec.ts index 81227021834..2f845532a02 100644 --- a/src/m365/entra/commands/approleassignment/approleassignment-list.spec.ts +++ b/src/m365/entra/commands/approleassignment/approleassignment-list.spec.ts @@ -443,7 +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; + commandOptionsSchema = commandInfo.command.getSchemaToParse() as typeof options; }); beforeEach(() => { @@ -489,28 +489,28 @@ describe(commands.APPROLEASSIGNMENT_LIST, () => { it('retrieves App Role assignments for the specified appDisplayName', async () => { sinon.stub(request, 'get').callsFake(RequestStub.retrieveAppRoles); - await command.action(logger, { options: { output: 'json', appDisplayName: CommandActionParameters.appNameWithRoleAssignments } }); + await command.action(logger, { options: commandOptionsSchema.parse({ output: 'json', appDisplayName: CommandActionParameters.appNameWithRoleAssignments }) }); assert(loggerLogSpy.calledWith(jsonOutput)); }); it('retrieves App Role assignments for the specified appId', async () => { sinon.stub(request, 'get').callsFake(RequestStub.retrieveAppRoles); - await command.action(logger, { options: { output: 'json', appId: CommandActionParameters.appIdWithRoleAssignments } }); + await command.action(logger, { options: commandOptionsSchema.parse({ output: 'json', appId: CommandActionParameters.appIdWithRoleAssignments }) }); assert(loggerLogSpy.calledWith(jsonOutput)); }); it('retrieves App Role assignments for the specified appId and outputs text', async () => { sinon.stub(request, 'get').callsFake(RequestStub.retrieveAppRoles); - await command.action(logger, { options: { output: 'text', appId: CommandActionParameters.appIdWithRoleAssignments } }); + await command.action(logger, { options: commandOptionsSchema.parse({ output: 'text', appId: CommandActionParameters.appIdWithRoleAssignments }) }); assert(loggerLogSpy.calledWith(jsonOutput)); }); it('retrieves App Role assignments for the specified appObjectId and outputs text', async () => { sinon.stub(request, 'get').callsFake(RequestStub.retrieveAppRoles); - await command.action(logger, { options: { output: 'text', appObjectId: CommandActionParameters.objectIdWithRoleAssignments } }); + await command.action(logger, { options: commandOptionsSchema.parse({ output: 'text', appObjectId: CommandActionParameters.objectIdWithRoleAssignments }) }); assert(loggerLogSpy.calledWith(jsonOutput)); }); @@ -523,13 +523,13 @@ describe(commands.APPROLEASSIGNMENT_LIST, () => { it('correctly handles a service principal that does not have any app role assignments', async () => { sinon.stub(request, 'get').callsFake(RequestStub.retrieveAppRoles); - await assert.rejects(command.action(logger, { options: { appObjectId: CommandActionParameters.objectIdNoRoleAssignments } } as any), new CommandError('no app role assignments found')); + await assert.rejects(command.action(logger, { options: commandOptionsSchema.parse({ appObjectId: CommandActionParameters.objectIdNoRoleAssignments }) }), new CommandError('no app role assignments found')); }); it('correctly handles no app role assignments for the specified app', async () => { sinon.stub(request, 'get').callsFake(RequestStub.retrieveAppRoles); - await assert.rejects(command.action(logger, { options: { appId: CommandActionParameters.appIdWithNoRoleAssignments } } as any), new CommandError('app registration not found')); + await assert.rejects(command.action(logger, { options: commandOptionsSchema.parse({ appId: CommandActionParameters.appIdWithNoRoleAssignments }) }), new CommandError('app registration not found')); }); it('correctly handles API OData error', async () => { @@ -544,7 +544,7 @@ 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`)); + await assert.rejects(command.action(logger, { options: commandOptionsSchema.parse({ appObjectId: '021d971f-779d-439b-8006-9f084423f344' }) }), 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 specified', () => { diff --git a/src/m365/entra/commands/approleassignment/approleassignment-remove.spec.ts b/src/m365/entra/commands/approleassignment/approleassignment-remove.spec.ts index 88642feab0d..9da76bc1737 100644 --- a/src/m365/entra/commands/approleassignment/approleassignment-remove.spec.ts +++ b/src/m365/entra/commands/approleassignment/approleassignment-remove.spec.ts @@ -29,7 +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; + commandOptionsSchema = commandInfo.command.getSchemaToParse() as typeof options; }); beforeEach(() => { @@ -93,13 +93,13 @@ describe(commands.APPROLEASSIGNMENT_REMOVE, () => { }); it('prompts before removing the app role assignment when force option not passed', async () => { - await command.action(logger, { options: { appId: 'dc311e81-e099-4c64-bd66-c7183465f3f2', resource: 'SharePoint', scopes: 'Sites.Read.All' } }); + await command.action(logger, { options: commandOptionsSchema.parse({ appId: 'dc311e81-e099-4c64-bd66-c7183465f3f2', resource: 'SharePoint', scopes: 'Sites.Read.All' }) }); assert(promptIssued); }); it('prompts before removing the app role assignment when force option not passed (debug)', async () => { - await command.action(logger, { options: { debug: true, appId: 'dc311e81-e099-4c64-bd66-c7183465f3f2', resource: 'SharePoint', scopes: 'Sites.Read.All' } }); + await command.action(logger, { options: commandOptionsSchema.parse({ debug: true, appId: 'dc311e81-e099-4c64-bd66-c7183465f3f2', resource: 'SharePoint', scopes: 'Sites.Read.All' }) }); assert(promptIssued); }); @@ -108,7 +108,7 @@ describe(commands.APPROLEASSIGNMENT_REMOVE, () => { sinonUtil.restore(cli.promptForConfirmation); sinon.stub(cli, 'promptForConfirmation').resolves(false); - await command.action(logger, { options: { appDisplayName: 'myapp', resource: 'SharePoint', scopes: 'Sites.Read.All' } }); + await command.action(logger, { options: commandOptionsSchema.parse({ appDisplayName: 'myapp', resource: 'SharePoint', scopes: 'Sites.Read.All' }) }); assert(deleteRequestStub.notCalled); }); @@ -116,37 +116,37 @@ describe(commands.APPROLEASSIGNMENT_REMOVE, () => { sinonUtil.restore(cli.promptForConfirmation); sinon.stub(cli, 'promptForConfirmation').resolves(true); - await command.action(logger, { options: { debug: true, appDisplayName: 'myapp', resource: 'SharePoint', scopes: 'Sites.Read.All' } }); + await command.action(logger, { options: commandOptionsSchema.parse({ debug: true, appDisplayName: 'myapp', resource: 'SharePoint', scopes: 'Sites.Read.All' }) }); assert(deleteRequestStub.called); }); it('deletes app role assignments for service principal with specified displayName', async () => { - await command.action(logger, { options: { appDisplayName: 'myapp', resource: 'SharePoint', scopes: 'Sites.Read.All', force: true } }); + await command.action(logger, { options: commandOptionsSchema.parse({ appDisplayName: 'myapp', resource: 'SharePoint', scopes: 'Sites.Read.All', force: true }) }); assert(deleteRequestStub.called); }); it('deletes app role assignments for service principal with specified objectId and multiple scopes', async () => { - await command.action(logger, { options: { appObjectId: '3e64c22f-3f14-4bce-a267-cb44c9a08e17', resource: 'SharePoint', scopes: 'Sites.Read.All,Sites.FullControl.All', force: true } }); + await command.action(logger, { options: commandOptionsSchema.parse({ appObjectId: '3e64c22f-3f14-4bce-a267-cb44c9a08e17', resource: 'SharePoint', scopes: 'Sites.Read.All,Sites.FullControl.All', force: true }) }); assert(deleteRequestStub.calledTwice); }); it('deletes app role assignments for service principal with specified appId (debug)', async () => { - await command.action(logger, { options: { debug: true, appId: 'dc311e81-e099-4c64-bd66-c7183465f3f2', resource: 'SharePoint', scopes: 'Sites.Read.All', force: true } }); + await command.action(logger, { options: commandOptionsSchema.parse({ debug: true, appId: 'dc311e81-e099-4c64-bd66-c7183465f3f2', resource: 'SharePoint', scopes: 'Sites.Read.All', force: true }) }); assert(deleteRequestStub.called); }); it('handles intune alias for the resource option value', async () => { - await command.action(logger, { options: { debug: true, appId: 'dc311e81-e099-4c64-bd66-c7183465f3f2', resource: 'intune', scopes: 'Sites.Read.All', force: true } }); + await command.action(logger, { options: commandOptionsSchema.parse({ debug: true, appId: 'dc311e81-e099-4c64-bd66-c7183465f3f2', resource: 'intune', scopes: 'Sites.Read.All', force: true }) }); assert(deleteRequestStub.called); }); it('handles exchange alias for the resource option value', async () => { - await command.action(logger, { options: { debug: true, appId: 'dc311e81-e099-4c64-bd66-c7183465f3f2', resource: 'exchange', scopes: 'Sites.Read.All', force: true } }); + await command.action(logger, { options: commandOptionsSchema.parse({ debug: true, appId: 'dc311e81-e099-4c64-bd66-c7183465f3f2', resource: 'exchange', scopes: 'Sites.Read.All', force: true }) }); assert(deleteRequestStub.called); }); it('handles appId for the resource option value', async () => { - await command.action(logger, { options: { debug: true, appId: 'dc311e81-e099-4c64-bd66-c7183465f3f2', resource: 'fff194f1-7dce-4428-8301-1badb5518201', scopes: 'Sites.Read.All', force: true } }); + await command.action(logger, { options: commandOptionsSchema.parse({ debug: true, appId: 'dc311e81-e099-4c64-bd66-c7183465f3f2', resource: 'fff194f1-7dce-4428-8301-1badb5518201', scopes: 'Sites.Read.All', force: true }) }); assert(deleteRequestStub.called); }); @@ -164,7 +164,7 @@ describe(commands.APPROLEASSIGNMENT_REMOVE, () => { throw 'Invalid request'; }); - await assert.rejects(command.action(logger, { options: { debug: true, appId: '3e64c22f-3f14-4bce-a267-cb44c9a08e17', resource: 'SharePoint', scopes: 'Sites.Read.All', force: true } } as any), + await assert.rejects(command.action(logger, { options: commandOptionsSchema.parse({ debug: true, appId: '3e64c22f-3f14-4bce-a267-cb44c9a08e17', resource: 'SharePoint', scopes: 'Sites.Read.All', force: true }) }), new CommandError(`Resource not found`)); }); @@ -182,7 +182,7 @@ describe(commands.APPROLEASSIGNMENT_REMOVE, () => { throw 'Invalid request'; }); - await assert.rejects(command.action(logger, { options: { debug: true, appId: '3e64c22f-3f14-4bce-a267-cb44c9a08e17', resource: 'SharePoint', scopes: 'Sites.Read.All', force: true } } as any), + await assert.rejects(command.action(logger, { options: commandOptionsSchema.parse({ debug: true, appId: '3e64c22f-3f14-4bce-a267-cb44c9a08e17', resource: 'SharePoint', scopes: 'Sites.Read.All', force: true }) }), new CommandError(`The resource 'SharePoint' does not have any application permissions available.`)); }); @@ -200,7 +200,7 @@ describe(commands.APPROLEASSIGNMENT_REMOVE, () => { throw 'Invalid request'; }); - await assert.rejects(command.action(logger, { options: { debug: true, appId: '3e64c22f-3f14-4bce-a267-cb44c9a08e17', resource: 'SharePoint', scopes: 'Sites.Read.All', force: true } } as any), + await assert.rejects(command.action(logger, { options: commandOptionsSchema.parse({ debug: true, appId: '3e64c22f-3f14-4bce-a267-cb44c9a08e17', resource: 'SharePoint', scopes: 'Sites.Read.All', force: true }) }), new CommandError(`The scope value 'Sites.Read.All' you have specified does not exist for SharePoint. ${os.EOL}Available scopes (application permissions) are: ${os.EOL}Scope1${os.EOL}Scope2`)); }); @@ -218,12 +218,12 @@ describe(commands.APPROLEASSIGNMENT_REMOVE, () => { throw 'Invalid request'; }); - await assert.rejects(command.action(logger, { options: { debug: true, appId: '3e64c22f-3f14-4bce-a267-cb44c9a08e17', resource: 'SharePoint', scopes: 'Sites.Read.All', force: true } } as any), + await assert.rejects(command.action(logger, { options: commandOptionsSchema.parse({ debug: true, appId: '3e64c22f-3f14-4bce-a267-cb44c9a08e17', resource: 'SharePoint', scopes: 'Sites.Read.All', force: true }) }), new CommandError("app registration not found")); }); it('rejects if app role assignment is not found', async () => { - await assert.rejects(command.action(logger, { options: { debug: true, appId: '3e64c22f-3f14-4bce-a267-cb44c9a08e17', resource: 'SharePoint', scopes: 'Sites.ReadWrite.All', force: true } } as any), + await assert.rejects(command.action(logger, { options: commandOptionsSchema.parse({ debug: true, appId: '3e64c22f-3f14-4bce-a267-cb44c9a08e17', resource: 'SharePoint', scopes: 'Sites.ReadWrite.All', force: true }) }), new CommandError("App role assignment not found")); }); diff --git a/src/m365/entra/commands/approleassignment/approleassignment-remove.ts b/src/m365/entra/commands/approleassignment/approleassignment-remove.ts index d0e4ab86e2a..67eb5350a59 100644 --- a/src/m365/entra/commands/approleassignment/approleassignment-remove.ts +++ b/src/m365/entra/commands/approleassignment/approleassignment-remove.ts @@ -16,7 +16,7 @@ export const options = z.strictObject({ appObjectId: z.uuid().optional(), appDisplayName: z.string().optional(), resource: z.string().alias('r'), - scopes: z.string().alias('s'), + scopes: z.string().transform((value) => value.split(',').map(String)).alias('s'), force: z.boolean().optional().alias('f') }); @@ -115,7 +115,7 @@ class EntraAppRoleAssignmentRemoveCommand extends GraphCommand { throw `The resource '${args.options.resource}' does not have any application permissions available.`; } - for (const scope of args.options.scopes.split(',')) { + for (const scope of args.options.scopes) { const existingRoles = appRolesFound.filter((role: AppRole) => { return role.value!.toLocaleLowerCase() === scope.toLocaleLowerCase().trim(); }); diff --git a/src/m365/entra/commands/enterpriseapp/enterpriseapp-get.spec.ts b/src/m365/entra/commands/enterpriseapp/enterpriseapp-get.spec.ts index 2b9dc315f7b..7bbcbfcc1c1 100644 --- a/src/m365/entra/commands/enterpriseapp/enterpriseapp-get.spec.ts +++ b/src/m365/entra/commands/enterpriseapp/enterpriseapp-get.spec.ts @@ -44,7 +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; + commandOptionsSchema = commandInfo.command.getSchemaToParse() as typeof options; }); beforeEach(() => { @@ -102,7 +102,7 @@ describe(commands.ENTERPRISEAPP_GET, () => { throw 'Invalid request'; }); - await command.action(logger, { options: { debug: true, displayName: 'foo' } }); + await command.action(logger, { options: commandOptionsSchema.parse({ debug: true, displayName: 'foo' }) }); assert(loggerLogSpy.calledWith(spAppInfo)); }); @@ -119,7 +119,7 @@ describe(commands.ENTERPRISEAPP_GET, () => { throw 'Invalid request'; }); - await command.action(logger, { options: { id: '65415bb1-9267-4313-bbf5-ae259732ee12' } }); + await command.action(logger, { options: commandOptionsSchema.parse({ id: '65415bb1-9267-4313-bbf5-ae259732ee12' }) }); assert(loggerLogSpy.calledWith(spAppInfo)); }); @@ -136,7 +136,7 @@ describe(commands.ENTERPRISEAPP_GET, () => { throw 'Invalid request'; }); - await command.action(logger, { options: { objectId: '59e617e5-e447-4adc-8b88-00af644d7c92' } }); + await command.action(logger, { options: commandOptionsSchema.parse({ objectId: '59e617e5-e447-4adc-8b88-00af644d7c92' }) }); assert(loggerLogSpy.calledWith(spAppInfo)); }); @@ -152,7 +152,7 @@ describe(commands.ENTERPRISEAPP_GET, () => { } }); - await assert.rejects(command.action(logger, { options: { id: 'b2307a39-e878-458b-bc90-03bc578531d6' } } as any), + await assert.rejects(command.action(logger, { options: commandOptionsSchema.parse({ id: 'b2307a39-e878-458b-bc90-03bc578531d6' }) }), new CommandError('An error has occurred')); }); @@ -248,7 +248,7 @@ describe(commands.ENTERPRISEAPP_GET, () => { sinon.stub(cli, 'handleMultipleResultsFound').resolves(spAppInfo.value[0]); - await command.action(logger, { options: { debug: true, displayName: 'foo' } }); + await command.action(logger, { options: commandOptionsSchema.parse({ debug: true, displayName: 'foo' }) }); assert(loggerLogSpy.calledWith(spAppInfo)); }); @@ -311,4 +311,9 @@ describe(commands.ENTERPRISEAPP_GET, () => { const actual = commandOptionsSchema.safeParse({ displayName: 'abc', objectId: '6a7b1395-d313-4682-8ed4-65a6265a6320' }); assert.strictEqual(actual.success, false); }); + + it('fails validation if both id and objectId are specified', () => { + const actual = commandOptionsSchema.safeParse({ id: '6a7b1395-d313-4682-8ed4-65a6265a6320', objectId: '6a7b1395-d313-4682-8ed4-65a6265a6320' }); + assert.strictEqual(actual.success, false); + }); }); diff --git a/src/m365/entra/commands/enterpriseapp/enterpriseapp-list.spec.ts b/src/m365/entra/commands/enterpriseapp/enterpriseapp-list.spec.ts index ae54ca405ec..59d90cf88bc 100644 --- a/src/m365/entra/commands/enterpriseapp/enterpriseapp-list.spec.ts +++ b/src/m365/entra/commands/enterpriseapp/enterpriseapp-list.spec.ts @@ -1,6 +1,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 { Logger } from '../../../../cli/Logger.js'; import request from '../../../../request.js'; import { telemetry } from '../../../../telemetry.js'; @@ -8,12 +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 './enterpriseapp-list.js'; +import command, { options } from './enterpriseapp-list.js'; describe(commands.ENTERPRISEAPP_LIST, () => { let log: string[]; let logger: Logger; let loggerLogSpy: sinon.SinonSpy; + let commandInfo: CommandInfo; + let commandOptionsSchema: typeof options; const displayName = "My custom enterprise application"; const tag = "WindowsAzureActiveDirectoryIntegratedApp"; @@ -97,6 +101,8 @@ describe(commands.ENTERPRISEAPP_LIST, () => { sinon.stub(pid, 'getProcessName').callsFake(() => ''); sinon.stub(session, 'getId').callsFake(() => ''); auth.connection.active = true; + commandInfo = cli.getCommandInfo(command); + commandOptionsSchema = commandInfo.command.getSchemaToParse() as typeof options; }); beforeEach(() => { @@ -152,7 +158,7 @@ describe(commands.ENTERPRISEAPP_LIST, () => { return 'Invalid request'; }); - await command.action(logger, { options: { verbose: true } }); + await command.action(logger, { options: commandOptionsSchema.parse({ verbose: true }) }); assert(loggerLogSpy.calledWith(servicePrincipalResponse.value)); }); @@ -165,7 +171,7 @@ describe(commands.ENTERPRISEAPP_LIST, () => { return 'Invalid request'; }); - await command.action(logger, { options: { displayName: displayName } }); + await command.action(logger, { options: commandOptionsSchema.parse({ displayName: displayName }) }); assert(loggerLogSpy.calledWith(servicePrincipalResponse.value)); }); @@ -178,7 +184,7 @@ describe(commands.ENTERPRISEAPP_LIST, () => { return 'Invalid request'; }); - await command.action(logger, { options: { tag: tag } }); + await command.action(logger, { options: commandOptionsSchema.parse({ tag: tag }) }); assert(loggerLogSpy.calledWith(servicePrincipalResponse.value)); }); @@ -195,6 +201,6 @@ describe(commands.ENTERPRISEAPP_LIST, () => { throw error; }); - await assert.rejects(command.action(logger, { options: {} } as any), error['odata.error'].message.value); + await assert.rejects(command.action(logger, { options: commandOptionsSchema.parse({}) }), error['odata.error'].message.value); }); }); diff --git a/src/m365/entra/commands/enterpriseapp/enterpriseapp-remove.spec.ts b/src/m365/entra/commands/enterpriseapp/enterpriseapp-remove.spec.ts index 98d46977bcd..c43b368ca75 100644 --- a/src/m365/entra/commands/enterpriseapp/enterpriseapp-remove.spec.ts +++ b/src/m365/entra/commands/enterpriseapp/enterpriseapp-remove.spec.ts @@ -56,7 +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; + commandOptionsSchema = commandInfo.command.getSchemaToParse() as typeof options; sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => settingName === settingsNames.prompt ? false : defaultValue); }); @@ -169,14 +169,14 @@ describe(commands.ENTERPRISEAPP_REMOVE, () => { }); it('prompts before removing the enterprise application when force option not passed', async () => { - await command.action(logger, { options: { id: '65415bb1-9267-4313-bbf5-ae259732ee12' } }); + await command.action(logger, { options: commandOptionsSchema.parse({ id: '65415bb1-9267-4313-bbf5-ae259732ee12' }) }); assert(promptIssued); }); it('aborts removing the enterprise application when prompt not confirmed', async () => { const deleteCallsSpy = sinon.stub(request, 'delete').resolves(); - await command.action(logger, { options: { id: '65415bb1-9267-4313-bbf5-ae259732ee12' } }); + await command.action(logger, { options: commandOptionsSchema.parse({ id: '65415bb1-9267-4313-bbf5-ae259732ee12' }) }); assert(deleteCallsSpy.notCalled); }); @@ -190,13 +190,13 @@ describe(commands.ENTERPRISEAPP_REMOVE, () => { }); const deleteCallsSpy: sinon.SinonStub = deleteRequestStub(); - await command.action(logger, { options: { verbose: true, displayName: 'foo', force: true } }); + await command.action(logger, { options: commandOptionsSchema.parse({ verbose: true, displayName: 'foo', force: true }) }); assert(deleteCallsSpy.calledOnce); }); it('deletes the specified enterprise application using its id', async () => { const deleteCallsSpy: sinon.SinonStub = deleteRequestStub(); - await command.action(logger, { options: { id: '65415bb1-9267-4313-bbf5-ae259732ee12', force: true } }); + await command.action(logger, { options: commandOptionsSchema.parse({ id: '65415bb1-9267-4313-bbf5-ae259732ee12', force: true }) }); assert(deleteCallsSpy.calledOnce); }); @@ -205,7 +205,7 @@ describe(commands.ENTERPRISEAPP_REMOVE, () => { sinon.stub(cli, 'promptForConfirmation').resolves(true); const deleteCallsSpy: sinon.SinonStub = deleteRequestStub(); - await command.action(logger, { options: { objectId: '59e617e5-e447-4adc-8b88-00af644d7c92', verbose: true } }); + await command.action(logger, { options: commandOptionsSchema.parse({ objectId: '59e617e5-e447-4adc-8b88-00af644d7c92', verbose: true }) }); assert(deleteCallsSpy.calledOnce); }); @@ -221,7 +221,7 @@ describe(commands.ENTERPRISEAPP_REMOVE, () => { } }); - await assert.rejects(command.action(logger, { options: { displayName: 'foo', force: true } } as any), + await assert.rejects(command.action(logger, { options: commandOptionsSchema.parse({ displayName: 'foo', force: true }) }), new CommandError('An error has occurred')); }); @@ -304,7 +304,7 @@ describe(commands.ENTERPRISEAPP_REMOVE, () => { sinon.stub(cli, 'handleMultipleResultsFound').resolves(spAppInfo.value[0]); const deleteCallsSpy: sinon.SinonStub = deleteRequestStub(); - await command.action(logger, { options: { verbose: true, displayName: 'foo', force: true } }); + await command.action(logger, { options: commandOptionsSchema.parse({ verbose: true, displayName: 'foo', force: true }) }); assert(deleteCallsSpy.calledOnce); }); });