diff --git a/command-snapshot.json b/command-snapshot.json index 6c698b13..bb48f2de 100644 --- a/command-snapshot.json +++ b/command-snapshot.json @@ -82,8 +82,17 @@ { "command": "org:login:sfdx-url", "plugin": "@salesforce/plugin-auth", - "flags": ["alias", "json", "loglevel", "no-prompt", "set-default", "set-default-dev-hub", "sfdx-url-file"], "alias": ["force:auth:sfdxurl:store", "auth:sfdxurl:store"], + "flags": [ + "alias", + "json", + "loglevel", + "no-prompt", + "set-default", + "set-default-dev-hub", + "sfdx-url-file", + "sfdx-url-stdin" + ], "flagChars": ["a", "d", "f", "p", "s"], "flagAliases": [ "noprompt", diff --git a/messages/sfdxurl.store.md b/messages/sfdxurl.store.md index 706da034..b27c7293 100644 --- a/messages/sfdxurl.store.md +++ b/messages/sfdxurl.store.md @@ -22,6 +22,14 @@ You can also create a JSON file that has a top-level property named sfdxAuthUrl Path to a file that contains the Salesforce DX authorization URL. +# flags.sfdx-url-stdin.summary + +Read sfdx auth url from stdin + +# errors.missingAuthUrl + +Error retrieving the auth URL. Please ensure it meets the description shown in the documentation for this command. + # examples - Authorize an org using the SFDX authorization URL in the files/authFile.json file: @@ -31,3 +39,7 @@ Path to a file that contains the Salesforce DX authorization URL. - Similar to previous example, but set the org as your default and give it an alias MyDefaultOrg: <%= config.bin %> <%= command.id %> --sfdx-url-file files/authFile.json --set-default --alias MyDefaultOrg + +- Authorize an org reading the SFDX authorization URL from stdin: + + echo 'force://PlatformCLI::CoffeeAndBacon@su0503.my.salesforce.com' | <%= config.bin %> <%= command.id %> --sfdx-url-stdin --set-default --alias MyDefaultOrg diff --git a/src/commands/org/login/sfdx-url.ts b/src/commands/org/login/sfdx-url.ts index 003b4e9e..007e3101 100644 --- a/src/commands/org/login/sfdx-url.ts +++ b/src/commands/org/login/sfdx-url.ts @@ -11,6 +11,7 @@ import { AuthFields, AuthInfo, Messages } from '@salesforce/core'; import { AnyJson } from '@salesforce/ts-types'; import { parseJson } from '@salesforce/kit'; import { AuthBaseCommand } from '../../../authBaseCommand'; +import { read } from '../../../stdin'; Messages.importMessagesDirectory(__dirname); const messages = Messages.loadMessages('@salesforce/plugin-auth', 'sfdxurl.store'); @@ -22,6 +23,7 @@ type AuthJson = AnyJson & { result?: AnyJson & { sfdxAuthUrl: string }; sfdxAuthUrl: string; }; + export default class LoginSfdxUrl extends AuthBaseCommand { public static readonly summary = messages.getMessage('summary'); public static readonly description = messages.getMessage('description', [AUTH_URL_FORMAT]); @@ -29,12 +31,18 @@ export default class LoginSfdxUrl extends AuthBaseCommand { public static aliases = ['force:auth:sfdxurl:store', 'auth:sfdxurl:store']; public static readonly flags = { + 'sfdx-url-stdin': Flags.boolean({ + summary: messages.getMessage('flags.sfdx-url-stdin.summary'), + exclusive: ['sfdx-url-file'], + exactlyOne: ['sfdx-url-file'], + }), 'sfdx-url-file': Flags.file({ char: 'f', summary: messages.getMessage('flags.sfdx-url-file.summary'), - required: true, deprecateAliases: true, aliases: ['sfdxurlfile'], + exclusive: ['sfdx-url-stdin'], + exactlyOne: ['sfdx-url-stdin'], }), 'set-default-dev-hub': Flags.boolean({ char: 'd', @@ -67,16 +75,20 @@ export default class LoginSfdxUrl extends AuthBaseCommand { public async run(): Promise { const { flags } = await this.parse(LoginSfdxUrl); - if (await this.shouldExitCommand(flags['no-prompt'])) return {}; - const authFile = flags['sfdx-url-file']; + if (await this.shouldExitCommand(flags['no-prompt'])) return {}; - const sfdxAuthUrl = authFile.endsWith('.json') ? await getUrlFromJson(authFile) : await readFile(authFile, 'utf8'); + const sfdxUrlFile = flags['sfdx-url-file']; + const sfdxAuthUrl = flags['sfdx-url-stdin'] + ? await read() + : sfdxUrlFile + ? sfdxUrlFile.endsWith('.json') + ? await getUrlFromJson(sfdxUrlFile) + : await readFile(sfdxUrlFile, 'utf8') + : null; if (!sfdxAuthUrl) { - throw new Error( - `Error getting the auth URL from file ${authFile}. Please ensure it meets the description shown in the documentation for this command.` - ); + throw messages.createError('errors.missingAuthUrl'); } const oauth2Options = AuthInfo.parseSfdxAuthUrl(sfdxAuthUrl); diff --git a/src/stdin.ts b/src/stdin.ts new file mode 100644 index 00000000..342d2273 --- /dev/null +++ b/src/stdin.ts @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2023, salesforce.com, inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ +export function read(): Promise { + return new Promise((resolve) => { + const stdin = process.openStdin(); + stdin.setEncoding('utf-8'); + + let data = ''; + stdin.on('data', (chunk) => { + data += chunk; + }); + + stdin.on('end', () => { + resolve(data); + }); + + if (stdin.isTTY) { + resolve(''); + } + }); +} diff --git a/test/commands/org/login/login.sfdx-url.nut.ts b/test/commands/org/login/login.sfdx-url.nut.ts index 63d2f6bd..63a99c41 100644 --- a/test/commands/org/login/login.sfdx-url.nut.ts +++ b/test/commands/org/login/login.sfdx-url.nut.ts @@ -6,6 +6,7 @@ */ import { execCmd, prepareForAuthUrl, TestSession } from '@salesforce/cli-plugins-testkit'; import { expect } from 'chai'; +import * as exec from 'shelljs'; import { Env } from '@salesforce/kit'; import { ensureString, getString } from '@salesforce/ts-types'; import { AuthFields } from '@salesforce/core'; @@ -56,4 +57,9 @@ describe('org:login:sfdx-url NUTs', () => { const output = getString(result, 'shellOutput.stdout'); expect(output).to.include(`Successfully authorized ${username} with org ID`); }); + + it('should authorize an org using sfdx-url (human readable)', () => { + const res = exec.cat(authUrl).exec('bin/dev org:login:sfdx-url -d --sfdx-url-stdin', { silent: true }); + expect(res.stdout).to.include(`Successfully authorized ${username} with org ID`); + }); }); diff --git a/test/commands/org/login/login.sfdx-url.test.ts b/test/commands/org/login/login.sfdx-url.test.ts index a6081303..7e188ac4 100644 --- a/test/commands/org/login/login.sfdx-url.test.ts +++ b/test/commands/org/login/login.sfdx-url.test.ts @@ -6,13 +6,17 @@ */ import * as fs from 'fs/promises'; -import { AuthFields, AuthInfo, Global, Mode } from '@salesforce/core'; +import { AuthFields, AuthInfo, Global, Mode, Messages } from '@salesforce/core'; import { MockTestOrgData, TestContext } from '@salesforce/core/lib/testSetup'; -import { expect } from 'chai'; +import { expect, assert } from 'chai'; import { Config } from '@oclif/core'; import { StubbedType, stubInterface } from '@salesforce/ts-sinon'; import { SfCommand } from '@salesforce/sf-plugins-core'; import LoginSfdxUrl from '../../../../src/commands/org/login/sfdx-url'; +import * as stdin from '../../../../src/stdin'; + +Messages.importMessagesDirectory(__dirname); +const messages = Messages.loadMessages('@salesforce/plugin-auth', 'sfdxurl.store'); interface Options { authInfoCreateFails?: boolean; @@ -91,7 +95,7 @@ describe('org:login:sfdx-url', () => { const response = await store.run(); expect.fail(`Should have thrown an error. Response: ${JSON.stringify(response)}`); } catch (e) { - expect((e as Error).message).to.includes('Error getting the auth URL from file'); + expect((e as Error).message).to.includes(messages.getMessage('errors.missingAuthUrl')); } }); @@ -213,4 +217,71 @@ describe('org:login:sfdx-url', () => { await store.run(); expect(authInfoStub.save.callCount).to.equal(1); }); + + it('should return auth fields when reading auth url from stdin', async () => { + await prepareStubs({ fileDoesNotExist: true }); + $$.SANDBOX.stub(stdin, 'read').resolves('force://PlatformCLI::CoffeeAndBacon@su0503.my.salesforce.com'); + const store = new LoginSfdxUrl(['--sfdx-url-stdin', '--json'], {} as Config); + const response = await store.run(); + expect(response.username).to.equal(testData.username); + }); + + it('should throw error when piping empty string to stdin', async () => { + await prepareStubs({ fileDoesNotExist: true }); + $$.SANDBOX.stub(stdin, 'read').resolves(''); + const store = new LoginSfdxUrl(['--sfdx-url-stdin', '--json'], {} as Config); + + try { + const response = await store.run(); + expect.fail(`Should have thrown an error. Response: ${JSON.stringify(response)}`); + } catch (e) { + expect((e as Error).message).to.includes(messages.getMessage('errors.missingAuthUrl')); + } + }); + + it('should throw error when not piping anything to stdin', async () => { + await prepareStubs({ fileDoesNotExist: true }); + $$.SANDBOX.stub(stdin, 'read').resolves(undefined); + const store = new LoginSfdxUrl(['--sfdx-url-stdin', '--json'], {} as Config); + + try { + const response = await store.run(); + expect.fail(`Should have thrown an error. Response: ${JSON.stringify(response)}`); + } catch (e) { + expect((e as Error).message).to.includes(messages.getMessage('errors.missingAuthUrl')); + } + }); + + it('should ignore stdin when not using --sfdx-url-stdin', async () => { + await prepareStubs(); + $$.SANDBOX.stub(stdin, 'read').resolves('force://foo::bar@su0503.my.salesforce.com'); + const parseSfdxAuthUrlSpy = $$.SANDBOX.spy(AuthInfo, 'parseSfdxAuthUrl'); + const store = new LoginSfdxUrl(['-f', keyPathTxt, '--json'], {} as Config); + await store.run(); + + assert.isTrue(parseSfdxAuthUrlSpy.calledWith('force://PlatformCLI::CoffeeAndBacon@su0503.my.salesforce.com')); + }); + + it('should throw error when passing both sfdx-url-stdin and sfdx-url-file', async () => { + const store = new LoginSfdxUrl(['--sfdx-url-stdin', '--sfdx-url-file', 'path/to/key.txt', '--json'], {} as Config); + + try { + const response = await store.run(); + expect.fail(`Should have thrown an error. Response: ${JSON.stringify(response)}`); + } catch (e) { + expect((e as Error).message).to.includes('--sfdx-url-file cannot also be provided when using --sfdx-url-stdin'); + } + }); + + it('should throw error when not passing sfdx-url-stdin and sfdx-url-file', async () => { + const store = new LoginSfdxUrl(['--json'], {} as Config); + + try { + const response = await store.run(); + expect.fail(`Should have thrown an error. Response: ${JSON.stringify(response)}`); + } catch (e) { + expect((e as Error).message).to.includes('Exactly one of the following must be provided: --sfdx-url-file'); + expect((e as Error).message).to.includes('Exactly one of the following must be provided: --sfdx-url-stdin'); + } + }); });