Skip to content

Commit

Permalink
Adds prompting to 'connection use'. Closes pnp#6173
Browse files Browse the repository at this point in the history
  • Loading branch information
milanholemans authored and Jwaegebaert committed Aug 3, 2024
1 parent 9b7ec8e commit 3c50c64
Show file tree
Hide file tree
Showing 8 changed files with 109 additions and 67 deletions.
10 changes: 8 additions & 2 deletions docs/docs/cmd/connection/connection-use.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,21 @@ m365 connection use [options]
## Options

```md definition-list
`-n, --name <name>`
`-n, --name [name]`
: The name of the connection to switch to.
```

<Global />

## 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

Expand Down
2 changes: 1 addition & 1 deletion src/Auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion src/m365/connection/commands/connection-remove.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down
8 changes: 7 additions & 1 deletion src/m365/connection/commands/connection-remove.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
});
});
}
Expand All @@ -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<void> {
const deleteConnection = async (): Promise<void> => {
const connection = await auth.getConnection(args.options.name);
Expand Down
2 changes: 1 addition & 1 deletion src/m365/connection/commands/connection-set.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down
5 changes: 5 additions & 0 deletions src/m365/connection/commands/connection-set.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ class ConnectionSetCommand extends Command {

this.#initOptions();
this.#initValidators();
this.#initTypes();
}

#initOptions(): void {
Expand All @@ -52,6 +53,10 @@ class ConnectionSetCommand extends Command {
);
}

#initTypes(): void {
this.types.string.push('name', 'newName');
}

public async commandAction(logger: Logger, args: CommandArgs): Promise<void> {
const connection = await auth.getConnection(args.options.name);

Expand Down
114 changes: 57 additions & 57 deletions src/m365/connection/commands/connection-use.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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",
Expand All @@ -38,13 +35,52 @@ describe(commands.USE, () => {
"cloudType": "Public"
};

const connections = [
{
authType: AuthType.DeviceCode,
active: true,
name: '028de82d-7fd9-476e-a9fd-be9714280ff3',
identityName: '[email protected]',
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;
Expand All @@ -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: '[email protected]',
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(() => {
Expand All @@ -115,7 +114,6 @@ describe(commands.USE, () => {

afterEach(() => {
sinonUtil.restore([
cli.getSettingWithDefaultValue,
auth.ensureAccessToken,
cli.handleMultipleResultsFound
]);
Expand All @@ -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 () => {
Expand Down Expand Up @@ -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));
});
});
33 changes: 29 additions & 4 deletions src/m365/connection/commands/connection-use.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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 <name>'
option: '-n, --name [name]'
}
);
}

#initTypes(): void {
this.types.string.push('name');
}

public async commandAction(logger: Logger, args: CommandArgs): Promise<void> {
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<Connection>('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}...`);
Expand Down

0 comments on commit 3c50c64

Please sign in to comment.