diff --git a/src/m365/entra/commands/administrativeunit/administrativeunit-remove.spec.ts b/src/m365/entra/commands/administrativeunit/administrativeunit-remove.spec.ts index 0a8bfd0d5f7..87abe7fdccd 100644 --- a/src/m365/entra/commands/administrativeunit/administrativeunit-remove.spec.ts +++ b/src/m365/entra/commands/administrativeunit/administrativeunit-remove.spec.ts @@ -1,18 +1,19 @@ import assert from 'assert'; import sinon from 'sinon'; +import { z } from 'zod'; import auth from '../../../../Auth.js'; -import commands from '../../commands.js'; -import request from '../../../../request.js'; -import { telemetry } from '../../../../telemetry.js'; +import { cli } from '../../../../cli/cli.js'; +import { CommandInfo } from '../../../../cli/CommandInfo.js'; import { Logger } from '../../../../cli/Logger.js'; import { CommandError } from '../../../../Command.js'; import { pid } from '../../../../utils/pid.js'; 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-remove.js'; +import { telemetry } from '../../../../telemetry.js'; +import request from '../../../../request.js'; import { entraAdministrativeUnit } from '../../../../utils/entraAdministrativeUnit.js'; +import commands from '../../commands.js'; +import command from './administrativeunit-remove.js'; describe(commands.ADMINISTRATIVEUNIT_REMOVE, () => { const administrativeUnitId = 'fc33aa61-cf0e-46b6-9506-f633347202ab'; @@ -21,6 +22,7 @@ describe(commands.ADMINISTRATIVEUNIT_REMOVE, () => { let log: string[]; let logger: Logger; let commandInfo: CommandInfo; + let commandOptionsSchema: z.ZodTypeAny; let promptIssued: boolean; before(() => { @@ -30,6 +32,7 @@ describe(commands.ADMINISTRATIVEUNIT_REMOVE, () => { sinon.stub(session, 'getId').returns(''); auth.connection.active = true; commandInfo = cli.getCommandInfo(command); + commandOptionsSchema = commandInfo.command.getSchemaToParse()!; }); beforeEach(() => { @@ -75,6 +78,40 @@ describe(commands.ADMINISTRATIVEUNIT_REMOVE, () => { assert.notStrictEqual(command.description, null); }); + it('fails validation when neither id nor displayName is specified', () => { + const actual = commandOptionsSchema.safeParse({}); + assert.strictEqual(actual.success, false); + }); + + it('passes validation when id is specified', () => { + const actual = commandOptionsSchema.safeParse({ + id: administrativeUnitId + }); + assert.strictEqual(actual.success, true); + }); + + it('passes validation when displayName is specified', () => { + const actual = commandOptionsSchema.safeParse({ + displayName: displayName + }); + assert.strictEqual(actual.success, true); + }); + + it('fails validation when id is not a valid UUID', () => { + const actual = commandOptionsSchema.safeParse({ + id: 'invalid' + }); + assert.strictEqual(actual.success, false); + }); + + it('fails validation when both id and displayName are specified', () => { + const actual = commandOptionsSchema.safeParse({ + id: administrativeUnitId, + displayName: displayName + }); + assert.strictEqual(actual.success, false); + }); + it('removes the specified administrative unit by id without prompting for confirmation', async () => { const deleteRequestStub = sinon.stub(request, 'delete').callsFake(async (opts) => { if (opts.url === `https://graph.microsoft.com/v1.0/directory/administrativeUnits/${administrativeUnitId}`) { @@ -84,7 +121,12 @@ describe(commands.ADMINISTRATIVEUNIT_REMOVE, () => { throw 'Invalid request'; }); - await command.action(logger, { options: { id: administrativeUnitId, force: true } }); + await command.action(logger, { + options: commandOptionsSchema.parse({ + id: administrativeUnitId, + force: true + }) + }); assert(deleteRequestStub.called); }); @@ -102,7 +144,11 @@ describe(commands.ADMINISTRATIVEUNIT_REMOVE, () => { sinonUtil.restore(cli.promptForConfirmation); sinon.stub(cli, 'promptForConfirmation').resolves(true); - await command.action(logger, { options: { displayName: displayName } }); + await command.action(logger, { + options: commandOptionsSchema.parse({ + displayName: displayName + }) + }); assert(deleteRequestStub.called); }); @@ -126,12 +172,21 @@ describe(commands.ADMINISTRATIVEUNIT_REMOVE, () => { throw 'Invalid request'; }); - await assert.rejects(command.action(logger, { options: { id: administrativeUnitId, force: true } }), + await assert.rejects(command.action(logger, { + options: commandOptionsSchema.parse({ + id: administrativeUnitId, + force: true + }) + }), new CommandError(error.error.message)); }); it('prompts before removing the specified administrative unit when confirm option not passed', async () => { - await command.action(logger, { options: { id: administrativeUnitId } }); + await command.action(logger, { + options: commandOptionsSchema.parse({ + id: administrativeUnitId + }) + }); assert(promptIssued); }); @@ -139,17 +194,11 @@ describe(commands.ADMINISTRATIVEUNIT_REMOVE, () => { it('aborts removing administrative unit when prompt not confirmed', async () => { const deleteSpy = sinon.stub(request, 'delete').resolves(); - await command.action(logger, { options: { id: administrativeUnitId } }); + await command.action(logger, { + options: commandOptionsSchema.parse({ + id: administrativeUnitId + }) + }); assert(deleteSpy.notCalled); }); - - it('fails validation if id is not a valid GUID', async () => { - const actual = await command.validate({ options: { id: 'invalid' } }, commandInfo); - assert.notStrictEqual(actual, true); - }); - - it('passes validation when id is a valid GUID', async () => { - const actual = await command.validate({ options: { id: administrativeUnitId } }, commandInfo); - assert.strictEqual(actual, true); - }); }); \ No newline at end of file diff --git a/src/m365/entra/commands/administrativeunit/administrativeunit-remove.ts b/src/m365/entra/commands/administrativeunit/administrativeunit-remove.ts index 35bb9a277dd..e7c584a16b3 100644 --- a/src/m365/entra/commands/administrativeunit/administrativeunit-remove.ts +++ b/src/m365/entra/commands/administrativeunit/administrativeunit-remove.ts @@ -1,22 +1,27 @@ +import { z } from 'zod'; +import { cli } from '../../../../cli/cli.js'; +import { Logger } from '../../../../cli/Logger.js'; +import { globalOptionsZod } from '../../../../Command.js'; +import request, { CliRequestOptions } from '../../../../request.js'; import { entraAdministrativeUnit } from '../../../../utils/entraAdministrativeUnit.js'; -import GlobalOptions from "../../../../GlobalOptions.js"; -import { Logger } from "../../../../cli/Logger.js"; -import { validation } from "../../../../utils/validation.js"; -import request, { CliRequestOptions } from "../../../../request.js"; -import GraphCommand from "../../../base/GraphCommand.js"; -import commands from "../../commands.js"; -import { cli } from "../../../../cli/cli.js"; +import { zod } from '../../../../utils/zod.js'; +import GraphCommand from '../../../base/GraphCommand.js'; +import commands from '../../commands.js'; + +const options = globalOptionsZod + .extend({ + id: zod.alias('i', z.string().uuid().optional()), + displayName: zod.alias('n', z.string().optional()), + force: zod.alias('f', z.boolean().optional()) + }) + .strict(); + +declare type Options = z.infer; interface CommandArgs { options: Options; } -interface Options extends GlobalOptions { - id?: string; - displayName?: string; - force?: boolean -} - class EntraAdministrativeUnitRemoveCommand extends GraphCommand { public get name(): string { return commands.ADMINISTRATIVEUNIT_REMOVE; @@ -25,62 +30,18 @@ class EntraAdministrativeUnitRemoveCommand extends GraphCommand { return 'Removes an administrative unit'; } - constructor() { - super(); - - this.#initOptions(); - this.#initValidators(); - this.#initOptionSets(); - this.#initTelemetry(); - this.#initTypes(); + public get schema(): z.ZodTypeAny | undefined { + return options; } - #initTelemetry(): void { - this.telemetry.push((args: CommandArgs) => { - Object.assign(this.telemetryProperties, { - id: args.options.id !== 'undefined', - displayName: args.options.displayName !== 'undefined', - force: !!args.options.force + public getRefinedSchema(schema: typeof options): z.ZodEffects | undefined { + return schema + .refine(options => options.id || options.displayName, { + message: 'Specify either id or displayName' + }) + .refine(options => !(options.id && options.displayName), { + message: 'Specify either id or displayName but not both' }); - }); - } - - #initOptions(): void { - this.options.unshift( - { - option: '-i, --id [id]' - }, - { - option: '-n, --displayName [displayName]' - }, - { - option: '-f, --force' - } - ); - } - - #initOptionSets(): void { - this.optionSets.push( - { - options: ['id', 'displayName'] - } - ); - } - - #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 for option id.`; - } - - return true; - } - ); - } - - #initTypes(): void { - this.types.string.push('id', 'displayName'); } public async commandAction(logger: Logger, args: CommandArgs): Promise {