diff --git a/src/m365/entra/commands/administrativeunit/administrativeunit-member-add.spec.ts b/src/m365/entra/commands/administrativeunit/administrativeunit-member-add.spec.ts index 0ab793149a6..26336aebb22 100644 --- a/src/m365/entra/commands/administrativeunit/administrativeunit-member-add.spec.ts +++ b/src/m365/entra/commands/administrativeunit/administrativeunit-member-add.spec.ts @@ -11,12 +11,11 @@ import { session } from '../../../../utils/session.js'; import { sinonUtil } from '../../../../utils/sinonUtil.js'; import { cli } from '../../../../cli/cli.js'; import { CommandInfo } from '../../../../cli/CommandInfo.js'; -import command from './administrativeunit-member-add.js'; +import command, { options } from './administrativeunit-member-add.js'; import { entraAdministrativeUnit } from '../../../../utils/entraAdministrativeUnit.js'; import { entraGroup } from '../../../../utils/entraGroup.js'; import { entraUser } from '../../../../utils/entraUser.js'; import { entraDevice } from '../../../../utils/entraDevice.js'; -import { settingsNames } from '../../../../settingsNames.js'; describe(commands.ADMINISTRATIVEUNIT_MEMBER_ADD, () => { const administrativeUnitId = 'fc33aa61-cf0e-46b6-9506-f633347202ab'; @@ -31,6 +30,7 @@ describe(commands.ADMINISTRATIVEUNIT_MEMBER_ADD, () => { let log: string[]; let logger: Logger; let commandInfo: CommandInfo; + let commandOptionsSchema: typeof options; before(() => { sinon.stub(auth, 'restoreAuth').resolves(); @@ -39,6 +39,7 @@ describe(commands.ADMINISTRATIVEUNIT_MEMBER_ADD, () => { sinon.stub(session, 'getId').returns(''); auth.connection.active = true; commandInfo = cli.getCommandInfo(command); + commandOptionsSchema = commandInfo.command.getSchemaToParse() as typeof options; }); beforeEach(() => { @@ -63,7 +64,6 @@ describe(commands.ADMINISTRATIVEUNIT_MEMBER_ADD, () => { entraUser.getUserIdByUpn, entraGroup.getGroupIdByDisplayName, entraDevice.getDeviceByDisplayName, - cli.getSettingWithDefaultValue, cli.handleMultipleResultsFound, cli.promptForSelection ]); @@ -82,270 +82,134 @@ describe(commands.ADMINISTRATIVEUNIT_MEMBER_ADD, () => { assert.notStrictEqual(command.description, null); }); - it('passes validation when administrativeUnitId is a valid GUID', async () => { - const actual = await command.validate({ options: { administrativeUnitId: administrativeUnitId, userId: '00000000-0000-0000-0000-000000000000' } }, commandInfo); - assert.strictEqual(actual, true); + it('passes validation when administrativeUnitId is a valid GUID', () => { + const actual = commandOptionsSchema.safeParse({ administrativeUnitId: administrativeUnitId, userId: '00000000-0000-0000-0000-000000000000' }); + assert.strictEqual(actual.success, true); }); - it('fails validation if administrativeUnitId is not a valid GUID', async () => { - const actual = await command.validate({ options: { administrativeUnitId: 'invalid', userId: '00000000-0000-0000-0000-000000000000' } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation if administrativeUnitId is not a valid GUID', () => { + const actual = commandOptionsSchema.safeParse({ administrativeUnitId: 'invalid', userId: '00000000-0000-0000-0000-000000000000' }); + assert.strictEqual(actual.success, false); }); - it('passes validation when userId is a valid GUID', async () => { - const actual = await command.validate({ options: { administrativeUnitId: '00000000-0000-0000-0000-000000000000', userId: userId } }, commandInfo); - assert.strictEqual(actual, true); + it('passes validation when userId is a valid GUID', () => { + const actual = commandOptionsSchema.safeParse({ administrativeUnitId: '00000000-0000-0000-0000-000000000000', userId: userId }); + assert.strictEqual(actual.success, true); }); - it('fails validation if userId is not a valid GUID', async () => { - const actual = await command.validate({ options: { administrativeUnitId: '00000000-0000-0000-0000-000000000000', userId: 'invalid' } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation if userId is not a valid GUID', () => { + const actual = commandOptionsSchema.safeParse({ administrativeUnitId: '00000000-0000-0000-0000-000000000000', userId: 'invalid' }); + assert.strictEqual(actual.success, false); }); - it('passes validation when groupId is a valid GUID', async () => { - const actual = await command.validate({ options: { administrativeUnitId: '00000000-0000-0000-0000-000000000000', groupId: groupId } }, commandInfo); - assert.strictEqual(actual, true); + it('passes validation when groupId is a valid GUID', () => { + const actual = commandOptionsSchema.safeParse({ administrativeUnitId: '00000000-0000-0000-0000-000000000000', groupId: groupId }); + assert.strictEqual(actual.success, true); }); - it('fails validation if groupId is not a valid GUID', async () => { - const actual = await command.validate({ options: { administrativeUnitId: '00000000-0000-0000-0000-000000000000', groupId: 'invalid' } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation if groupId is not a valid GUID', () => { + const actual = commandOptionsSchema.safeParse({ administrativeUnitId: '00000000-0000-0000-0000-000000000000', groupId: 'invalid' }); + assert.strictEqual(actual.success, false); }); - it('passes validation when deviceId is a valid GUID', async () => { - const actual = await command.validate({ options: { administrativeUnitId: '00000000-0000-0000-0000-000000000000', deviceId: deviceId } }, commandInfo); - assert.strictEqual(actual, true); + it('passes validation when deviceId is a valid GUID', () => { + const actual = commandOptionsSchema.safeParse({ administrativeUnitId: '00000000-0000-0000-0000-000000000000', deviceId: deviceId }); + assert.strictEqual(actual.success, true); }); - it('fails validation if deviceId is not a valid GUID', async () => { - const actual = await command.validate({ options: { administrativeUnitId: '00000000-0000-0000-0000-000000000000', deviceId: 'invalid' } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation if deviceId is not a valid GUID', () => { + const actual = commandOptionsSchema.safeParse({ administrativeUnitId: '00000000-0000-0000-0000-000000000000', deviceId: 'invalid' }); + assert.strictEqual(actual.success, false); }); - it('fails validation when both administrativeUnitId and administrativeUnitName options are passed', async () => { - sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => { - if (settingName === settingsNames.prompt) { - return false; - } - - return defaultValue; - }); - - const actual = await command.validate({ options: { administrativeUnitId: administrativeUnitId, administrativeUnitName: administrativeUnitName, userId: '00000000-0000-0000-0000-000000000000' } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation when both administrativeUnitId and administrativeUnitName options are passed', () => { + const actual = commandOptionsSchema.safeParse({ administrativeUnitId: administrativeUnitId, administrativeUnitName: administrativeUnitName, userId: '00000000-0000-0000-0000-000000000000' }); + assert.strictEqual(actual.success, false); }); - it('fails validation if both administrativeUnitId and administrativeUnitName options are not passed', async () => { - sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => { - if (settingName === settingsNames.prompt) { - return false; - } - - return defaultValue; - }); - - const actual = await command.validate({ options: { userId: userId } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation if both administrativeUnitId and administrativeUnitName options are not passed', () => { + const actual = commandOptionsSchema.safeParse({ userId: userId }); + assert.strictEqual(actual.success, false); }); - it('fails validation when both userId and userName options are passed', async () => { - sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => { - if (settingName === settingsNames.prompt) { - return false; - } - - return defaultValue; - }); - - const actual = await command.validate({ options: { administrativeUnitId: '00000000-0000-0000-0000-000000000000', userId: userId, userName: userName } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation when both userId and userName options are passed', () => { + const actual = commandOptionsSchema.safeParse({ administrativeUnitId: '00000000-0000-0000-0000-000000000000', userId: userId, userName: userName }); + assert.strictEqual(actual.success, false); }); - it('fails validation when both userId and groupId options are passed', async () => { - sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => { - if (settingName === settingsNames.prompt) { - return false; - } - - return defaultValue; - }); - - const actual = await command.validate({ options: { administrativeUnitId: '00000000-0000-0000-0000-000000000000', userId: userId, groupId: groupId } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation when both userId and groupId options are passed', () => { + const actual = commandOptionsSchema.safeParse({ administrativeUnitId: '00000000-0000-0000-0000-000000000000', userId: userId, groupId: groupId }); + assert.strictEqual(actual.success, false); }); - it('fails validation when both userId and groupName options are passed', async () => { - sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => { - if (settingName === settingsNames.prompt) { - return false; - } - - return defaultValue; - }); - - const actual = await command.validate({ options: { administrativeUnitId: '00000000-0000-0000-0000-000000000000', userId: userId, groupName: groupName } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation when both userId and groupName options are passed', () => { + const actual = commandOptionsSchema.safeParse({ administrativeUnitId: '00000000-0000-0000-0000-000000000000', userId: userId, groupName: groupName }); + assert.strictEqual(actual.success, false); }); - it('fails validation when both userId and deviceId options are passed', async () => { - sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => { - if (settingName === settingsNames.prompt) { - return false; - } - - return defaultValue; - }); - - const actual = await command.validate({ options: { administrativeUnitId: '00000000-0000-0000-0000-000000000000', userId: userId, deviceId: deviceId } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation when both userId and deviceId options are passed', () => { + const actual = commandOptionsSchema.safeParse({ administrativeUnitId: '00000000-0000-0000-0000-000000000000', userId: userId, deviceId: deviceId }); + assert.strictEqual(actual.success, false); }); - it('fails validation when both userId and deviceName options are passed', async () => { - sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => { - if (settingName === settingsNames.prompt) { - return false; - } - - return defaultValue; - }); - - const actual = await command.validate({ options: { administrativeUnitId: '00000000-0000-0000-0000-000000000000', userId: userId, deviceName: deviceName } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation when both userId and deviceName options are passed', () => { + const actual = commandOptionsSchema.safeParse({ administrativeUnitId: '00000000-0000-0000-0000-000000000000', userId: userId, deviceName: deviceName }); + assert.strictEqual(actual.success, false); }); - it('fails validation when both userName and groupId options are passed', async () => { - sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => { - if (settingName === settingsNames.prompt) { - return false; - } - - return defaultValue; - }); - - const actual = await command.validate({ options: { administrativeUnitId: '00000000-0000-0000-0000-000000000000', userName: userName, groupId: groupId } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation when both userName and groupId options are passed', () => { + const actual = commandOptionsSchema.safeParse({ administrativeUnitId: '00000000-0000-0000-0000-000000000000', userName: userName, groupId: groupId }); + assert.strictEqual(actual.success, false); }); - it('fails validation when both userName and groupName options are passed', async () => { - sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => { - if (settingName === settingsNames.prompt) { - return false; - } - - return defaultValue; - }); - - const actual = await command.validate({ options: { administrativeUnitId: '00000000-0000-0000-0000-000000000000', userName: userName, groupName: groupName } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation when both userName and groupName options are passed', () => { + const actual = commandOptionsSchema.safeParse({ administrativeUnitId: '00000000-0000-0000-0000-000000000000', userName: userName, groupName: groupName }); + assert.strictEqual(actual.success, false); }); - it('fails validation when both userName and deviceId options are passed', async () => { - sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => { - if (settingName === settingsNames.prompt) { - return false; - } - - return defaultValue; - }); - - const actual = await command.validate({ options: { administrativeUnitId: '00000000-0000-0000-0000-000000000000', userName: userName, deviceId: deviceId } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation when both userName and deviceId options are passed', () => { + const actual = commandOptionsSchema.safeParse({ administrativeUnitId: '00000000-0000-0000-0000-000000000000', userName: userName, deviceId: deviceId }); + assert.strictEqual(actual.success, false); }); - it('fails validation when both userName and deviceName options are passed', async () => { - sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => { - if (settingName === settingsNames.prompt) { - return false; - } - - return defaultValue; - }); - - const actual = await command.validate({ options: { administrativeUnitId: '00000000-0000-0000-0000-000000000000', userName: userName, deviceName: deviceName } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation when both userName and deviceName options are passed', () => { + const actual = commandOptionsSchema.safeParse({ administrativeUnitId: '00000000-0000-0000-0000-000000000000', userName: userName, deviceName: deviceName }); + assert.strictEqual(actual.success, false); }); - it('fails validation when both groupId and groupName options are passed', async () => { - sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => { - if (settingName === settingsNames.prompt) { - return false; - } - - return defaultValue; - }); - - const actual = await command.validate({ options: { administrativeUnitId: '00000000-0000-0000-0000-000000000000', groupId: groupId, groupName: groupName } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation when both groupId and groupName options are passed', () => { + const actual = commandOptionsSchema.safeParse({ administrativeUnitId: '00000000-0000-0000-0000-000000000000', groupId: groupId, groupName: groupName }); + assert.strictEqual(actual.success, false); }); - it('fails validation when both groupId and deviceId options are passed', async () => { - sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => { - if (settingName === settingsNames.prompt) { - return false; - } - - return defaultValue; - }); - - const actual = await command.validate({ options: { administrativeUnitId: '00000000-0000-0000-0000-000000000000', groupId: groupId, deviceId: deviceId } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation when both groupId and deviceId options are passed', () => { + const actual = commandOptionsSchema.safeParse({ administrativeUnitId: '00000000-0000-0000-0000-000000000000', groupId: groupId, deviceId: deviceId }); + assert.strictEqual(actual.success, false); }); - it('fails validation when both groupId and deviceName options are passed', async () => { - sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => { - if (settingName === settingsNames.prompt) { - return false; - } - - return defaultValue; - }); - - const actual = await command.validate({ options: { administrativeUnitId: '00000000-0000-0000-0000-000000000000', groupId: groupId, deviceName: deviceName } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation when both groupId and deviceName options are passed', () => { + const actual = commandOptionsSchema.safeParse({ administrativeUnitId: '00000000-0000-0000-0000-000000000000', groupId: groupId, deviceName: deviceName }); + assert.strictEqual(actual.success, false); }); - it('fails validation when both groupName and deviceId options are passed', async () => { - sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => { - if (settingName === settingsNames.prompt) { - return false; - } - - return defaultValue; - }); - - const actual = await command.validate({ options: { administrativeUnitId: '00000000-0000-0000-0000-000000000000', groupName: groupName, deviceId: deviceId } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation when both groupName and deviceId options are passed', () => { + const actual = commandOptionsSchema.safeParse({ administrativeUnitId: '00000000-0000-0000-0000-000000000000', groupName: groupName, deviceId: deviceId }); + assert.strictEqual(actual.success, false); }); - it('fails validation when both groupName and deviceName options are passed', async () => { - sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => { - if (settingName === settingsNames.prompt) { - return false; - } - - return defaultValue; - }); - - const actual = await command.validate({ options: { administrativeUnitId: '00000000-0000-0000-0000-000000000000', groupName: groupName, deviceName: deviceName } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation when both groupName and deviceName options are passed', () => { + const actual = commandOptionsSchema.safeParse({ administrativeUnitId: '00000000-0000-0000-0000-000000000000', groupName: groupName, deviceName: deviceName }); + assert.strictEqual(actual.success, false); }); - it('fails validation when both deviceId and deviceName options are passed', async () => { - sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => { - if (settingName === settingsNames.prompt) { - return false; - } - - return defaultValue; - }); - - const actual = await command.validate({ options: { administrativeUnitId: '00000000-0000-0000-0000-000000000000', deviceId: deviceId, deviceName: deviceName } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation when both deviceId and deviceName options are passed', () => { + const actual = commandOptionsSchema.safeParse({ administrativeUnitId: '00000000-0000-0000-0000-000000000000', deviceId: deviceId, deviceName: deviceName }); + assert.strictEqual(actual.success, false); }); - it('passes validation if all the required options are specified', async () => { - const actual = await command.validate({ options: { administrativeUnitId: administrativeUnitId, userId: userId } }, commandInfo); - assert.strictEqual(actual, true); + it('passes validation if all the required options are specified', () => { + const actual = commandOptionsSchema.safeParse({ administrativeUnitId: administrativeUnitId, userId: userId }); + assert.strictEqual(actual.success, true); }); it('adds a user specified by its id to an administrative unit specified by its id', async () => { diff --git a/src/m365/entra/commands/administrativeunit/administrativeunit-member-add.ts b/src/m365/entra/commands/administrativeunit/administrativeunit-member-add.ts index b9a0bd8d964..01858f42cb3 100644 --- a/src/m365/entra/commands/administrativeunit/administrativeunit-member-add.ts +++ b/src/m365/entra/commands/administrativeunit/administrativeunit-member-add.ts @@ -1,29 +1,32 @@ -import GlobalOptions from "../../../../GlobalOptions.js"; +import { z } from 'zod'; +import { globalOptionsZod } from '../../../../Command.js'; import { Logger } from "../../../../cli/Logger.js"; import { entraAdministrativeUnit } from "../../../../utils/entraAdministrativeUnit.js"; import { entraGroup } from "../../../../utils/entraGroup.js"; import { entraUser } from "../../../../utils/entraUser.js"; -import { validation } from "../../../../utils/validation.js"; import GraphCommand from "../../../base/GraphCommand.js"; import commands from "../../commands.js"; import request, { CliRequestOptions } from "../../../../request.js"; import { entraDevice } from "../../../../utils/entraDevice.js"; +export const options = z.strictObject({ + ...globalOptionsZod.shape, + administrativeUnitId: z.uuid().optional().alias('i'), + administrativeUnitName: z.string().optional().alias('n'), + userId: z.uuid().optional(), + userName: z.string().optional(), + groupId: z.uuid().optional(), + groupName: z.string().optional(), + deviceId: z.uuid().optional(), + deviceName: z.string().optional() +}); + +declare type Options = z.infer; + interface CommandArgs { options: Options; } -interface Options extends GlobalOptions { - administrativeUnitId?: string; - administrativeUnitName?: string; - userId?: string; - userName?: string; - groupId?: string; - groupName?: string; - deviceId?: string; - deviceName?: string; -} - class EntraAdministrativeUnitMemberAddCommand extends GraphCommand { public get name(): string { return commands.ADMINISTRATIVEUNIT_MEMBER_ADD; @@ -33,84 +36,26 @@ class EntraAdministrativeUnitMemberAddCommand extends GraphCommand { return 'Adds a member (user, group, device) to an administrative unit'; } - constructor() { - super(); - - this.#initTelemetry(); - this.#initOptions(); - this.#initValidators(); - this.#initOptionSets(); - } - - #initTelemetry(): void { - this.telemetry.push((args: CommandArgs) => { - Object.assign(this.telemetryProperties, { - userId: typeof args.options.userId !== 'undefined', - userName: typeof args.options.userName !== 'undefined', - groupId: typeof args.options.groupId !== 'undefined', - groupName: typeof args.options.groupName !== 'undefined', - deviceId: typeof args.options.deviceId !== 'undefined', - deviceName: typeof args.options.deviceName !== 'undefined' - }); - }); - } - - #initOptions(): void { - this.options.unshift( - { - option: '-i, --administrativeUnitId [administrativeUnitId]' - }, - { - option: '-n, --administrativeUnitName [administrativeUnitName]' - }, - { - option: "--userId [userId]" - }, - { - option: "--userName [userName]" - }, - { - option: "--groupId [groupId]" - }, - { - option: "--groupName [groupName]" - }, - { - option: "--deviceId [deviceId]" - }, - { - option: "--deviceName [deviceName]" - } - ); + public get schema(): z.ZodType | undefined { + return options; } - #initValidators(): void { - this.validators.push( - async (args: CommandArgs) => { - if (args.options.administrativeUnitId && !validation.isValidGuid(args.options.administrativeUnitId as string)) { - return `${args.options.administrativeUnitId} is not a valid GUID`; + public getRefinedSchema(schema: typeof options): z.ZodObject | undefined { + return schema + .refine(options => [options.administrativeUnitId, options.administrativeUnitName].filter(Boolean).length === 1, { + error: 'Specify either administrativeUnitId or administrativeUnitName', + params: { + customCode: 'optionSet', + options: ['administrativeUnitId', 'administrativeUnitName'] } - - if (args.options.userId && !validation.isValidGuid(args.options.userId as string)) { - return `${args.options.userId} is not a valid GUID`; - } - - if (args.options.groupId && !validation.isValidGuid(args.options.groupId as string)) { - return `${args.options.groupId} is not a valid GUID`; - } - - if (args.options.deviceId && !validation.isValidGuid(args.options.deviceId as string)) { - return `${args.options.deviceId} is not a valid GUID`; + }) + .refine(options => [options.userId, options.userName, options.groupId, options.groupName, options.deviceId, options.deviceName].filter(Boolean).length === 1, { + error: 'Specify either userId, userName, groupId, groupName, deviceId, or deviceName', + params: { + customCode: 'optionSet', + options: ['userId', 'userName', 'groupId', 'groupName', 'deviceId', 'deviceName'] } - - return true; - } - ); - } - - #initOptionSets(): void { - this.optionSets.push({ options: ['administrativeUnitId', 'administrativeUnitName'] }); - this.optionSets.push({ options: ['userId', 'userName', 'groupId', 'groupName', 'deviceId', 'deviceName'] }); + }); } public async commandAction(logger: Logger, args: CommandArgs): Promise { diff --git a/src/m365/entra/commands/administrativeunit/administrativeunit-member-get.spec.ts b/src/m365/entra/commands/administrativeunit/administrativeunit-member-get.spec.ts index 2d31db91528..4d919e2be12 100644 --- a/src/m365/entra/commands/administrativeunit/administrativeunit-member-get.spec.ts +++ b/src/m365/entra/commands/administrativeunit/administrativeunit-member-get.spec.ts @@ -8,12 +8,11 @@ import { telemetry } from '../../../../telemetry.js'; import { pid } from '../../../../utils/pid.js'; import { session } from '../../../../utils/session.js'; import { cli } from '../../../../cli/cli.js'; -import command from './administrativeunit-member-get.js'; +import command, { options } from './administrativeunit-member-get.js'; import request from '../../../../request.js'; import { sinonUtil } from '../../../../utils/sinonUtil.js'; import { CommandError } from '../../../../Command.js'; import { entraAdministrativeUnit } from '../../../../utils/entraAdministrativeUnit.js'; -import { settingsNames } from '../../../../settingsNames.js'; describe(commands.ADMINISTRATIVEUNIT_MEMBER_GET, () => { const administrativeUnitId = 'fc33aa61-cf0e-46b6-9506-f633347202ab'; @@ -41,6 +40,7 @@ describe(commands.ADMINISTRATIVEUNIT_MEMBER_GET, () => { let logger: Logger; let loggerLogSpy: sinon.SinonSpy; let commandInfo: CommandInfo; + let commandOptionsSchema: typeof options; before(() => { sinon.stub(auth, 'restoreAuth').resolves(); @@ -49,6 +49,7 @@ describe(commands.ADMINISTRATIVEUNIT_MEMBER_GET, () => { sinon.stub(session, 'getId').returns(''); auth.connection.active = true; commandInfo = cli.getCommandInfo(command); + commandOptionsSchema = commandInfo.command.getSchemaToParse() as typeof options; }); beforeEach(() => { @@ -70,8 +71,7 @@ describe(commands.ADMINISTRATIVEUNIT_MEMBER_GET, () => { afterEach(() => { sinonUtil.restore([ request.get, - entraAdministrativeUnit.getAdministrativeUnitByDisplayName, - cli.getSettingWithDefaultValue + entraAdministrativeUnit.getAdministrativeUnitByDisplayName ]); }); @@ -88,45 +88,29 @@ describe(commands.ADMINISTRATIVEUNIT_MEMBER_GET, () => { assert.notStrictEqual(command.description, null); }); - it('passes validation when member id and administrativeUnitId are GUIDs', async () => { - const actual = await command.validate({ options: { id: userId, administrativeUnitId: administrativeUnitId } }, commandInfo); - assert.strictEqual(actual, true); + it('passes validation when member id and administrativeUnitId are GUIDs', () => { + const actual = commandOptionsSchema.safeParse({ id: userId, administrativeUnitId: administrativeUnitId }); + assert.strictEqual(actual.success, true); }); - it('fails validation if member id is not a valid GUID', async () => { - const actual = await command.validate({ options: { id: 'invalid', administrativeUnitId: administrativeUnitId } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation if member id is not a valid GUID', () => { + const actual = commandOptionsSchema.safeParse({ id: 'invalid', administrativeUnitId: administrativeUnitId }); + assert.strictEqual(actual.success, false); }); - it('fails validation if administrativeUnitId is not a valid GUID', async () => { - const actual = await command.validate({ options: { id: userId, administrativeUnitId: 'invalid' } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation if administrativeUnitId is not a valid GUID', () => { + const actual = commandOptionsSchema.safeParse({ id: userId, administrativeUnitId: 'invalid' }); + assert.strictEqual(actual.success, false); }); - it('fails validation when both administrativeUnitId and administrativeUnitName options are passed', async () => { - sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => { - if (settingName === settingsNames.prompt) { - return false; - } - - return defaultValue; - }); - - const actual = await command.validate({ options: { id: userId, administrativeUnitId: administrativeUnitId, administrativeUnitName: administrativeUnitName } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation when both administrativeUnitId and administrativeUnitName options are passed', () => { + const actual = commandOptionsSchema.safeParse({ id: userId, administrativeUnitId: administrativeUnitId, administrativeUnitName: administrativeUnitName }); + assert.strictEqual(actual.success, false); }); - it('fails validation if both administrativeUnitId and administrativeUnitName options are not passed', async () => { - sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => { - if (settingName === settingsNames.prompt) { - return false; - } - - return defaultValue; - }); - - const actual = await command.validate({ options: { id: userId } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation if both administrativeUnitId and administrativeUnitName options are not passed', () => { + const actual = commandOptionsSchema.safeParse({ id: userId }); + assert.strictEqual(actual.success, false); }); it('get member info for an administrative unit specified by id and member specified by id', async () => { diff --git a/src/m365/entra/commands/administrativeunit/administrativeunit-member-get.ts b/src/m365/entra/commands/administrativeunit/administrativeunit-member-get.ts index 8e03b756c1c..2d652092e8e 100644 --- a/src/m365/entra/commands/administrativeunit/administrativeunit-member-get.ts +++ b/src/m365/entra/commands/administrativeunit/administrativeunit-member-get.ts @@ -1,23 +1,26 @@ import { DirectoryObject } from '@microsoft/microsoft-graph-types'; -import GlobalOptions from '../../../../GlobalOptions.js'; +import { z } from 'zod'; +import { globalOptionsZod } from '../../../../Command.js'; import GraphCommand from '../../../base/GraphCommand.js'; import commands from '../../commands.js'; import { Logger } from '../../../../cli/Logger.js'; -import { validation } from '../../../../utils/validation.js'; import { entraAdministrativeUnit } from '../../../../utils/entraAdministrativeUnit.js'; import request, { CliRequestOptions } from '../../../../request.js'; +export const options = z.strictObject({ + ...globalOptionsZod.shape, + id: z.uuid().alias('i'), + administrativeUnitId: z.uuid().optional().alias('u'), + administrativeUnitName: z.string().optional().alias('n'), + properties: z.string().optional().alias('p') +}); + +declare type Options = z.infer; + interface CommandArgs { options: Options; } -export interface Options extends GlobalOptions { - id: string; - administrativeUnitId?: string; - administrativeUnitName?: string; - properties?: string; -} - interface DirectoryObjectEx extends DirectoryObject { '@odata.context'?: string; '@odata.type'?: string; @@ -33,60 +36,19 @@ class EntraAdministrativeUnitMemberGetCommand extends GraphCommand { return 'Retrieve a specific member (user, group, or device) of an administrative unit'; } - constructor() { - super(); - - this.#initTelemetry(); - this.#initOptions(); - this.#initValidators(); - this.#initOptionSets(); - } - - #initTelemetry(): void { - this.telemetry.push((args: CommandArgs) => { - Object.assign(this.telemetryProperties, { - administrativeUnitId: typeof args.options.administrativeUnitId !== 'undefined', - administrativeUnitName: typeof args.options.administrativeUnitName !== 'undefined', - properties: typeof args.options.properties !== 'undefined' - }); - }); - } - - #initOptions(): void { - this.options.unshift( - { - option: '-i, --id ' - }, - { - option: '-u, --administrativeUnitId [administrativeUnitId]' - }, - { - option: '-n, --administrativeUnitName [administrativeUnitName]' - }, - { - option: '-p, --properties [properties]' - } - ); + 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`; + public getRefinedSchema(schema: typeof options): z.ZodObject | undefined { + return schema + .refine(options => [options.administrativeUnitId, options.administrativeUnitName].filter(Boolean).length === 1, { + error: 'Specify either administrativeUnitId or administrativeUnitName', + params: { + customCode: 'optionSet', + options: ['administrativeUnitId', 'administrativeUnitName'] } - - if (args.options.administrativeUnitId && !validation.isValidGuid(args.options.administrativeUnitId)) { - return `${args.options.administrativeUnitId} is not a valid GUID`; - } - - return true; - } - ); - } - - #initOptionSets(): void { - this.optionSets.push({ options: ['administrativeUnitId', 'administrativeUnitName'] }); + }); } public async commandAction(logger: Logger, args: CommandArgs): Promise { diff --git a/src/m365/entra/commands/administrativeunit/administrativeunit-member-list.spec.ts b/src/m365/entra/commands/administrativeunit/administrativeunit-member-list.spec.ts index a1dfcc83d1c..67fce897723 100644 --- a/src/m365/entra/commands/administrativeunit/administrativeunit-member-list.spec.ts +++ b/src/m365/entra/commands/administrativeunit/administrativeunit-member-list.spec.ts @@ -9,8 +9,7 @@ import { session } from '../../../../utils/session.js'; import { Logger } from '../../../../cli/Logger.js'; import { CommandError } from '../../../../Command.js'; import { sinonUtil } from '../../../../utils/sinonUtil.js'; -import command from './administrativeunit-member-list.js'; -import { settingsNames } from '../../../../settingsNames.js'; +import command, { options } from './administrativeunit-member-list.js'; import { CommandInfo } from '../../../../cli/CommandInfo.js'; import { cli } from '../../../../cli/cli.js'; import { entraAdministrativeUnit } from '../../../../utils/entraAdministrativeUnit.js'; @@ -278,6 +277,7 @@ describe(commands.ADMINISTRATIVEUNIT_MEMBER_LIST, () => { let logger: Logger; let loggerLogSpy: sinon.SinonSpy; let commandInfo: CommandInfo; + let commandOptionsSchema: typeof options; before(() => { sinon.stub(auth, 'restoreAuth').resolves(); @@ -286,6 +286,7 @@ describe(commands.ADMINISTRATIVEUNIT_MEMBER_LIST, () => { sinon.stub(session, 'getId').returns(''); auth.connection.active = true; commandInfo = cli.getCommandInfo(command); + commandOptionsSchema = commandInfo.command.getSchemaToParse() as typeof options; }); beforeEach(() => { @@ -307,8 +308,7 @@ describe(commands.ADMINISTRATIVEUNIT_MEMBER_LIST, () => { afterEach(() => { sinonUtil.restore([ request.get, - entraAdministrativeUnit.getAdministrativeUnitByDisplayName, - cli.getSettingWithDefaultValue + entraAdministrativeUnit.getAdministrativeUnitByDisplayName ]); }); @@ -325,65 +325,49 @@ describe(commands.ADMINISTRATIVEUNIT_MEMBER_LIST, () => { assert.notStrictEqual(command.description, null); }); - it('passes validation when administrativeUnitId is a valid GUID', async () => { - const actual = await command.validate({ options: { administrativeUnitId: administrativeUnitId } }, commandInfo); - assert.strictEqual(actual, true); + it('passes validation when administrativeUnitId is a valid GUID', () => { + const actual = commandOptionsSchema.safeParse({ administrativeUnitId: administrativeUnitId }); + assert.strictEqual(actual.success, true); }); - it('fails validation if administrativeUnitId is not a valid GUID', async () => { - const actual = await command.validate({ options: { administrativeUnitId: 'invalid' } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation if administrativeUnitId is not a valid GUID', () => { + const actual = commandOptionsSchema.safeParse({ administrativeUnitId: 'invalid' }); + assert.strictEqual(actual.success, false); }); - it('fails validation when both administrativeUnitId and administrativeUnitName options are passed', async () => { - sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => { - if (settingName === settingsNames.prompt) { - return false; - } - - return defaultValue; - }); - - const actual = await command.validate({ options: { administrativeUnitId: administrativeUnitId, administrativeUnitName: administrativeUnitName } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation when both administrativeUnitId and administrativeUnitName options are passed', () => { + const actual = commandOptionsSchema.safeParse({ administrativeUnitId: administrativeUnitId, administrativeUnitName: administrativeUnitName }); + assert.strictEqual(actual.success, false); }); - it('fails validation if both administrativeUnitId and administrativeUnitName options are not passed', 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 both administrativeUnitId and administrativeUnitName options are not passed', () => { + const actual = commandOptionsSchema.safeParse({}); + assert.strictEqual(actual.success, false); }); - it('fails validation if wrong type is specified', async () => { - const actual = await command.validate({ options: { administrativeUnitId: administrativeUnitId, type: 'application' } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation if wrong type is specified', () => { + const actual = commandOptionsSchema.safeParse({ administrativeUnitId: administrativeUnitId, type: 'application' }); + assert.strictEqual(actual.success, false); }); - it('passes validation if user type is specified', async () => { - const actual = await command.validate({ options: { administrativeUnitId: administrativeUnitId, type: 'user' } }, commandInfo); - assert.strictEqual(actual, true); + it('passes validation if user type is specified', () => { + const actual = commandOptionsSchema.safeParse({ administrativeUnitId: administrativeUnitId, type: 'user' }); + assert.strictEqual(actual.success, true); }); - it('passes validation if group type is specified', async () => { - const actual = await command.validate({ options: { administrativeUnitId: administrativeUnitId, type: 'group' } }, commandInfo); - assert.strictEqual(actual, true); + it('passes validation if group type is specified', () => { + const actual = commandOptionsSchema.safeParse({ administrativeUnitId: administrativeUnitId, type: 'group' }); + assert.strictEqual(actual.success, true); }); - it('passes validation if device type is specified', async () => { - const actual = await command.validate({ options: { administrativeUnitId: administrativeUnitId, type: 'device' } }, commandInfo); - assert.strictEqual(actual, true); + it('passes validation if device type is specified', () => { + const actual = commandOptionsSchema.safeParse({ administrativeUnitId: administrativeUnitId, type: 'device' }); + assert.strictEqual(actual.success, true); }); - it('fails validation if filter is specified but type is missing', async () => { - const actual = await command.validate({ options: { administrativeUnitId: administrativeUnitId, filter: "userType eq 'Memmber'" } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation if filter is specified but type is missing', () => { + const actual = commandOptionsSchema.safeParse({ administrativeUnitId: administrativeUnitId, filter: "userType eq 'Memmber'" }); + assert.strictEqual(actual.success, false); }); it('should get all members of an administrative unit specified by its id when type not specified', async () => { diff --git a/src/m365/entra/commands/administrativeunit/administrativeunit-member-list.ts b/src/m365/entra/commands/administrativeunit/administrativeunit-member-list.ts index 0055e560e47..67f618559aa 100644 --- a/src/m365/entra/commands/administrativeunit/administrativeunit-member-list.ts +++ b/src/m365/entra/commands/administrativeunit/administrativeunit-member-list.ts @@ -1,23 +1,33 @@ import { DirectoryObject } from '@microsoft/microsoft-graph-types'; +import { z } from 'zod'; +import { globalOptionsZod } from '../../../../Command.js'; import { Logger } from '../../../../cli/Logger.js'; import { odata } from '../../../../utils/odata.js'; import GraphCommand from '../../../base/GraphCommand.js'; import commands from '../../commands.js'; -import GlobalOptions from '../../../../GlobalOptions.js'; import { entraAdministrativeUnit } from '../../../../utils/entraAdministrativeUnit.js'; -import { validation } from '../../../../utils/validation.js'; import { CliRequestOptions } from '../../../../request.js'; +import { zod } from '../../../../utils/zod.js'; -interface CommandArgs { - options: Options; +enum MemberType { + User = 'user', + Group = 'group', + Device = 'device' } -interface Options extends GlobalOptions { - administrativeUnitId?: string; - administrativeUnitName?: string; - type?: string; - properties?: string; - filter?: string; +export const options = z.strictObject({ + ...globalOptionsZod.shape, + administrativeUnitId: z.uuid().optional().alias('i'), + administrativeUnitName: z.string().optional().alias('n'), + type: zod.coercedEnum(MemberType).optional().alias('t'), + properties: z.string().optional().alias('p'), + filter: z.string().optional().alias('f') +}); + +declare type Options = z.infer; + +interface CommandArgs { + options: Options; } interface DirectoryObjectEx extends DirectoryObject { @@ -38,70 +48,22 @@ class EntraAdministrativeUnitMemberListCommand extends GraphCommand { return ['id', 'displayName']; } - constructor() { - super(); - - this.#initTelemetry(); - this.#initOptions(); - this.#initValidators(); - this.#initOptionSets(); - } - - #initTelemetry(): void { - this.telemetry.push((args: CommandArgs) => { - Object.assign(this.telemetryProperties, { - type: typeof args.options.type !== 'undefined', - properties: typeof args.options.properties !== 'undefined', - filter: typeof args.options.filter !== 'undefined' - }); - }); + public get schema(): z.ZodType | undefined { + return options; } - #initOptions(): void { - this.options.unshift( - { - option: '-i, --administrativeUnitId [administrativeUnitId]' - }, - { - option: '-n, --administrativeUnitName [administrativeUnitName]' - }, - { - option: '-t, --type [type]', - autocomplete: ['user', 'group', 'device'] - }, - { - option: '-p, --properties [properties]' - }, - { - option: '-f, --filter [filter]' - } - ); - } - - #initValidators(): void { - this.validators.push( - async (args: CommandArgs) => { - if (args.options.administrativeUnitId && !validation.isValidGuid(args.options.administrativeUnitId as string)) { - return `${args.options.administrativeUnitId} is not a valid GUID`; - } - - if (args.options.type) { - if (['user', 'group', 'device'].every(type => type !== args.options.type)) { - return `${args.options.type} is not a valid type value. Allowed values user|group|device`; - } - } - - if (args.options.filter && !args.options.type) { - return 'Filter can be specified only if type is set'; + public getRefinedSchema(schema: typeof options): z.ZodObject | undefined { + return schema + .refine(options => [options.administrativeUnitId, options.administrativeUnitName].filter(Boolean).length === 1, { + error: 'Specify either administrativeUnitId or administrativeUnitName', + params: { + customCode: 'optionSet', + options: ['administrativeUnitId', 'administrativeUnitName'] } - - return true; - } - ); - } - - #initOptionSets(): void { - this.optionSets.push({ options: ['administrativeUnitId', 'administrativeUnitName'] }); + }) + .refine(options => !options.filter || options.type, { + error: 'Filter can be specified only if type is set' + }); } public async commandAction(logger: Logger, args: CommandArgs): Promise { diff --git a/src/m365/entra/commands/administrativeunit/administrativeunit-member-remove.spec.ts b/src/m365/entra/commands/administrativeunit/administrativeunit-member-remove.spec.ts index 7c6a2b3da3b..bc0d640eb29 100644 --- a/src/m365/entra/commands/administrativeunit/administrativeunit-member-remove.spec.ts +++ b/src/m365/entra/commands/administrativeunit/administrativeunit-member-remove.spec.ts @@ -11,12 +11,11 @@ import { session } from '../../../../utils/session.js'; import { sinonUtil } from '../../../../utils/sinonUtil.js'; import { cli } from '../../../../cli/cli.js'; import { CommandInfo } from '../../../../cli/CommandInfo.js'; -import command from './administrativeunit-member-remove.js'; +import command, { options } from './administrativeunit-member-remove.js'; import { entraAdministrativeUnit } from '../../../../utils/entraAdministrativeUnit.js'; import { entraGroup } from '../../../../utils/entraGroup.js'; import { entraUser } from '../../../../utils/entraUser.js'; import { entraDevice } from '../../../../utils/entraDevice.js'; -import { settingsNames } from '../../../../settingsNames.js'; describe(commands.ADMINISTRATIVEUNIT_MEMBER_REMOVE, () => { const administrativeUnitId = 'fc33aa61-cf0e-46b6-9506-f633347202ab'; @@ -31,6 +30,7 @@ describe(commands.ADMINISTRATIVEUNIT_MEMBER_REMOVE, () => { let log: string[]; let logger: Logger; let commandInfo: CommandInfo; + let commandOptionsSchema: typeof options; let promptIssued: boolean; before(() => { @@ -40,6 +40,7 @@ describe(commands.ADMINISTRATIVEUNIT_MEMBER_REMOVE, () => { sinon.stub(session, 'getId').returns(''); auth.connection.active = true; commandInfo = cli.getCommandInfo(command); + commandOptionsSchema = commandInfo.command.getSchemaToParse() as typeof options; }); beforeEach(() => { @@ -70,7 +71,6 @@ describe(commands.ADMINISTRATIVEUNIT_MEMBER_REMOVE, () => { entraUser.getUserIdByUpn, entraGroup.getGroupIdByDisplayName, entraDevice.getDeviceByDisplayName, - cli.getSettingWithDefaultValue, cli.handleMultipleResultsFound, cli.promptForConfirmation ]); @@ -89,366 +89,174 @@ describe(commands.ADMINISTRATIVEUNIT_MEMBER_REMOVE, () => { assert.notStrictEqual(command.description, null); }); - it('passes validation when administrativeUnitId is a valid GUID', async () => { - const actual = await command.validate({ options: { administrativeUnitId: administrativeUnitId, id: '00000000-0000-0000-0000-000000000000' } }, commandInfo); - assert.strictEqual(actual, true); + it('passes validation when administrativeUnitId is a valid GUID', () => { + const actual = commandOptionsSchema.safeParse({ administrativeUnitId: administrativeUnitId, id: '00000000-0000-0000-0000-000000000000' }); + assert.strictEqual(actual.success, true); }); - it('fails validation if administrativeUnitId is not a valid GUID', async () => { - const actual = await command.validate({ options: { administrativeUnitId: 'invalid', id: '00000000-0000-0000-0000-000000000000' } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation if administrativeUnitId is not a valid GUID', () => { + const actual = commandOptionsSchema.safeParse({ administrativeUnitId: 'invalid', id: '00000000-0000-0000-0000-000000000000' }); + assert.strictEqual(actual.success, false); }); - it('passes validation when id is a valid GUID', async () => { - const actual = await command.validate({ options: { administrativeUnitId: '00000000-0000-0000-0000-000000000000', id: userId } }, commandInfo); - assert.strictEqual(actual, true); + it('passes validation when id is a valid GUID', () => { + const actual = commandOptionsSchema.safeParse({ administrativeUnitId: '00000000-0000-0000-0000-000000000000', id: userId }); + assert.strictEqual(actual.success, true); }); - it('fails validation if id is not a valid GUID', async () => { - const actual = await command.validate({ options: { administrativeUnitId: '00000000-0000-0000-0000-000000000000', id: 'invalid' } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation if id is not a valid GUID', () => { + const actual = commandOptionsSchema.safeParse({ administrativeUnitId: '00000000-0000-0000-0000-000000000000', id: 'invalid' }); + assert.strictEqual(actual.success, false); }); - it('passes validation when userId is a valid GUID', async () => { - const actual = await command.validate({ options: { administrativeUnitId: '00000000-0000-0000-0000-000000000000', userId: userId } }, commandInfo); - assert.strictEqual(actual, true); + it('passes validation when userId is a valid GUID', () => { + const actual = commandOptionsSchema.safeParse({ administrativeUnitId: '00000000-0000-0000-0000-000000000000', userId: userId }); + assert.strictEqual(actual.success, true); }); - it('fails validation if userId is not a valid GUID', async () => { - const actual = await command.validate({ options: { administrativeUnitId: '00000000-0000-0000-0000-000000000000', userId: 'invalid' } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation if userId is not a valid GUID', () => { + const actual = commandOptionsSchema.safeParse({ administrativeUnitId: '00000000-0000-0000-0000-000000000000', userId: 'invalid' }); + assert.strictEqual(actual.success, false); }); - it('passes validation when groupId is a valid GUID', async () => { - const actual = await command.validate({ options: { administrativeUnitId: '00000000-0000-0000-0000-000000000000', groupId: groupId } }, commandInfo); - assert.strictEqual(actual, true); + it('passes validation when groupId is a valid GUID', () => { + const actual = commandOptionsSchema.safeParse({ administrativeUnitId: '00000000-0000-0000-0000-000000000000', groupId: groupId }); + assert.strictEqual(actual.success, true); }); - it('fails validation if groupId is not a valid GUID', async () => { - const actual = await command.validate({ options: { administrativeUnitId: '00000000-0000-0000-0000-000000000000', groupId: 'invalid' } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation if groupId is not a valid GUID', () => { + const actual = commandOptionsSchema.safeParse({ administrativeUnitId: '00000000-0000-0000-0000-000000000000', groupId: 'invalid' }); + assert.strictEqual(actual.success, false); }); - it('passes validation when deviceId is a valid GUID', async () => { - const actual = await command.validate({ options: { administrativeUnitId: '00000000-0000-0000-0000-000000000000', deviceId: deviceId } }, commandInfo); - assert.strictEqual(actual, true); + it('passes validation when deviceId is a valid GUID', () => { + const actual = commandOptionsSchema.safeParse({ administrativeUnitId: '00000000-0000-0000-0000-000000000000', deviceId: deviceId }); + assert.strictEqual(actual.success, true); }); - it('fails validation if deviceId is not a valid GUID', async () => { - const actual = await command.validate({ options: { administrativeUnitId: '00000000-0000-0000-0000-000000000000', deviceId: 'invalid' } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation if deviceId is not a valid GUID', () => { + const actual = commandOptionsSchema.safeParse({ administrativeUnitId: '00000000-0000-0000-0000-000000000000', deviceId: 'invalid' }); + assert.strictEqual(actual.success, false); }); - it('fails validation when both administrativeUnitId and administrativeUnitName options are passed', async () => { - sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => { - if (settingName === settingsNames.prompt) { - return false; - } - - return defaultValue; - }); - - const actual = await command.validate({ options: { administrativeUnitId: administrativeUnitId, administrativeUnitName: administrativeUnitName, userId: '00000000-0000-0000-0000-000000000000' } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation when both administrativeUnitId and administrativeUnitName options are passed', () => { + const actual = commandOptionsSchema.safeParse({ administrativeUnitId: administrativeUnitId, administrativeUnitName: administrativeUnitName, userId: '00000000-0000-0000-0000-000000000000' }); + assert.strictEqual(actual.success, false); }); - it('fails validation if both administrativeUnitId and administrativeUnitName options are not passed', async () => { - sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => { - if (settingName === settingsNames.prompt) { - return false; - } - - return defaultValue; - }); - - const actual = await command.validate({ options: { userId: userId } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation if both administrativeUnitId and administrativeUnitName options are not passed', () => { + const actual = commandOptionsSchema.safeParse({ userId: userId }); + assert.strictEqual(actual.success, false); }); - it('fails validation if id, userId, userName, groupId, groupName, deviceId and deviceName options are not passed', async () => { - sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => { - if (settingName === settingsNames.prompt) { - return false; - } - - return defaultValue; - }); - - const actual = await command.validate({ options: { administrativeUnitId: administrativeUnitId } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation if id, userId, userName, groupId, groupName, deviceId and deviceName options are not passed', () => { + const actual = commandOptionsSchema.safeParse({ administrativeUnitId: administrativeUnitId }); + assert.strictEqual(actual.success, false); }); - it('fails validation when both id and userId options are passed', async () => { - sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => { - if (settingName === settingsNames.prompt) { - return false; - } - - return defaultValue; - }); - - const actual = await command.validate({ options: { administrativeUnitId: '00000000-0000-0000-0000-000000000000', id: userId, userId: userId } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation when both id and userId options are passed', () => { + const actual = commandOptionsSchema.safeParse({ administrativeUnitId: '00000000-0000-0000-0000-000000000000', id: userId, userId: userId }); + assert.strictEqual(actual.success, false); }); - it('fails validation when both id and userName options are passed', async () => { - sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => { - if (settingName === settingsNames.prompt) { - return false; - } - - return defaultValue; - }); - - const actual = await command.validate({ options: { administrativeUnitId: '00000000-0000-0000-0000-000000000000', id: userId, userName: userName } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation when both id and userName options are passed', () => { + const actual = commandOptionsSchema.safeParse({ administrativeUnitId: '00000000-0000-0000-0000-000000000000', id: userId, userName: userName }); + assert.strictEqual(actual.success, false); }); - it('fails validation when both id and groupId options are passed', async () => { - sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => { - if (settingName === settingsNames.prompt) { - return false; - } - - return defaultValue; - }); - - const actual = await command.validate({ options: { administrativeUnitId: '00000000-0000-0000-0000-000000000000', id: userId, groupId: groupId } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation when both id and groupId options are passed', () => { + const actual = commandOptionsSchema.safeParse({ administrativeUnitId: '00000000-0000-0000-0000-000000000000', id: userId, groupId: groupId }); + assert.strictEqual(actual.success, false); }); - it('fails validation when both id and groupName options are passed', async () => { - sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => { - if (settingName === settingsNames.prompt) { - return false; - } - - return defaultValue; - }); - - const actual = await command.validate({ options: { administrativeUnitId: '00000000-0000-0000-0000-000000000000', id: userId, groupName: groupName } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation when both id and groupName options are passed', () => { + const actual = commandOptionsSchema.safeParse({ administrativeUnitId: '00000000-0000-0000-0000-000000000000', id: userId, groupName: groupName }); + assert.strictEqual(actual.success, false); }); - it('fails validation when both id and deviceId options are passed', async () => { - sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => { - if (settingName === settingsNames.prompt) { - return false; - } - - return defaultValue; - }); - - const actual = await command.validate({ options: { administrativeUnitId: '00000000-0000-0000-0000-000000000000', id: userId, deviceId: deviceId } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation when both id and deviceId options are passed', () => { + const actual = commandOptionsSchema.safeParse({ administrativeUnitId: '00000000-0000-0000-0000-000000000000', id: userId, deviceId: deviceId }); + assert.strictEqual(actual.success, false); }); - it('fails validation when both id and deviceName options are passed', async () => { - sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => { - if (settingName === settingsNames.prompt) { - return false; - } - - return defaultValue; - }); - - const actual = await command.validate({ options: { administrativeUnitId: '00000000-0000-0000-0000-000000000000', id: userId, deviceName: deviceName } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation when both id and deviceName options are passed', () => { + const actual = commandOptionsSchema.safeParse({ administrativeUnitId: '00000000-0000-0000-0000-000000000000', id: userId, deviceName: deviceName }); + assert.strictEqual(actual.success, false); }); - it('fails validation when both userId and userName options are passed', async () => { - sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => { - if (settingName === settingsNames.prompt) { - return false; - } - - return defaultValue; - }); - - const actual = await command.validate({ options: { administrativeUnitId: '00000000-0000-0000-0000-000000000000', userId: userId, userName: userName } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation when both userId and userName options are passed', () => { + const actual = commandOptionsSchema.safeParse({ administrativeUnitId: '00000000-0000-0000-0000-000000000000', userId: userId, userName: userName }); + assert.strictEqual(actual.success, false); }); - it('fails validation when both userId and groupId options are passed', async () => { - sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => { - if (settingName === settingsNames.prompt) { - return false; - } - - return defaultValue; - }); - - const actual = await command.validate({ options: { administrativeUnitId: '00000000-0000-0000-0000-000000000000', userId: userId, groupId: groupId } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation when both userId and groupId options are passed', () => { + const actual = commandOptionsSchema.safeParse({ administrativeUnitId: '00000000-0000-0000-0000-000000000000', userId: userId, groupId: groupId }); + assert.strictEqual(actual.success, false); }); - it('fails validation when both userId and groupName options are passed', async () => { - sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => { - if (settingName === settingsNames.prompt) { - return false; - } - - return defaultValue; - }); - - const actual = await command.validate({ options: { administrativeUnitId: '00000000-0000-0000-0000-000000000000', userId: userId, groupName: groupName } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation when both userId and groupName options are passed', () => { + const actual = commandOptionsSchema.safeParse({ administrativeUnitId: '00000000-0000-0000-0000-000000000000', userId: userId, groupName: groupName }); + assert.strictEqual(actual.success, false); }); - it('fails validation when both userId and deviceId options are passed', async () => { - sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => { - if (settingName === settingsNames.prompt) { - return false; - } - - return defaultValue; - }); - - const actual = await command.validate({ options: { administrativeUnitId: '00000000-0000-0000-0000-000000000000', userId: userId, deviceId: deviceId } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation when both userId and deviceId options are passed', () => { + const actual = commandOptionsSchema.safeParse({ administrativeUnitId: '00000000-0000-0000-0000-000000000000', userId: userId, deviceId: deviceId }); + assert.strictEqual(actual.success, false); }); - it('fails validation when both userId and deviceName options are passed', async () => { - sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => { - if (settingName === settingsNames.prompt) { - return false; - } - - return defaultValue; - }); - - const actual = await command.validate({ options: { administrativeUnitId: '00000000-0000-0000-0000-000000000000', userId: userId, deviceName: deviceName } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation when both userId and deviceName options are passed', () => { + const actual = commandOptionsSchema.safeParse({ administrativeUnitId: '00000000-0000-0000-0000-000000000000', userId: userId, deviceName: deviceName }); + assert.strictEqual(actual.success, false); }); - it('fails validation when both userName and groupId options are passed', async () => { - sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => { - if (settingName === settingsNames.prompt) { - return false; - } - - return defaultValue; - }); - - const actual = await command.validate({ options: { administrativeUnitId: '00000000-0000-0000-0000-000000000000', userName: userName, groupId: groupId } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation when both userName and groupId options are passed', () => { + const actual = commandOptionsSchema.safeParse({ administrativeUnitId: '00000000-0000-0000-0000-000000000000', userName: userName, groupId: groupId }); + assert.strictEqual(actual.success, false); }); - it('fails validation when both userName and groupName options are passed', async () => { - sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => { - if (settingName === settingsNames.prompt) { - return false; - } - - return defaultValue; - }); - - const actual = await command.validate({ options: { administrativeUnitId: '00000000-0000-0000-0000-000000000000', userName: userName, groupName: groupName } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation when both userName and groupName options are passed', () => { + const actual = commandOptionsSchema.safeParse({ administrativeUnitId: '00000000-0000-0000-0000-000000000000', userName: userName, groupName: groupName }); + assert.strictEqual(actual.success, false); }); - it('fails validation when both userName and deviceId options are passed', async () => { - sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => { - if (settingName === settingsNames.prompt) { - return false; - } - - return defaultValue; - }); - - const actual = await command.validate({ options: { administrativeUnitId: '00000000-0000-0000-0000-000000000000', userName: userName, deviceId: deviceId } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation when both userName and deviceId options are passed', () => { + const actual = commandOptionsSchema.safeParse({ administrativeUnitId: '00000000-0000-0000-0000-000000000000', userName: userName, deviceId: deviceId }); + assert.strictEqual(actual.success, false); }); - it('fails validation when both userName and deviceName options are passed', async () => { - sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => { - if (settingName === settingsNames.prompt) { - return false; - } - - return defaultValue; - }); - - const actual = await command.validate({ options: { administrativeUnitId: '00000000-0000-0000-0000-000000000000', userName: userName, deviceName: deviceName } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation when both userName and deviceName options are passed', () => { + const actual = commandOptionsSchema.safeParse({ administrativeUnitId: '00000000-0000-0000-0000-000000000000', userName: userName, deviceName: deviceName }); + assert.strictEqual(actual.success, false); }); - it('fails validation when both groupId and groupName options are passed', async () => { - sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => { - if (settingName === settingsNames.prompt) { - return false; - } - - return defaultValue; - }); - - const actual = await command.validate({ options: { administrativeUnitId: '00000000-0000-0000-0000-000000000000', groupId: groupId, groupName: groupName } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation when both groupId and groupName options are passed', () => { + const actual = commandOptionsSchema.safeParse({ administrativeUnitId: '00000000-0000-0000-0000-000000000000', groupId: groupId, groupName: groupName }); + assert.strictEqual(actual.success, false); }); - it('fails validation when both groupId and deviceId options are passed', async () => { - sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => { - if (settingName === settingsNames.prompt) { - return false; - } - - return defaultValue; - }); - - const actual = await command.validate({ options: { administrativeUnitId: '00000000-0000-0000-0000-000000000000', groupId: groupId, deviceId: deviceId } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation when both groupId and deviceId options are passed', () => { + const actual = commandOptionsSchema.safeParse({ administrativeUnitId: '00000000-0000-0000-0000-000000000000', groupId: groupId, deviceId: deviceId }); + assert.strictEqual(actual.success, false); }); - it('fails validation when both groupId and deviceName options are passed', async () => { - sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => { - if (settingName === settingsNames.prompt) { - return false; - } - - return defaultValue; - }); - - const actual = await command.validate({ options: { administrativeUnitId: '00000000-0000-0000-0000-000000000000', groupId: groupId, deviceName: deviceName } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation when both groupId and deviceName options are passed', () => { + const actual = commandOptionsSchema.safeParse({ administrativeUnitId: '00000000-0000-0000-0000-000000000000', groupId: groupId, deviceName: deviceName }); + assert.strictEqual(actual.success, false); }); - it('fails validation when both groupName and deviceId options are passed', async () => { - sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => { - if (settingName === settingsNames.prompt) { - return false; - } - - return defaultValue; - }); - - const actual = await command.validate({ options: { administrativeUnitId: '00000000-0000-0000-0000-000000000000', groupName: groupName, deviceId: deviceId } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation when both groupName and deviceId options are passed', () => { + const actual = commandOptionsSchema.safeParse({ administrativeUnitId: '00000000-0000-0000-0000-000000000000', groupName: groupName, deviceId: deviceId }); + assert.strictEqual(actual.success, false); }); - it('fails validation when both groupName and deviceName options are passed', async () => { - sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => { - if (settingName === settingsNames.prompt) { - return false; - } - - return defaultValue; - }); - - const actual = await command.validate({ options: { administrativeUnitId: '00000000-0000-0000-0000-000000000000', groupName: groupName, deviceName: deviceName } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation when both groupName and deviceName options are passed', () => { + const actual = commandOptionsSchema.safeParse({ administrativeUnitId: '00000000-0000-0000-0000-000000000000', groupName: groupName, deviceName: deviceName }); + assert.strictEqual(actual.success, false); }); - it('fails validation when both deviceId and deviceName options are passed', async () => { - sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => { - if (settingName === settingsNames.prompt) { - return false; - } - - return defaultValue; - }); - - const actual = await command.validate({ options: { administrativeUnitId: '00000000-0000-0000-0000-000000000000', deviceId: deviceId, deviceName: deviceName } }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation when both deviceId and deviceName options are passed', () => { + const actual = commandOptionsSchema.safeParse({ administrativeUnitId: '00000000-0000-0000-0000-000000000000', deviceId: deviceId, deviceName: deviceName }); + assert.strictEqual(actual.success, false); }); it('removes the member specified by id from administrative unit specified by id without prompting for confirmation', async () => { diff --git a/src/m365/entra/commands/administrativeunit/administrativeunit-member-remove.ts b/src/m365/entra/commands/administrativeunit/administrativeunit-member-remove.ts index 0f4f0cb7ed4..84053c97d64 100644 --- a/src/m365/entra/commands/administrativeunit/administrativeunit-member-remove.ts +++ b/src/m365/entra/commands/administrativeunit/administrativeunit-member-remove.ts @@ -1,32 +1,35 @@ -import GlobalOptions from '../../../../GlobalOptions.js'; +import { z } from 'zod'; +import { globalOptionsZod } from '../../../../Command.js'; import { entraAdministrativeUnit } from '../../../../utils/entraAdministrativeUnit.js'; import { entraDevice } from '../../../../utils/entraDevice.js'; import { entraGroup } from '../../../../utils/entraGroup.js'; import { entraUser } from '../../../../utils/entraUser.js'; -import { validation } from '../../../../utils/validation.js'; import GraphCommand from '../../../base/GraphCommand.js'; import commands from '../../commands.js'; import request, { CliRequestOptions } from '../../../../request.js'; import { Logger } from '../../../../cli/Logger.js'; import { cli } from '../../../../cli/cli.js'; +export const options = z.strictObject({ + ...globalOptionsZod.shape, + id: z.uuid().optional().alias('i'), + userId: z.uuid().optional(), + userName: z.string().optional(), + groupId: z.uuid().optional(), + groupName: z.string().optional(), + deviceId: z.uuid().optional(), + deviceName: z.string().optional(), + administrativeUnitId: z.uuid().optional(), + administrativeUnitName: z.string().optional(), + force: z.boolean().optional().alias('f') +}); + +declare type Options = z.infer; + interface CommandArgs { options: Options; } -export interface Options extends GlobalOptions { - id?: string; - userId?: string; - userName?: string; - groupId?: string; - groupName?: string; - deviceId?: string; - deviceName?: string; - administrativeUnitId?: string; - administrativeUnitName?: string; - force?: boolean; -} - class EntraAdministrativeUnitMemberRemoveCommand extends GraphCommand { public get name(): string { return commands.ADMINISTRATIVEUNIT_MEMBER_REMOVE; @@ -36,98 +39,26 @@ class EntraAdministrativeUnitMemberRemoveCommand extends GraphCommand { return 'Remove a specific member (user, group, or device) from an administrative unit'; } - constructor() { - super(); - - this.#initOptions(); - this.#initValidators(); - this.#initOptionSets(); - this.#initTelemetry(); + public get schema(): z.ZodType | undefined { + return options; } - #initTelemetry(): void { - this.telemetry.push((args: CommandArgs) => { - Object.assign(this.telemetryProperties, { - id: typeof args.options.id !== 'undefined', - userId: typeof args.options.userId !== 'undefined', - userName: typeof args.options.userName !== 'undefined', - groupId: typeof args.options.groupId !== 'undefined', - groupName: typeof args.options.groupName !== 'undefined', - deviceId: typeof args.options.deviceId !== 'undefined', - deviceName: typeof args.options.deviceName !== 'undefined', - administrativeUnitId: typeof args.options.administrativeUnitId !== 'undefined', - administrativeUnitName: typeof args.options.administrativeUnitName !== 'undefined', - force: !!args.options.force - }); - }); - } - - #initOptions(): void { - this.options.unshift( - { - option: '-i, --id [id]' - }, - { - option: "--userId [userId]" - }, - { - option: "--userName [userName]" - }, - { - option: "--groupId [groupId]" - }, - { - option: "--groupName [groupName]" - }, - { - option: "--deviceId [deviceId]" - }, - { - option: "--deviceName [deviceName]" - }, - { - option: '--administrativeUnitId [administrativeUnitId]' - }, - { - option: '--administrativeUnitName [administrativeUnitName]' - }, - { - option: '-f, --force' - } - ); - } - - #initValidators(): void { - this.validators.push( - async (args: CommandArgs) => { - if (args.options.administrativeUnitId && !validation.isValidGuid(args.options.administrativeUnitId as string)) { - return `${args.options.administrativeUnitId} is not a valid GUID`; - } - - if (args.options.id && !validation.isValidGuid(args.options.id as string)) { - return `${args.options.id} is not a valid GUID`; - } - - if (args.options.userId && !validation.isValidGuid(args.options.userId as string)) { - return `${args.options.userId} is not a valid GUID`; + public getRefinedSchema(schema: typeof options): z.ZodObject | undefined { + return schema + .refine(options => [options.administrativeUnitId, options.administrativeUnitName].filter(Boolean).length === 1, { + error: 'Specify either administrativeUnitId or administrativeUnitName', + params: { + customCode: 'optionSet', + options: ['administrativeUnitId', 'administrativeUnitName'] } - - if (args.options.groupId && !validation.isValidGuid(args.options.groupId as string)) { - return `${args.options.groupId} is not a valid GUID`; - } - - if (args.options.deviceId && !validation.isValidGuid(args.options.deviceId as string)) { - return `${args.options.deviceId} is not a valid GUID`; + }) + .refine(options => [options.id, options.userId, options.userName, options.groupId, options.groupName, options.deviceId, options.deviceName].filter(Boolean).length === 1, { + error: 'Specify either id, userId, userName, groupId, groupName, deviceId, or deviceName', + params: { + customCode: 'optionSet', + options: ['id', 'userId', 'userName', 'groupId', 'groupName', 'deviceId', 'deviceName'] } - - return true; - } - ); - } - - #initOptionSets(): void { - this.optionSets.push({ options: ['administrativeUnitId', 'administrativeUnitName'] }); - this.optionSets.push({ options: ['id', 'userId', 'userName', 'groupId', 'groupName', 'deviceId', 'deviceName'] }); + }); } public async commandAction(logger: Logger, args: CommandArgs): Promise { diff --git a/src/m365/entra/commands/administrativeunit/administrativeunit-roleassignment-add.spec.ts b/src/m365/entra/commands/administrativeunit/administrativeunit-roleassignment-add.spec.ts index a1d38390b7a..321db85e626 100644 --- a/src/m365/entra/commands/administrativeunit/administrativeunit-roleassignment-add.spec.ts +++ b/src/m365/entra/commands/administrativeunit/administrativeunit-roleassignment-add.spec.ts @@ -4,7 +4,7 @@ import auth from '../../../../Auth.js'; import { cli } from '../../../../cli/cli.js'; import { CommandInfo } from '../../../../cli/CommandInfo.js'; import commands from '../../commands.js'; -import command from './administrativeunit-roleassignment-add.js'; +import command, { options } from './administrativeunit-roleassignment-add.js'; import { telemetry } from '../../../../telemetry.js'; import { pid } from '../../../../utils/pid.js'; import { session } from '../../../../utils/session.js'; @@ -15,7 +15,6 @@ import { entraAdministrativeUnit } from '../../../../utils/entraAdministrativeUn import { entraUser } from '../../../../utils/entraUser.js'; import { roleAssignment } from '../../../../utils/roleAssignment.js'; import { roleDefinition } from '../../../../utils/roleDefinition.js'; -import { settingsNames } from '../../../../settingsNames.js'; import request from '../../../../request.js'; describe(commands.ADMINISTRATIVEUNIT_ROLEASSIGNMENT_ADD, () => { @@ -36,6 +35,7 @@ describe(commands.ADMINISTRATIVEUNIT_ROLEASSIGNMENT_ADD, () => { let logger: Logger; let loggerLogSpy: sinon.SinonSpy; let commandInfo: CommandInfo; + let commandOptionsSchema: typeof options; before(() => { sinon.stub(auth, 'restoreAuth').resolves(); @@ -44,6 +44,7 @@ describe(commands.ADMINISTRATIVEUNIT_ROLEASSIGNMENT_ADD, () => { sinon.stub(session, 'getId').returns(''); auth.connection.active = true; commandInfo = cli.getCommandInfo(command); + commandOptionsSchema = commandInfo.command.getSchemaToParse() as typeof options; }); beforeEach(() => { @@ -68,7 +69,6 @@ describe(commands.ADMINISTRATIVEUNIT_ROLEASSIGNMENT_ADD, () => { entraUser.getUserIdByUpn, roleAssignment.createRoleAssignmentWithAdministrativeUnitScope, roleDefinition.getRoleDefinitionByDisplayName, - cli.getSettingWithDefaultValue, request.post ]); }); @@ -86,173 +86,103 @@ describe(commands.ADMINISTRATIVEUNIT_ROLEASSIGNMENT_ADD, () => { assert.notStrictEqual(command.description, null); }); - it('passes validation if administrative unit id, role definition id and user id are passed', async () => { - const actual = await command.validate({ - options: { - administrativeUnitId: administrativeUnitId, - roleDefinitionId: roleDefinitionId, - userId: userId - } - }, commandInfo); - assert.strictEqual(actual, true); + it('passes validation if administrative unit id, role definition id and user id are passed', () => { + const actual = commandOptionsSchema.safeParse({ + administrativeUnitId: administrativeUnitId, + roleDefinitionId: roleDefinitionId, + userId: userId + }); + assert.strictEqual(actual.success, true); }); - it('passes validation if administrative unit name, role definition name and user name are passed', async () => { - const actual = await command.validate({ - options: { - administrativeUnitName: administrativeUnitName, - roleDefinitionName: roleDefinitionName, - userName: userName - } - }, commandInfo); - assert.strictEqual(actual, true); + it('passes validation if administrative unit name, role definition name and user name are passed', () => { + const actual = commandOptionsSchema.safeParse({ + administrativeUnitName: administrativeUnitName, + roleDefinitionName: roleDefinitionName, + userName: userName + }); + assert.strictEqual(actual.success, true); }); - it('fails validation if both user id and user name are not passed', async () => { - sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => { - if (settingName === settingsNames.prompt) { - return false; - } - - return defaultValue; + it('fails validation if both user id and user name are not passed', () => { + const actual = commandOptionsSchema.safeParse({ + administrativeUnitId: administrativeUnitId, + roleDefinitionId: roleDefinitionId }); - - const actual = await command.validate({ - options: { - administrativeUnitId: administrativeUnitId, - roleDefinitionId: roleDefinitionId - } - }, commandInfo); - assert.notStrictEqual(actual, true); + assert.strictEqual(actual.success, false); }); - it('fails validation if both user id and user name are passed', async () => { - sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => { - if (settingName === settingsNames.prompt) { - return false; - } - - return defaultValue; + it('fails validation if both user id and user name are passed', () => { + const actual = commandOptionsSchema.safeParse({ + administrativeUnitId: administrativeUnitId, + roleDefinitionId: roleDefinitionId, + userId: userId, + userName: userName }); - - const actual = await command.validate({ - options: { - administrativeUnitId: administrativeUnitId, - roleDefinitionId: roleDefinitionId, - userId: userId, - userName: userName - } - }, commandInfo); - assert.notStrictEqual(actual, true); + assert.strictEqual(actual.success, false); }); - it('fails validation if both role definition id and role definition name are not passed', async () => { - sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => { - if (settingName === settingsNames.prompt) { - return false; - } - - return defaultValue; + it('fails validation if both role definition id and role definition name are not passed', () => { + const actual = commandOptionsSchema.safeParse({ + administrativeUnitId: administrativeUnitId, + userId: userId }); - - const actual = await command.validate({ - options: { - administrativeUnitId: administrativeUnitId, - userId: userId - } - }, commandInfo); - assert.notStrictEqual(actual, true); + assert.strictEqual(actual.success, false); }); - it('fails validation if both role definition id and role definition name are passed', async () => { - sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => { - if (settingName === settingsNames.prompt) { - return false; - } - - return defaultValue; + it('fails validation if both role definition id and role definition name are passed', () => { + const actual = commandOptionsSchema.safeParse({ + administrativeUnitId: administrativeUnitId, + roleDefinitionId: roleDefinitionId, + roleDefinitionName: roleDefinitionName, + userId: userId }); - - const actual = await command.validate({ - options: { - administrativeUnitId: administrativeUnitId, - roleDefinitionId: roleDefinitionId, - roleDefinitionName: roleDefinitionName, - userId: userId - } - }, commandInfo); - assert.notStrictEqual(actual, true); + assert.strictEqual(actual.success, false); }); - it('fails validation if both administrative unit id and administrative unit name are not passed', async () => { - sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => { - if (settingName === settingsNames.prompt) { - return false; - } - - return defaultValue; + it('fails validation if both administrative unit id and administrative unit name are not passed', () => { + const actual = commandOptionsSchema.safeParse({ + roleDefinitionId: roleDefinitionId, + userId: userId }); - - const actual = await command.validate({ - options: { - roleDefinitionId: roleDefinitionId, - userId: userId - } - }, commandInfo); - assert.notStrictEqual(actual, true); + assert.strictEqual(actual.success, false); }); - it('fails validation if both administrative unit id and administrative unit name are passed', async () => { - sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => { - if (settingName === settingsNames.prompt) { - return false; - } - - return defaultValue; + it('fails validation if both administrative unit id and administrative unit name are passed', () => { + const actual = commandOptionsSchema.safeParse({ + administrativeUnitId: administrativeUnitId, + administrativeUnitName: administrativeUnitName, + roleDefinitionId: roleDefinitionId, + userId: userId }); - - const actual = await command.validate({ - options: { - administrativeUnitId: administrativeUnitId, - administrativeUnitName: administrativeUnitName, - roleDefinitionId: roleDefinitionId, - userId: userId - } - }, commandInfo); - assert.notStrictEqual(actual, true); + assert.strictEqual(actual.success, false); }); - it('fails validation if administrative unit id is not a valid GUID', async () => { - const actual = await command.validate({ - options: { - administrativeUnitId: '123', - roleDefinitionId: roleDefinitionId, - userId: userId - } - }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation if administrative unit id is not a valid GUID', () => { + const actual = commandOptionsSchema.safeParse({ + administrativeUnitId: '123', + roleDefinitionId: roleDefinitionId, + userId: userId + }); + assert.strictEqual(actual.success, false); }); - it('fails validation if role definition id is not a valid GUID', async () => { - const actual = await command.validate({ - options: { - administrativeUnitId: administrativeUnitId, - roleDefinitionId: '123', - userId: userId - } - }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation if role definition id is not a valid GUID', () => { + const actual = commandOptionsSchema.safeParse({ + administrativeUnitId: administrativeUnitId, + roleDefinitionId: '123', + userId: userId + }); + assert.strictEqual(actual.success, false); }); - it('fails validation if user id is not a valid GUID', async () => { - const actual = await command.validate({ - options: { - administrativeUnitId: administrativeUnitId, - roleDefinitionId: roleDefinitionId, - userId: '123' - } - }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation if user id is not a valid GUID', () => { + const actual = commandOptionsSchema.safeParse({ + administrativeUnitId: administrativeUnitId, + roleDefinitionId: roleDefinitionId, + userId: '123' + }); + assert.strictEqual(actual.success, false); }); it('correctly assigns a role specified by id to and administrative unit specified by id and to a user specified by id', async () => { diff --git a/src/m365/entra/commands/administrativeunit/administrativeunit-roleassignment-add.ts b/src/m365/entra/commands/administrativeunit/administrativeunit-roleassignment-add.ts index f26c9a37d51..daf852084e8 100644 --- a/src/m365/entra/commands/administrativeunit/administrativeunit-roleassignment-add.ts +++ b/src/m365/entra/commands/administrativeunit/administrativeunit-roleassignment-add.ts @@ -1,26 +1,29 @@ -import GlobalOptions from '../../../../GlobalOptions.js'; +import { z } from 'zod'; +import { globalOptionsZod } from '../../../../Command.js'; import { Logger } from '../../../../cli/Logger.js'; import { entraAdministrativeUnit } from '../../../../utils/entraAdministrativeUnit.js'; import { entraUser } from '../../../../utils/entraUser.js'; import { roleAssignment } from '../../../../utils/roleAssignment.js'; import { roleDefinition } from '../../../../utils/roleDefinition.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, + administrativeUnitId: z.uuid().optional().alias('i'), + administrativeUnitName: z.string().optional().alias('n'), + roleDefinitionId: z.uuid().optional(), + roleDefinitionName: z.string().optional(), + userId: z.uuid().optional(), + userName: z.string().optional() +}); + +declare type Options = z.infer; + interface CommandArgs { options: Options; } -interface Options extends GlobalOptions { - administrativeUnitId?: string; - administrativeUnitName?: string; - roleDefinitionId?: string; - roleDefinitionName?: string; - userId?: string; - userName?: string; -} - class EntraAdministrativeUnitRoleAssignmentAddCommand extends GraphCommand { public get name(): string { return commands.ADMINISTRATIVEUNIT_ROLEASSIGNMENT_ADD; @@ -30,75 +33,33 @@ class EntraAdministrativeUnitRoleAssignmentAddCommand extends GraphCommand { return 'Assigns a Microsoft Entra role with administrative unit scope to a user'; } - constructor() { - super(); - - this.#initTelemetry(); - this.#initOptions(); - this.#initValidators(); - this.#initOptionSets(); - } - - #initTelemetry(): void { - this.telemetry.push((args: CommandArgs) => { - Object.assign(this.telemetryProperties, { - administrativeUnitId: typeof args.options.administrativeUnitId !== 'undefined', - administrativeUnitName: typeof args.options.administrativeUnitName !== 'undefined', - roleDefinitionId: typeof args.options.roleDefinitionId !== 'undefined', - roleDefinitionName: typeof args.options.roleDefinitionName !== 'undefined', - userId: typeof args.options.userId !== 'undefined', - userName: typeof args.options.userName !== 'undefined' - }); - }); - } - - #initOptions(): void { - this.options.unshift( - { - option: '-i, --administrativeUnitId [administrativeUnitId]' - }, - { - option: '-n, --administrativeUnitName [administrativeUnitName]' - }, - { - option: '--roleDefinitionId [roleDefinitionId]' - }, - { - option: '--roleDefinitionName [roleDefinitionName]' - }, - { - option: '--userId [userId]' - }, - { - option: '--userName [userName]' - } - ); + public get schema(): z.ZodType | undefined { + return options; } - #initValidators(): void { - this.validators.push( - async (args: CommandArgs) => { - if (args.options.administrativeUnitId && !validation.isValidGuid(args.options.administrativeUnitId)) { - return `${args.options.administrativeUnitId} is not a valid GUID`; + public getRefinedSchema(schema: typeof options): z.ZodObject | undefined { + return schema + .refine(options => [options.administrativeUnitId, options.administrativeUnitName].filter(Boolean).length === 1, { + error: 'Specify either administrativeUnitId or administrativeUnitName', + params: { + customCode: 'optionSet', + options: ['administrativeUnitId', 'administrativeUnitName'] } - - if (args.options.roleDefinitionId && !validation.isValidGuid(args.options.roleDefinitionId)) { - return `${args.options.roleDefinitionId} is not a valid GUID`; + }) + .refine(options => [options.roleDefinitionId, options.roleDefinitionName].filter(Boolean).length === 1, { + error: 'Specify either roleDefinitionId or roleDefinitionName', + params: { + customCode: 'optionSet', + options: ['roleDefinitionId', 'roleDefinitionName'] } - - if (args.options.userId && !validation.isValidGuid(args.options.userId)) { - return `${args.options.userId} is not a valid GUID`; + }) + .refine(options => [options.userId, options.userName].filter(Boolean).length === 1, { + error: 'Specify either userId or userName', + params: { + customCode: 'optionSet', + options: ['userId', 'userName'] } - - return true; - } - ); - } - - #initOptionSets(): void { - this.optionSets.push({ options: ['administrativeUnitId', 'administrativeUnitName'] }); - this.optionSets.push({ options: ['roleDefinitionId', 'roleDefinitionName'] }); - this.optionSets.push({ options: ['userId', 'userName'] }); + }); } public async commandAction(logger: Logger, args: CommandArgs): Promise {