diff --git a/src/m365/connection/commands/connection-remove.spec.ts b/src/m365/connection/commands/connection-remove.spec.ts index 86e01ad5bd1..ae9d4605fe6 100644 --- a/src/m365/connection/commands/connection-remove.spec.ts +++ b/src/m365/connection/commands/connection-remove.spec.ts @@ -6,9 +6,8 @@ import { telemetry } from '../../../telemetry.js'; import { pid } from '../../../utils/pid.js'; import { session } from '../../../utils/session.js'; import commands from '../commands.js'; -import command from './connection-remove.js'; +import command, { options } from './connection-remove.js'; import { CommandInfo } from '../../../cli/CommandInfo.js'; -import { settingsNames } from '../../../settingsNames.js'; import { sinonUtil } from '../../../utils/sinonUtil.js'; import { CommandError } from '../../../Command.js'; import { cli } from '../../../cli/cli.js'; @@ -17,6 +16,7 @@ describe(commands.REMOVE, () => { let log: string[]; let logger: Logger; let commandInfo: CommandInfo; + let commandOptionsSchema: typeof options; before(() => { sinon.stub(auth, 'clearConnectionInfo').resolves(); @@ -25,7 +25,7 @@ describe(commands.REMOVE, () => { sinon.stub(pid, 'getProcessName').returns(''); sinon.stub(session, 'getId').returns(''); commandInfo = cli.getCommandInfo(command); - + commandOptionsSchema = commandInfo.command.getSchemaToParse() as typeof options; auth.connection.active = true; auth.connection.authType = AuthType.DeviceCode; @@ -97,7 +97,6 @@ describe(commands.REMOVE, () => { sinonUtil.restore([ auth.ensureAccessToken, auth.removeConnectionInfo, - cli.getSettingWithDefaultValue, cli.promptForConfirmation, cli.handleMultipleResultsFound ]); @@ -116,22 +115,19 @@ describe(commands.REMOVE, () => { assert.notStrictEqual(command.description, null); }); - it('fails validation if name is not specified', async () => { - sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => { - if (settingName === settingsNames.prompt) { - return false; - } - - return defaultValue; - }); + it('fails validation with no options', () => { + const actual = commandOptionsSchema.safeParse({}); + assert.strictEqual(actual.success, false); + }); - const actual = await command.validate({ options: {} }, commandInfo); - assert.notStrictEqual(actual, true); + it('fails validation with unknown options', () => { + const actual = commandOptionsSchema.safeParse({ unknownOption: "value" }); + assert.strictEqual(actual.success, false); }); it(`fails with error if the connection cannot be found`, async () => { sinon.stub(cli, 'promptForConfirmation').resolves(true); - await assert.rejects(command.action(logger, { options: { name: 'Non-existent connection' } }), new CommandError(`The connection 'Non-existent connection' cannot be found.`)); + await assert.rejects(command.action(logger, { options: commandOptionsSchema.parse({ name: 'Non-existent connection' }) }), new CommandError(`The connection 'Non-existent connection' cannot be found.`)); }); it('fails with error when restoring auth information leads to error', async () => { @@ -139,7 +135,7 @@ describe(commands.REMOVE, () => { sinon.stub(auth, 'restoreAuth').callsFake(() => Promise.reject('An error has occurred')); try { - await assert.rejects(command.action(logger, { options: {} } as any), new CommandError('An error has occurred')); + await assert.rejects(command.action(logger, { options: commandOptionsSchema.parse({ name: 'test' }) }), new CommandError('An error has occurred')); } finally { sinonUtil.restore(auth.restoreAuth); @@ -149,25 +145,24 @@ describe(commands.REMOVE, () => { it(`removes the 'Contoso Application' connection when prompt is confirmed`, async () => { sinon.stub(cli, 'promptForConfirmation').resolves(true); const removeStub = sinon.stub(auth, 'removeConnectionInfo').resolves(); - await command.action(logger, { options: { name: 'acd6df42-10a9-4315-8928-53334f1c9d01' } }); + await command.action(logger, { options: commandOptionsSchema.parse({ name: 'acd6df42-10a9-4315-8928-53334f1c9d01' }) }); assert(removeStub.calledOnce); }); it(`removes the 'Contoso Application' connection and not prompting for confirmation`, async () => { const removeStub = sinon.stub(auth, 'removeConnectionInfo').resolves(); - await command.action(logger, { options: { name: 'acd6df42-10a9-4315-8928-53334f1c9d01', force: true } }); + await command.action(logger, { options: commandOptionsSchema.parse({ name: 'acd6df42-10a9-4315-8928-53334f1c9d01', force: true }) }); assert(removeStub.calledOnce); }); - it('aborts removing the connection when prompt not confirmed', async () => { sinon.stub(cli, 'promptForConfirmation').resolves(false); const removeStub = sinon.stub(auth, 'removeConnectionInfo').resolves(); await command.action(logger, { - options: { + options: commandOptionsSchema.parse({ name: 'acd6df42-10a9-4315-8928-53334f1c9d01' - } + }) }); assert(removeStub.notCalled); }); @@ -176,7 +171,7 @@ describe(commands.REMOVE, () => { sinon.stub(cli, 'promptForConfirmation').resolves(true); const removeStub = sinon.stub(auth, 'removeConnectionInfo').resolves(); - await command.action(logger, { options: { name: '028de82d-7fd9-476e-a9fd-be9714280ff3', debug: true } }); + await command.action(logger, { options: commandOptionsSchema.parse({ name: '028de82d-7fd9-476e-a9fd-be9714280ff3', debug: true }) }); assert(removeStub.calledOnce); }); }); \ No newline at end of file diff --git a/src/m365/connection/commands/connection-remove.ts b/src/m365/connection/commands/connection-remove.ts index 5c1951564eb..0cc8a643f68 100644 --- a/src/m365/connection/commands/connection-remove.ts +++ b/src/m365/connection/commands/connection-remove.ts @@ -1,19 +1,22 @@ +import { z } from 'zod'; import { Logger } from '../../../cli/Logger.js'; import auth from '../../../Auth.js'; import commands from '../commands.js'; -import Command, { CommandError } from '../../../Command.js'; -import GlobalOptions from '../../../GlobalOptions.js'; +import Command, { CommandError, globalOptionsZod } from '../../../Command.js'; import { cli } from '../../../cli/cli.js'; +export const options = z.strictObject({ + ...globalOptionsZod.shape, + name: z.string().alias('n'), + force: z.boolean().optional().alias('f') +}); + +declare type Options = z.infer; + interface CommandArgs { options: Options; } -interface Options extends GlobalOptions { - name: string; - force?: boolean; -} - class ConnectionRemoveCommand extends Command { public get name(): string { return commands.REMOVE; @@ -23,37 +26,8 @@ class ConnectionRemoveCommand extends Command { return 'Remove the specified connection'; } - constructor() { - super(); - - this.#initTelemetry(); - this.#initOptions(); - this.#initTypes(); - } - - - #initTelemetry(): void { - this.telemetry.push((args: CommandArgs) => { - Object.assign(this.telemetryProperties, { - force: !!args.options.force - }); - }); - } - - #initOptions(): void { - this.options.unshift( - { - option: '-n, --name ' - }, - { - option: '-f, --force' - } - ); - } - - #initTypes(): void { - this.types.string.push('name'); - this.types.boolean.push('force'); + public get schema(): z.ZodType | undefined { + return options; } public async commandAction(logger: Logger, args: CommandArgs): Promise { diff --git a/src/m365/connection/commands/connection-use.spec.ts b/src/m365/connection/commands/connection-use.spec.ts index b26df13a5be..0efdc6a4a5e 100644 --- a/src/m365/connection/commands/connection-use.spec.ts +++ b/src/m365/connection/commands/connection-use.spec.ts @@ -6,9 +6,9 @@ import { telemetry } from '../../../telemetry.js'; import { pid } from '../../../utils/pid.js'; import { session } from '../../../utils/session.js'; import commands from '../commands.js'; -import command from './connection-use.js'; -import { settingsNames } from '../../../settingsNames.js'; +import command, { options } from './connection-use.js'; import { sinonUtil } from '../../../utils/sinonUtil.js'; +import { CommandInfo } from '../../../cli/CommandInfo.js'; import { CommandError } from '../../../Command.js'; import { cli } from '../../../cli/cli.js'; import { ConnectionDetails } from '../../commands/ConnectionDetails.js'; @@ -17,6 +17,8 @@ describe(commands.USE, () => { let log: string[]; let logger: Logger; let loggerLogSpy: sinon.SinonSpy; + let commandInfo: CommandInfo; + let commandOptionsSchema: typeof options; const mockContosoApplicationIdentityResponse = { "connectedAs": "Contoso Application", "connectionName": "acd6df42-10a9-4315-8928-53334f1c9d01", @@ -80,7 +82,9 @@ describe(commands.USE, () => { sinon.stub(telemetry, 'trackEvent').resolves(); sinon.stub(pid, 'getProcessName').returns(''); sinon.stub(session, 'getId').returns(''); - sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => settingName === settingsNames.prompt ? false : defaultValue); + + commandInfo = cli.getCommandInfo(command); + commandOptionsSchema = commandInfo.command.getSchemaToParse() as typeof options; auth.connection.active = true; auth.connection.authType = AuthType.DeviceCode; @@ -132,8 +136,18 @@ describe(commands.USE, () => { assert.notStrictEqual(command.description, null); }); + it('passes validation with no options', () => { + const actual = commandOptionsSchema.safeParse({}); + assert.strictEqual(actual.success, true); + }); + + it('fails validation with unknown options', () => { + const actual = commandOptionsSchema.safeParse({ unknownOption: "value" }); + assert.strictEqual(actual.success, false); + }); + it(`fails with error if the connection cannot be found`, async () => { - await assert.rejects(command.action(logger, { options: { name: 'Non-existent connection' } }), + await assert.rejects(command.action(logger, { options: commandOptionsSchema.parse({ name: 'Non-existent connection' }) }), new CommandError(`The connection 'Non-existent connection' cannot be found.`)); }); @@ -142,7 +156,7 @@ describe(commands.USE, () => { sinon.stub(auth, 'restoreAuth').callsFake(() => Promise.reject('An error has occurred')); try { - await assert.rejects(command.action(logger, { options: {} } as any), new CommandError('An error has occurred')); + await assert.rejects(command.action(logger, { options: commandOptionsSchema.parse({}) }), new CommandError('An error has occurred')); } finally { sinonUtil.restore(auth.restoreAuth); @@ -150,17 +164,17 @@ describe(commands.USE, () => { }); it(`switches to the 'Contoso Application' identity using the name option`, async () => { - await command.action(logger, { options: { name: 'acd6df42-10a9-4315-8928-53334f1c9d01' } }); + await command.action(logger, { options: commandOptionsSchema.parse({ name: 'acd6df42-10a9-4315-8928-53334f1c9d01' }) }); assert(loggerLogSpy.calledOnceWithExactly(mockContosoApplicationIdentityResponse)); }); it(`switches to the user identity using the name option`, async () => { - await command.action(logger, { options: { name: '028de82d-7fd9-476e-a9fd-be9714280ff3' } }); + await command.action(logger, { options: commandOptionsSchema.parse({ name: '028de82d-7fd9-476e-a9fd-be9714280ff3' }) }); assert(loggerLogSpy.calledOnceWithExactly(mockUserIdentityResponse)); }); it(`switches to the user identity using the name option (debug)`, async () => { - await command.action(logger, { options: { name: '028de82d-7fd9-476e-a9fd-be9714280ff3', debug: true } }); + await command.action(logger, { options: commandOptionsSchema.parse({ name: '028de82d-7fd9-476e-a9fd-be9714280ff3', debug: true }) }); const logged = loggerLogSpy.args[0][0] as unknown as ConnectionDetails; assert.strictEqual(logged.connectedAs, mockUserIdentityResponse.connectedAs); }); @@ -168,14 +182,14 @@ describe(commands.USE, () => { it('switches to the identity connection using prompting', async () => { sinon.stub(cli, 'handleMultipleResultsFound').resolves(connections[1]); - await command.action(logger, { options: {} }); + await command.action(logger, { options: commandOptionsSchema.parse({}) }); assert(loggerLogSpy.calledOnceWithExactly(mockContosoApplicationIdentityResponse)); }); it(`switches to the user identity using prompting`, async () => { sinon.stub(cli, 'handleMultipleResultsFound').resolves(connections[0]); - await command.action(logger, { options: {} }); + await command.action(logger, { options: commandOptionsSchema.parse({}) }); assert(loggerLogSpy.calledOnceWithExactly(mockUserIdentityResponse)); }); }); \ No newline at end of file diff --git a/src/m365/connection/commands/connection-use.ts b/src/m365/connection/commands/connection-use.ts index e065674eaf1..2c7ece1dc43 100644 --- a/src/m365/connection/commands/connection-use.ts +++ b/src/m365/connection/commands/connection-use.ts @@ -1,19 +1,22 @@ +import { z } from 'zod'; import { Logger } from '../../../cli/Logger.js'; import auth, { Connection } from '../../../Auth.js'; import commands from '../commands.js'; -import Command, { CommandError } from '../../../Command.js'; -import GlobalOptions from '../../../GlobalOptions.js'; +import Command, { CommandError, globalOptionsZod } from '../../../Command.js'; import { formatting } from '../../../utils/formatting.js'; import { cli } from '../../../cli/cli.js'; +export const options = z.strictObject({ + ...globalOptionsZod.shape, + name: z.string().optional().alias('n') +}); + +declare type Options = z.infer; + interface CommandArgs { options: Options; } -interface Options extends GlobalOptions { - name?: string; -} - class ConnectionUseCommand extends Command { public get name(): string { return commands.USE; @@ -23,32 +26,8 @@ class ConnectionUseCommand extends Command { return 'Activate the specified Microsoft 365 tenant connection'; } - constructor() { - super(); - - this.#initOptions(); - this.#initTelemetry(); - this.#initTypes(); - } - - #initTelemetry(): void { - this.telemetry.push((args: CommandArgs) => { - Object.assign(this.telemetryProperties, { - name: typeof args.options.name !== 'undefined' - }); - }); - } - - #initOptions(): void { - this.options.unshift( - { - option: '-n, --name [name]' - } - ); - } - - #initTypes(): void { - this.types.string.push('name'); + public get schema(): z.ZodType | undefined { + return options; } public async commandAction(logger: Logger, args: CommandArgs): Promise {