From d1e70f4365ae308834b761d53484dd3fa8bcd244 Mon Sep 17 00:00:00 2001 From: Steve Hetzel Date: Thu, 3 Oct 2024 14:56:21 -0600 Subject: [PATCH] fix: better scratch org create authentication and lint fixes --- src/config/ttlConfig.ts | 5 + src/org/authInfo.ts | 2 +- src/org/scratchOrgCache.ts | 2 + src/org/scratchOrgCreate.ts | 23 +++-- src/org/scratchOrgInfoApi.ts | 67 +++++++++---- test/unit/config/ttlConfig.test.ts | 32 ++++++ test/unit/org/scratchOrgCreate.test.ts | 70 +++++++++----- test/unit/org/scratchOrgInfoApi.test.ts | 123 ++++++++++++++++++++++-- 8 files changed, 266 insertions(+), 58 deletions(-) diff --git a/src/config/ttlConfig.ts b/src/config/ttlConfig.ts index ebe1e31e0c..7faa7c6446 100644 --- a/src/config/ttlConfig.ts +++ b/src/config/ttlConfig.ts @@ -44,6 +44,11 @@ export class TTLConfig extends C } protected async init(): Promise { + // Normally, this is done in super.init() but we don't call it to prevent + // redundant read() calls. + if (this.hasEncryption()) { + await this.initCrypto(); + } const contents = await this.read(this.options.throwOnNotFound); const date = new Date().getTime(); diff --git a/src/org/authInfo.ts b/src/org/authInfo.ts index df7cc5d5a5..15558cfc64 100644 --- a/src/org/authInfo.ts +++ b/src/org/authInfo.ts @@ -1115,7 +1115,7 @@ export class AuthInfo extends AsyncOptionalCreatable { // Exchange the auth code for an access token and refresh token. let authFields: TokenResponse; try { - this.logger.info(`Exchanging auth code for access token using loginUrl: ${options.loginUrl}`); + this.logger.debug(`Exchanging auth code for access token using loginUrl: ${options.loginUrl}`); authFields = await oauth2.requestToken(ensure(options.authCode)); } catch (err) { const msg = err instanceof Error ? `${err.name}::${err.message}` : typeof err === 'string' ? err : 'UNKNOWN'; diff --git a/src/org/scratchOrgCache.ts b/src/org/scratchOrgCache.ts index 21b5d09770..516b937782 100644 --- a/src/org/scratchOrgCache.ts +++ b/src/org/scratchOrgCache.ts @@ -24,6 +24,7 @@ export type CachedOptions = { }; export class ScratchOrgCache extends TTLConfig { + protected static readonly encryptedKeys = ['clientSecret']; public static getFileName(): string { return 'scratch-create-cache.json'; } @@ -34,6 +35,7 @@ export class ScratchOrgCache extends TTLConfig isState: true, filename: ScratchOrgCache.getFileName(), stateFolder: Global.SF_STATE_FOLDER, + encryptedKeys: ScratchOrgCache.encryptedKeys, ttl: Duration.days(1), }; } diff --git a/src/org/scratchOrgCreate.ts b/src/org/scratchOrgCreate.ts index 4d41166a64..aaeae21455 100644 --- a/src/org/scratchOrgCreate.ts +++ b/src/org/scratchOrgCreate.ts @@ -111,7 +111,7 @@ export const scratchOrgResume = async (jobId: string): Promise => { try { const project = await SfProject.resolve(); const projectJson = await project.resolveProjectConfig(); - return projectJson.signupTargetLoginUrl as string; + const signupTargetLoginUrl = projectJson.signupTargetLoginUrl; + if (signupTargetLoginUrl) { + Logger.childFromRoot('getSignupTargetLoginUrl').debug( + `Found signupTargetLoginUrl in project file: ${signupTargetLoginUrl as string}` + ); + return signupTargetLoginUrl as string; + } } catch { // a project isn't required for org:create } diff --git a/src/org/scratchOrgInfoApi.ts b/src/org/scratchOrgInfoApi.ts index 1ae21ef00a..662cad741f 100644 --- a/src/org/scratchOrgInfoApi.ts +++ b/src/org/scratchOrgInfoApi.ts @@ -5,8 +5,8 @@ * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ +import { inspect } from 'node:util'; import { env, Duration, upperFirst, omit } from '@salesforce/kit'; - import { AnyJson } from '@salesforce/ts-types'; import { OAuth2Config, SaveResult } from '@jsforce/jsforce-node'; import { retryDecorator, RetryError } from 'ts-retry-promise'; @@ -43,13 +43,13 @@ const errorCodes = Messages.loadMessages('@salesforce/core', 'scratchOrgErrorCod * * @param scratchOrgInfoComplete The completed ScratchOrgInfo * @param hubOrgLoginUrl the hun org login url - * @param signupTargetLoginUrlConfig the login url + * @param signupTargetLoginUrl the login url * @returns {string} */ const getOrgInstanceAuthority = function ( scratchOrgInfoComplete: ScratchOrgInfo, hubOrgLoginUrl: string, - signupTargetLoginUrlConfig?: string + signupTargetLoginUrl?: string ): string { const createdOrgInstance = scratchOrgInfoComplete.SignupInstance; @@ -65,7 +65,7 @@ const getOrgInstanceAuthority = function ( altUrl = scratchOrgInfoComplete.LoginUrl; } - return signupTargetLoginUrlConfig ?? altUrl; + return signupTargetLoginUrl ?? altUrl; }; /** @@ -79,7 +79,7 @@ const buildOAuth2Options = async (options: { scratchOrgInfoComplete: ScratchOrgInfo; clientSecret?: string; retry?: number; - signupTargetLoginUrlConfig?: string; + signupTargetLoginUrl?: string; }): Promise<{ options: OAuth2Config; retries: number; @@ -92,11 +92,12 @@ const buildOAuth2Options = async (options: { loginUrl: getOrgInstanceAuthority( options.scratchOrgInfoComplete, options.hubOrg.getField(Org.Fields.LOGIN_URL), - options.signupTargetLoginUrlConfig + options.signupTargetLoginUrl ), }; logger.debug(`isJwtFlow: ${isJwtFlow}`); + logger.debug(`using resolved loginUrl: ${oauth2Options.loginUrl}`); if (isJwtFlow && !process.env.SFDX_CLIENT_SECRET) { oauth2Options.privateKeyFile = options.hubOrg.getConnection().getAuthInfoFields().privateKey; @@ -190,7 +191,7 @@ export const queryScratchOrgInfo = async (hubOrg: Org, id: string): Promise} @@ -199,10 +200,10 @@ export const authorizeScratchOrg = async (options: { scratchOrgInfoComplete: ScratchOrgInfo; hubOrg: Org; clientSecret?: string; - signupTargetLoginUrlConfig?: string; + signupTargetLoginUrl?: string; retry?: number; }): Promise => { - const { scratchOrgInfoComplete, hubOrg, clientSecret, signupTargetLoginUrlConfig, retry } = options; + const { scratchOrgInfoComplete, hubOrg, clientSecret, signupTargetLoginUrl, retry } = options; await emit({ stage: 'authenticate', scratchOrgInfo: scratchOrgInfoComplete }); const logger = await Logger.child('authorizeScratchOrg'); logger.debug(`scratchOrgInfoComplete: ${JSON.stringify(scratchOrgInfoComplete, null, 4)}`); @@ -217,17 +218,47 @@ export const authorizeScratchOrg = async (options: { clientSecret, scratchOrgInfoComplete, retry, - signupTargetLoginUrlConfig, + signupTargetLoginUrl, }); - const authInfo = await getAuthInfo({ - hubOrg, - username: scratchOrgInfoComplete.SignupUsername, - oauth2Options: oAuth2Options.options, - retries: oAuth2Options.retries, - timeout: oAuth2Options.timeout, - delay: oAuth2Options.delay, - }); + let authInfo: AuthInfo; + + try { + // This will use the authCode from the scratch org signup to exchange for an auth token via OAuth. + authInfo = await getAuthInfo({ + hubOrg, + username: scratchOrgInfoComplete.SignupUsername, + oauth2Options: oAuth2Options.options, + retries: oAuth2Options.retries, + timeout: oAuth2Options.timeout, + delay: oAuth2Options.delay, + }); + } catch (err1) { + // If we didn't already try authenticating with the LoginUrl from ScratchOrgInfo object, + // try the oauth flow again using it now. + if (scratchOrgInfoComplete.LoginUrl && oAuth2Options.options.loginUrl !== scratchOrgInfoComplete.LoginUrl) { + logger.debug( + `Auth failed with loginUrl ${oAuth2Options.options.loginUrl} so trying with ${scratchOrgInfoComplete.LoginUrl}` + ); + oAuth2Options.options = { ...oAuth2Options.options, ...{ loginUrl: scratchOrgInfoComplete.LoginUrl } }; + try { + authInfo = await getAuthInfo({ + hubOrg, + username: scratchOrgInfoComplete.SignupUsername, + oauth2Options: oAuth2Options.options, + retries: oAuth2Options.retries, + timeout: oAuth2Options.timeout, + delay: oAuth2Options.delay, + }); + } catch (err2) { + // Log this error but throw the original error + logger.debug(`Auth failed with ScratchOrgInfo.LoginUrl ${scratchOrgInfoComplete.LoginUrl}\n${inspect(err2)}`); + throw err1; + } + } else { + throw err1; + } + } await authInfo.save({ devHubUsername: hubOrg.getUsername(), diff --git a/test/unit/config/ttlConfig.test.ts b/test/unit/config/ttlConfig.test.ts index ccfa870f17..79e4533787 100644 --- a/test/unit/config/ttlConfig.test.ts +++ b/test/unit/config/ttlConfig.test.ts @@ -11,6 +11,7 @@ import { Duration, sleep } from '@salesforce/kit'; import { TTLConfig } from '../../../src/config/ttlConfig'; import { TestContext } from '../../../src/testSetup'; import { Global } from '../../../src/global'; +import { ScratchOrgCache } from '../../../src/org/scratchOrgCache'; describe('TTLConfig', () => { const $$ = new TestContext(); @@ -38,6 +39,37 @@ describe('TTLConfig', () => { } } + class TestScratchOrgCache extends ScratchOrgCache { + public hasCryptoInitialized(): boolean { + return !!this.crypto; + } + public shouldEncryptKey(key: string): boolean { + return !!this.isCryptoKey(key); + } + } + + describe('ScratchOrgCache', () => { + describe('set', () => { + it('should timestamp every entry', async () => { + const config = await TestScratchOrgCache.create(); + config.set('123', { hubUsername: 'foo' }); + const entry = config.get('123'); + expect(entry).to.have.property('timestamp'); + expect(config.hasCryptoInitialized()).to.be.true; + }); + it('should encrypt clientSecret', async () => { + const clientSecret = '4947FFFDE29D89CFC3F'; + const config = await TestScratchOrgCache.create(); + config.set('123', { clientSecret }); + expect(config.shouldEncryptKey('clientSecret')).to.be.true; + const nonDecryptedEntry = config.get('123'); + expect(nonDecryptedEntry).to.have.property('clientSecret').and.not.equal(clientSecret); + const decryptedEntry = config.get('123', true); + expect(decryptedEntry).to.have.property('clientSecret', clientSecret); + }); + }); + }); + describe('set', () => { it('should timestamp every entry', async () => { const config = await TestConfig.create(); diff --git a/test/unit/org/scratchOrgCreate.test.ts b/test/unit/org/scratchOrgCreate.test.ts index 574da44556..df2a062e30 100644 --- a/test/unit/org/scratchOrgCreate.test.ts +++ b/test/unit/org/scratchOrgCreate.test.ts @@ -24,12 +24,16 @@ describe('scratchOrgCreate', () => { const authInfoStub = sinon.createStubInstance(AuthInfo); const sfProjectJsonStub = sinon.createStubInstance(SfProjectJson); const cacheStub = sinon.createStubInstance(ScratchOrgCache); + let authInfoCreateStub: sinon.SinonStub; + let sfProjectResolveStub: sinon.SinonStub; + const signupTargetLoginUrl = 'https://signup-target-login-url.salesforce.com'; const scratchOrgInfoId = '2SR3u0000008gBEGAY'; const username = 'PlatformCLI'; - const retrieve = { + const scratchOrgInfo = { Status: 'Active', SignupUsername: username, Id: scratchOrgInfoId, + SignupInstance: 'CS51', }; const authFields = { instanceUrl: 'https://salesforce.com', @@ -38,14 +42,14 @@ describe('scratchOrgCreate', () => { beforeEach(() => { sandbox.stub(ScratchOrgCache, 'create').resolves(cacheStub); sandbox.stub(Org, 'create').resolves(hubOrgStub); - sandbox.stub(AuthInfo, 'create').resolves(authInfoStub); + authInfoCreateStub = sandbox.stub(AuthInfo, 'create'); + authInfoCreateStub.resolves(authInfoStub); sfProjectJsonStub.getPackageDirectories.resolves([ { path: 'foo', package: 'fooPkgName', versionNumber: '4.7.0.NEXT', ancestorId: packageId }, ]); - sandbox.stub(SfProject, 'resolve').resolves({ - resolveProjectConfig: sandbox.stub().resolves({ - signupTargetLoginUrl: 'https://salesforce.com', - }), + sfProjectResolveStub = sandbox.stub(SfProject, 'resolve'); + sfProjectResolveStub.resolves({ + resolveProjectConfig: sandbox.stub().resolves({ signupTargetLoginUrl }), } as unknown as SfProject); hubOrgStub.isDevHubOrg.returns(true); hubOrgStub.determineIfDevHubOrg.withArgs(true).resolves(); @@ -77,7 +81,7 @@ describe('scratchOrgCreate', () => { create: sinon.stub().resolves({ id: scratchOrgInfoId, }), - retrieve: sinon.stub().withArgs(scratchOrgInfoId).resolves(retrieve), + retrieve: sinon.stub().withArgs(scratchOrgInfoId).resolves(scratchOrgInfo), }), singleRecordQuery: sandbox .stub() @@ -106,26 +110,21 @@ describe('scratchOrgCreate', () => { } as ScratchOrgCreateOptions; const scratchOrgCreateResult = await scratchOrgCreate(scratchOrgCreateOptions); expect(scratchOrgCreateResult).to.deep.equal({ - authFields: { - instanceUrl: 'https://salesforce.com', - orgId: '00D0R000000eJDy', - }, + authFields, authInfo: {}, - scratchOrgInfo: retrieve, + scratchOrgInfo, username, warnings: [], }); expect(scratchOrgCreateResult).to.deep.equal({ username, - scratchOrgInfo: { - Id: scratchOrgInfoId, - SignupUsername: 'PlatformCLI', - Status: 'Active', - }, + scratchOrgInfo, authInfo: {}, authFields, warnings: [], }); + const authInfoOptions = authInfoCreateStub.firstCall.args[0] as AuthInfo.Options; + expect(authInfoOptions.oauth2Options).to.have.property('loginUrl', signupTargetLoginUrl); }); it('exits early for wait 0', async () => { @@ -136,13 +135,13 @@ describe('scratchOrgCreate', () => { const scratchOrgCreateResult = await scratchOrgCreate(scratchOrgCreateOptions); // early return does not have the optional auth stuff expect(scratchOrgCreateResult).to.deep.equal({ - scratchOrgInfo: retrieve, + scratchOrgInfo, username, warnings: [], }); }); - it('resumes', async () => { + it('resumes with signupTargetLoginUrl override', async () => { cacheStub.get.withArgs(scratchOrgInfoId).returns({ hubUsername: 'PlatformCLI', }); @@ -151,14 +150,37 @@ describe('scratchOrgCreate', () => { // resume has all the data it originally would have expect(scratchOrgCreateResult).to.deep.equal({ username, - scratchOrgInfo: { - Id: scratchOrgInfoId, - SignupUsername: 'PlatformCLI', - Status: 'Active', - }, + scratchOrgInfo, + authInfo: {}, + authFields, + warnings: [], + }); + const authInfoOptions = authInfoCreateStub.firstCall.args[0] as AuthInfo.Options; + expect(authInfoOptions.oauth2Options).to.have.property('loginUrl', signupTargetLoginUrl); + }); + + it('resumes without signupTargetLoginUrl override', async () => { + cacheStub.get.withArgs(scratchOrgInfoId).returns({ + hubUsername: 'PlatformCLI', + }); + cacheStub.has.withArgs(scratchOrgInfoId).returns(true); + sfProjectResolveStub.restore(); + sandbox.stub(SfProject, 'resolve').resolves({ + resolveProjectConfig: sandbox.stub().resolves({}), + } as unknown as SfProject); + const scratchOrgCreateResult = await scratchOrgResume(scratchOrgInfoId); + // resume has all the data it originally would have + expect(scratchOrgCreateResult).to.deep.equal({ + username, + scratchOrgInfo, authInfo: {}, authFields, warnings: [], }); + const authInfoOptions = authInfoCreateStub.firstCall.args[0] as AuthInfo.Options; + expect(authInfoOptions.oauth2Options).to.have.property( + 'loginUrl', + `https://${scratchOrgInfo.SignupInstance}.salesforce.com` + ); }); }); diff --git a/test/unit/org/scratchOrgInfoApi.test.ts b/test/unit/org/scratchOrgInfoApi.test.ts index 7b15c0fa28..7c6a4e9a4e 100644 --- a/test/unit/org/scratchOrgInfoApi.test.ts +++ b/test/unit/org/scratchOrgInfoApi.test.ts @@ -35,14 +35,14 @@ const errorCodesMessages = Messages.loadMessages('@salesforce/core', 'scratchOrg const scratchOrgInfoId = '2SRK0000001QZxF'; const TEMPLATE_SCRATCH_ORG_INFO: ScratchOrgInfo = { - LoginUrl: 'https://login.salesforce.com', + LoginUrl: 'https://scratch-org-info-login-url.salesforce.com', Snapshot: '1234', AuthCode: '1234', Status: 'New', SignupEmail: 'sfdx-cli@salesforce.com', SignupUsername: 'sfdx-cli', Username: 'sfdx-cli', - SignupInstance: 'http://salesforce.com', + SignupInstance: 'CS51', }; describe('requestScratchOrgCreation', () => { @@ -466,7 +466,7 @@ describe('authorizeScratchOrg', () => { sandbox.restore(); }); - it('authorizeScratchOrg', async () => { + it('authorizeScratchOrg basic', async () => { hubOrgStub.isDevHubOrg.returns(true); const result = await authorizeScratchOrg({ scratchOrgInfoComplete: TEMPLATE_SCRATCH_ORG_INFO, @@ -474,21 +474,40 @@ describe('authorizeScratchOrg', () => { }); expect(result).to.be.equal(authInfo); + expect(authInfoStub.firstCall.args[0]).to.deep.equal({ + username: TEMPLATE_SCRATCH_ORG_INFO.SignupUsername, + parentUsername: username, + oauth2Options: { + loginUrl: `https://${TEMPLATE_SCRATCH_ORG_INFO.SignupInstance}.salesforce.com`, + privateKeyFile: privateKey, + }, + }); }); - it('authorizeScratchOrg with signupTargetLoginUrlConfig', async () => { + it('authorizeScratchOrg with signupTargetLoginUrl', async () => { + const signupTargetLoginUrl = 'http://signup-target-login-url.salesforce.com'; hubOrgStub.isDevHubOrg.returns(true); const result = await authorizeScratchOrg({ scratchOrgInfoComplete: TEMPLATE_SCRATCH_ORG_INFO, hubOrg: hubOrgStub, - signupTargetLoginUrlConfig: 'http://salesforce.com', + signupTargetLoginUrl, }); expect(result).to.be.equal(authInfo); + expect(authInfoStub.firstCall.args[0]).to.deep.equal({ + username: TEMPLATE_SCRATCH_ORG_INFO.SignupUsername, + parentUsername: username, + oauth2Options: { + loginUrl: signupTargetLoginUrl, + privateKeyFile: privateKey, + }, + }); }); - it('authorizeScratchOrg with SignupInstance', async () => { + it('authorizeScratchOrg with SignupInstance = utf8', async () => { + const hubOrgLoginUrl = 'https://hub-org-login-url.salesforce.com'; hubOrgStub.isDevHubOrg.returns(true); + hubOrgStub.getField.withArgs(Org.Fields.LOGIN_URL).returns(hubOrgLoginUrl); const scratchOrgInfoComplete = Object.assign({}, TEMPLATE_SCRATCH_ORG_INFO, { SignupInstance: 'utf8', }); @@ -498,12 +517,20 @@ describe('authorizeScratchOrg', () => { }); expect(result).to.be.equal(authInfo); + expect(authInfoStub.firstCall.args[0]).to.deep.equal({ + username: TEMPLATE_SCRATCH_ORG_INFO.SignupUsername, + parentUsername: username, + oauth2Options: { + loginUrl: hubOrgLoginUrl, + privateKeyFile: privateKey, + }, + }); }); it('authorizeScratchOrg with SignupInstance ends with s', async () => { hubOrgStub.isDevHubOrg.returns(true); const scratchOrgInfoComplete = Object.assign({}, TEMPLATE_SCRATCH_ORG_INFO, { - SignupInstance: 's', + SignupInstance: 'BEES', }); const result = await authorizeScratchOrg({ scratchOrgInfoComplete, @@ -511,11 +538,20 @@ describe('authorizeScratchOrg', () => { }); expect(result).to.be.equal(authInfo); + expect(authInfoStub.firstCall.args[0]).to.deep.equal({ + username: TEMPLATE_SCRATCH_ORG_INFO.SignupUsername, + parentUsername: username, + oauth2Options: { + loginUrl: TEMPLATE_SCRATCH_ORG_INFO.LoginUrl, + privateKeyFile: privateKey, + }, + }); }); it('authorizeScratchOrg isJwtFlow with SFDX_CLIENT_SECRET', async () => { hubOrgStub.isDevHubOrg.returns(true); - env.setString('SFDX_CLIENT_SECRET', '1234'); + const clientSecret = '1234-client-secret'; + env.setString('SFDX_CLIENT_SECRET', clientSecret); const result = await authorizeScratchOrg({ scratchOrgInfoComplete: TEMPLATE_SCRATCH_ORG_INFO, @@ -523,10 +559,21 @@ describe('authorizeScratchOrg', () => { }); expect(result).to.be.equal(authInfo); + expect(authInfoStub.firstCall.args[0]).to.deep.equal({ + username: TEMPLATE_SCRATCH_ORG_INFO.SignupUsername, + parentUsername: username, + oauth2Options: { + loginUrl: `https://${TEMPLATE_SCRATCH_ORG_INFO.SignupInstance}.salesforce.com`, + clientSecret, + redirectUri: undefined, + authCode: TEMPLATE_SCRATCH_ORG_INFO.AuthCode, + }, + }); }); it('authorizeScratchOrg not isJwtFlow and clientSecret', async () => { hubOrgStub.isDevHubOrg.returns(true); + const clientSecret = '1234'; connectionStub.getAuthInfoFields.returns({ privateKey: undefined, }); @@ -534,10 +581,20 @@ describe('authorizeScratchOrg', () => { const result = await authorizeScratchOrg({ scratchOrgInfoComplete: TEMPLATE_SCRATCH_ORG_INFO, hubOrg: hubOrgStub, - clientSecret: '1234', + clientSecret, }); expect(result).to.be.equal(authInfo); + expect(authInfoStub.firstCall.args[0]).to.deep.equal({ + username: TEMPLATE_SCRATCH_ORG_INFO.SignupUsername, + parentUsername: username, + oauth2Options: { + loginUrl: `https://${TEMPLATE_SCRATCH_ORG_INFO.SignupInstance}.salesforce.com`, + clientSecret, + redirectUri: undefined, + authCode: TEMPLATE_SCRATCH_ORG_INFO.AuthCode, + }, + }); }); it('authorizeScratchOrg not isJwtFlow no clientSecret', async () => { @@ -552,6 +609,15 @@ describe('authorizeScratchOrg', () => { }); expect(result).to.be.equal(authInfo); + expect(authInfoStub.firstCall.args[0]).to.deep.equal({ + username: TEMPLATE_SCRATCH_ORG_INFO.SignupUsername, + parentUsername: username, + oauth2Options: { + loginUrl: `https://${TEMPLATE_SCRATCH_ORG_INFO.SignupInstance}.salesforce.com`, + redirectUri: undefined, + authCode: TEMPLATE_SCRATCH_ORG_INFO.AuthCode, + }, + }); }); it('authorizeScratchOrg not DevHub with retry', async () => { @@ -565,6 +631,45 @@ describe('authorizeScratchOrg', () => { }); expect(result).to.be.equal(authInfo); + expect(authInfoStub.firstCall.args[0]).to.deep.equal({ + username: TEMPLATE_SCRATCH_ORG_INFO.SignupUsername, + parentUsername: username, + oauth2Options: { + loginUrl: `https://${TEMPLATE_SCRATCH_ORG_INFO.SignupInstance}.salesforce.com`, + privateKeyFile: privateKey, + }, + }); + }); + + it('authorizeScratchOrg retrying with ScratchOrgInfo.LoginUrl', async () => { + hubOrgStub.isDevHubOrg.returns(true); + authInfoStub.restore(); + authInfoStub = stubMethod(sandbox, AuthInfo, 'create'); + authInfoStub.onFirstCall().throws(Error('failed oauth using SignupInstance LoginUrl')); + authInfoStub.onSecondCall().resolves(authInfo); + const result = await authorizeScratchOrg({ + scratchOrgInfoComplete: TEMPLATE_SCRATCH_ORG_INFO, + hubOrg: hubOrgStub, + }); + + expect(result).to.be.equal(authInfo); + expect(authInfoStub.callCount).to.equal(2, 'AuthInfo.create() should have been called twice'); + expect(authInfoStub.firstCall.args[0]).to.deep.equal({ + username: TEMPLATE_SCRATCH_ORG_INFO.SignupUsername, + parentUsername: username, + oauth2Options: { + loginUrl: `https://${TEMPLATE_SCRATCH_ORG_INFO.SignupInstance}.salesforce.com`, + privateKeyFile: privateKey, + }, + }); + expect(authInfoStub.secondCall.args[0]).to.deep.equal({ + username: TEMPLATE_SCRATCH_ORG_INFO.SignupUsername, + parentUsername: username, + oauth2Options: { + loginUrl: TEMPLATE_SCRATCH_ORG_INFO.LoginUrl, + privateKeyFile: privateKey, + }, + }); }); it('authorizeScratchOrg retry fails and timeouts', async () => {