From 3c50c6495aaf0315b90aad546c5460c98285bb4d Mon Sep 17 00:00:00 2001 From: Milan Holemans <11723921+milanholemans@users.noreply.github.com> Date: Sat, 27 Jul 2024 19:16:42 +0200 Subject: [PATCH] Adds prompting to 'connection use'. Closes #6173 --- docs/docs/cmd/connection/connection-use.mdx | 10 +- src/Auth.ts | 2 +- .../commands/connection-remove.spec.ts | 2 +- .../connection/commands/connection-remove.ts | 8 +- .../commands/connection-set.spec.ts | 2 +- .../connection/commands/connection-set.ts | 5 + .../commands/connection-use.spec.ts | 114 +++++++++--------- .../connection/commands/connection-use.ts | 33 ++++- 8 files changed, 109 insertions(+), 67 deletions(-) diff --git a/docs/docs/cmd/connection/connection-use.mdx b/docs/docs/cmd/connection/connection-use.mdx index 716d5774a2d..81203480b06 100644 --- a/docs/docs/cmd/connection/connection-use.mdx +++ b/docs/docs/cmd/connection/connection-use.mdx @@ -15,7 +15,7 @@ m365 connection use [options] ## Options ```md definition-list -`-n, --name ` +`-n, --name [name]` : The name of the connection to switch to. ``` @@ -23,7 +23,13 @@ m365 connection use [options] ## Remarks -The value for `--name` can be found by running [m365 connection list](connection-list.mdx). You can update the name of a connection by running [m365 connection set](connection-set.mdx). +:::tip + +If you haven't disabled the "prompt" setting, running this command without options will show a list of available connections. You can then select the connection to activate it. + +::: + +You can update the name of a connection by running [m365 connection set](connection-set.mdx). ## Examples diff --git a/src/Auth.ts b/src/Auth.ts index 154bc6ed035..cc86d97f3fb 100644 --- a/src/Auth.ts +++ b/src/Auth.ts @@ -870,7 +870,7 @@ export class Auth { const connection = allConnections.find(i => i.name === name); if (!connection) { - throw new CommandError(`The connection '${name}' cannot be found`); + throw new CommandError(`The connection '${name}' cannot be found.`); } return connection; diff --git a/src/m365/connection/commands/connection-remove.spec.ts b/src/m365/connection/commands/connection-remove.spec.ts index 02a8d169d32..e035607f232 100644 --- a/src/m365/connection/commands/connection-remove.spec.ts +++ b/src/m365/connection/commands/connection-remove.spec.ts @@ -131,7 +131,7 @@ describe(commands.REMOVE, () => { 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: { 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 () => { diff --git a/src/m365/connection/commands/connection-remove.ts b/src/m365/connection/commands/connection-remove.ts index 7241fac6ba8..5c1951564eb 100644 --- a/src/m365/connection/commands/connection-remove.ts +++ b/src/m365/connection/commands/connection-remove.ts @@ -28,13 +28,14 @@ class ConnectionRemoveCommand extends Command { this.#initTelemetry(); this.#initOptions(); + this.#initTypes(); } #initTelemetry(): void { this.telemetry.push((args: CommandArgs) => { Object.assign(this.telemetryProperties, { - force: (!(!args.options.force)).toString() + force: !!args.options.force }); }); } @@ -50,6 +51,11 @@ class ConnectionRemoveCommand extends Command { ); } + #initTypes(): void { + this.types.string.push('name'); + this.types.boolean.push('force'); + } + public async commandAction(logger: Logger, args: CommandArgs): Promise { const deleteConnection = async (): Promise => { const connection = await auth.getConnection(args.options.name); diff --git a/src/m365/connection/commands/connection-set.spec.ts b/src/m365/connection/commands/connection-set.spec.ts index b47efb5a5d2..c5d4440f628 100644 --- a/src/m365/connection/commands/connection-set.spec.ts +++ b/src/m365/connection/commands/connection-set.spec.ts @@ -126,7 +126,7 @@ describe(commands.SET, () => { }); it(`fails with error if the connection cannot be found`, async () => { - await assert.rejects(command.action(logger, { options: { name: 'Non-existent connection', newName: 'something new' } }), new CommandError(`The connection 'Non-existent connection' cannot be found`)); + await assert.rejects(command.action(logger, { options: { name: 'Non-existent connection', newName: 'something new' } }), new CommandError(`The connection 'Non-existent connection' cannot be found.`)); }); it(`fails with error if the newName is already in use`, async () => { diff --git a/src/m365/connection/commands/connection-set.ts b/src/m365/connection/commands/connection-set.ts index bae96669971..3119b912daf 100644 --- a/src/m365/connection/commands/connection-set.ts +++ b/src/m365/connection/commands/connection-set.ts @@ -27,6 +27,7 @@ class ConnectionSetCommand extends Command { this.#initOptions(); this.#initValidators(); + this.#initTypes(); } #initOptions(): void { @@ -52,6 +53,10 @@ class ConnectionSetCommand extends Command { ); } + #initTypes(): void { + this.types.string.push('name', 'newName'); + } + public async commandAction(logger: Logger, args: CommandArgs): Promise { const connection = await auth.getConnection(args.options.name); diff --git a/src/m365/connection/commands/connection-use.spec.ts b/src/m365/connection/commands/connection-use.spec.ts index ae59d687aad..d1951799030 100644 --- a/src/m365/connection/commands/connection-use.spec.ts +++ b/src/m365/connection/commands/connection-use.spec.ts @@ -7,7 +7,6 @@ import { pid } from '../../../utils/pid.js'; import { session } from '../../../utils/session.js'; import commands from '../commands.js'; import command from './connection-use.js'; -import { CommandInfo } from '../../../cli/CommandInfo.js'; import { settingsNames } from '../../../settingsNames.js'; import { sinonUtil } from '../../../utils/sinonUtil.js'; import { CommandError } from '../../../Command.js'; @@ -18,8 +17,6 @@ describe(commands.USE, () => { let log: string[]; let logger: Logger; let loggerLogSpy: sinon.SinonSpy; - let commandInfo: CommandInfo; - const mockContosoApplicationIdentityResponse = { "connectedAs": "Contoso Application", "connectionName": "acd6df42-10a9-4315-8928-53334f1c9d01", @@ -38,13 +35,52 @@ describe(commands.USE, () => { "cloudType": "Public" }; + const connections = [ + { + authType: AuthType.DeviceCode, + active: true, + name: '028de82d-7fd9-476e-a9fd-be9714280ff3', + identityName: 'alexw@contoso.com', + identityId: '028de82d-7fd9-476e-a9fd-be9714280ff3', + identityTenantId: 'db308122-52f3-4241-af92-1734aa6e2e50', + appId: '31359c7f-bd7e-475c-86db-fdb8c937548e', + tenant: 'common', + cloudType: CloudType.Public, + certificateType: CertificateType.Unknown, + accessTokens: { + 'https://graph.microsoft.com': { + expiresOn: (new Date()).toISOString(), + accessToken: 'abc' + } + } + }, + { + authType: AuthType.Secret, + active: true, + name: 'acd6df42-10a9-4315-8928-53334f1c9d01', + identityName: 'Contoso Application', + identityId: 'acd6df42-10a9-4315-8928-53334f1c9d01', + identityTenantId: 'db308122-52f3-4241-af92-1734aa6e2e50', + appId: '39446e2e-5081-4887-980c-f285919fccca', + tenant: 'db308122-52f3-4241-af92-1734aa6e2e50', + cloudType: CloudType.Public, + certificateType: CertificateType.Unknown, + accessTokens: { + 'https://graph.microsoft.com': { + expiresOn: (new Date()).toISOString(), + accessToken: 'abc' + } + } + } + ]; + before(() => { sinon.stub(auth, 'clearConnectionInfo').resolves(); sinon.stub(auth, 'storeConnectionInfo').resolves(); sinon.stub(telemetry, 'trackEvent').returns(); sinon.stub(pid, 'getProcessName').returns(''); sinon.stub(session, 'getId').returns(''); - commandInfo = cli.getCommandInfo(command); + sinon.stub(cli, 'getSettingWithDefaultValue').callsFake((settingName, defaultValue) => settingName === settingsNames.prompt ? false : defaultValue); auth.connection.active = true; auth.connection.authType = AuthType.DeviceCode; @@ -55,44 +91,7 @@ describe(commands.USE, () => { auth.connection.appId = '31359c7f-bd7e-475c-86db-fdb8c937548e'; auth.connection.tenant = 'common'; - (auth as any)._allConnections = [ - { - authType: AuthType.DeviceCode, - active: true, - name: '028de82d-7fd9-476e-a9fd-be9714280ff3', - identityName: 'alexw@contoso.com', - identityId: '028de82d-7fd9-476e-a9fd-be9714280ff3', - identityTenantId: 'db308122-52f3-4241-af92-1734aa6e2e50', - appId: '31359c7f-bd7e-475c-86db-fdb8c937548e', - tenant: 'common', - cloudType: CloudType.Public, - certificateType: CertificateType.Unknown, - accessTokens: { - 'https://graph.microsoft.com': { - expiresOn: (new Date()).toISOString(), - accessToken: 'abc' - } - } - }, - { - authType: AuthType.Secret, - active: true, - name: 'acd6df42-10a9-4315-8928-53334f1c9d01', - identityName: 'Contoso Application', - identityId: 'acd6df42-10a9-4315-8928-53334f1c9d01', - identityTenantId: 'db308122-52f3-4241-af92-1734aa6e2e50', - appId: '39446e2e-5081-4887-980c-f285919fccca', - tenant: 'db308122-52f3-4241-af92-1734aa6e2e50', - cloudType: CloudType.Public, - certificateType: CertificateType.Unknown, - accessTokens: { - 'https://graph.microsoft.com': { - expiresOn: (new Date()).toISOString(), - accessToken: 'abc' - } - } - } - ]; + (auth as any)._allConnections = connections; }); beforeEach(() => { @@ -115,7 +114,6 @@ describe(commands.USE, () => { afterEach(() => { sinonUtil.restore([ - cli.getSettingWithDefaultValue, auth.ensureAccessToken, cli.handleMultipleResultsFound ]); @@ -134,21 +132,9 @@ describe(commands.USE, () => { 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; - }); - - const actual = await command.validate({ options: {} }, commandInfo); - assert.notStrictEqual(actual, true); - }); - it(`fails with error if the connection cannot be found`, async () => { - 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: { 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 () => { @@ -178,4 +164,18 @@ describe(commands.USE, () => { const logged = loggerLogSpy.args[0][0] as unknown as ConnectionDetails; assert.strictEqual(logged.connectedAs, mockUserIdentityResponse.connectedAs); }); + + it('switches to the identity connection using prompting', async () => { + sinon.stub(cli, 'handleMultipleResultsFound').resolves(connections[1]); + + await command.action(logger, { options: {} }); + 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: {} }); + 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 11b1d92c2bd..e065674eaf1 100644 --- a/src/m365/connection/commands/connection-use.ts +++ b/src/m365/connection/commands/connection-use.ts @@ -1,15 +1,17 @@ import { Logger } from '../../../cli/Logger.js'; -import auth from '../../../Auth.js'; +import auth, { Connection } from '../../../Auth.js'; import commands from '../commands.js'; import Command, { CommandError } from '../../../Command.js'; import GlobalOptions from '../../../GlobalOptions.js'; +import { formatting } from '../../../utils/formatting.js'; +import { cli } from '../../../cli/cli.js'; interface CommandArgs { options: Options; } interface Options extends GlobalOptions { - name: string; + name?: string; } class ConnectionUseCommand extends Command { @@ -25,18 +27,41 @@ class ConnectionUseCommand extends Command { 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 ' + option: '-n, --name [name]' } ); } + #initTypes(): void { + this.types.string.push('name'); + } + public async commandAction(logger: Logger, args: CommandArgs): Promise { - const connection = await auth.getConnection(args.options.name); + let connection: Connection; + if (args.options.name) { + connection = await auth.getConnection(args.options.name); + } + else { + const connections = await auth.getAllConnections(); + connections.sort((a, b) => a.name!.localeCompare(b.name!)); + const keyValuePair = formatting.convertArrayToHashTable('name', connections); + connection = await cli.handleMultipleResultsFound('Please select the connection you want to activate.', keyValuePair); + } if (this.verbose) { await logger.logToStderr(`Switching to connection '${connection.identityName}', appId: ${connection.appId}, tenantId: ${connection.identityTenantId}...`);