From a79117e2b83ef934addfd06ba74c06e32d66d056 Mon Sep 17 00:00:00 2001 From: Steve Hetzel Date: Thu, 15 Jun 2023 10:14:53 -0600 Subject: [PATCH 1/4] fix: identify sandboxes during authentication --- src/org/authInfo.ts | 49 +++++++++++++++++++++++++++++--- src/org/org.ts | 15 +++++++--- test/unit/org/authInfoTest.ts | 53 +++++++++++++++++++++++++++++++---- 3 files changed, 103 insertions(+), 14 deletions(-) diff --git a/src/org/authInfo.ts b/src/org/authInfo.ts index 0dd278cc36..091040d848 100644 --- a/src/org/authInfo.ts +++ b/src/org/authInfo.ts @@ -38,7 +38,7 @@ import { Messages } from '../messages'; import { getLoginAudienceCombos, SfdcUrl } from '../util/sfdcUrl'; import { Connection, SFDX_HTTP_HEADERS } from './connection'; import { OrgConfigProperties } from './orgConfigProperties'; -import { Org } from './org'; +import { Org, SandboxFields } from './org'; Messages.importMessagesDirectory(__dirname); const messages = Messages.loadMessages('@salesforce/core', 'core'); @@ -373,7 +373,7 @@ export class AuthInfo extends AsyncOptionalCreatable { /** * Given a set of decrypted fields and an authInfo, determine if the org belongs to an available - * dev hub. + * dev hub, or if the org is a sandbox of another CLI authed production org. * * @param fields * @param orgAuthInfo @@ -387,7 +387,7 @@ export class AuthInfo extends AsyncOptionalCreatable { // return if we already know the hub org we know it is a devhub or prod-like or no orgId present if (fields.isDevHub || fields.devHubUsername || !fields.orgId) return; - logger.debug('getting devHubs'); + logger.debug('getting devHubs to identify scratch orgs and sandboxes'); // TODO: return if url is not sandbox-like to avoid constantly asking about production orgs // TODO: someday we make this easier by asking the org if it is a scratch org @@ -419,7 +419,48 @@ export class AuthInfo extends AsyncOptionalCreatable { logger.debug(`error updating auth file for ${orgAuthInfo.getUsername()}`, error); } } catch (error) { - logger.error(`Error connecting to devhub ${hubAuthInfo.username}`, error); + if (error instanceof Error && error.name === 'SingleRecordQuery_NoRecords' && fields.orgId) { + // Not a scratch org owned by this hub. Check if it's a sandbox. + try { + const prodOrg = await Org.create({ aliasOrUsername: hubAuthInfo.username }); + const sbxProcess = await prodOrg.querySandboxProcessByOrgId(fields.orgId); + logger.debug(`${fields.orgId} is a sandbox of ${hubAuthInfo.username}`); + + try { + await orgAuthInfo.save({ + ...fields, + isScratch: false, + isSandbox: true, + }); + } catch (err) { + logger.debug(`error updating auth file for: ${orgAuthInfo.getUsername()}`, err); + } + + try { + // set the sandbox config value + const sfSandbox = { + sandboxUsername: fields.username, + sandboxOrgId: fields.orgId, + prodOrgUsername: hubAuthInfo.username, + sandboxName: sbxProcess.SandboxName, + sandboxProcessId: sbxProcess.Id, + sandboxInfoId: sbxProcess.SandboxInfoId, + timestamp: new Date().toISOString(), + } as SandboxFields; + + const stateAggregator = await StateAggregator.getInstance(); + stateAggregator.sandboxes.set(fields.orgId, sfSandbox); + logger.debug(`writing sandbox auth file for: ${orgAuthInfo.getUsername()} with ID: ${fields.orgId}`); + await stateAggregator.sandboxes.write(fields.orgId); + } catch (e) { + logger.debug(`error writing sandbox auth file for: ${orgAuthInfo.getUsername()}`, e); + } + } catch (err) { + logger.debug(`${fields.orgId} is not a sandbox of ${hubAuthInfo.username}`); + } + } else { + logger.error(`Error connecting to devhub ${hubAuthInfo.username}`, error); + } } }) ); diff --git a/src/org/org.ts b/src/org/org.ts index 713c780905..1e407946e3 100644 --- a/src/org/org.ts +++ b/src/org/org.ts @@ -908,7 +908,6 @@ export class Org extends AsyncOptionalCreatable { * query SandboxProcess via sandbox name * * @param name SandboxName to query for - * @private */ public async querySandboxProcessBySandboxName(name: string): Promise { return this.querySandboxProcess(`SandboxName='${name}'`); @@ -918,7 +917,6 @@ export class Org extends AsyncOptionalCreatable { * query SandboxProcess via SandboxInfoId * * @param id SandboxInfoId to query for - * @private */ public async querySandboxProcessBySandboxInfoId(id: string): Promise { return this.querySandboxProcess(`SandboxInfoId='${id}'`); @@ -928,12 +926,21 @@ export class Org extends AsyncOptionalCreatable { * query SandboxProcess via Id * * @param id SandboxProcessId to query for - * @private */ public async querySandboxProcessById(id: string): Promise { return this.querySandboxProcess(`Id='${id}'`); } + /** + * query SandboxProcess via SandboxOrganization (sandbox Org ID) + * + * @param sandboxOrgId SandboxOrganization ID to query for + */ + public async querySandboxProcessByOrgId(sandboxOrgId: string): Promise { + // Must query with a 15 character Org ID + return this.querySandboxProcess(`SandboxOrganization='${trimTo15(sandboxOrgId)}'`); + } + /** * Initialize async components. */ @@ -1291,7 +1298,7 @@ export class Org extends AsyncOptionalCreatable { // save auth info for new sandbox await authInfo.save(); - const sandboxOrgId = authInfo.getFields().orgId as string; + const sandboxOrgId = authInfo.getFields().orgId; if (!sandboxOrgId) { throw messages.createError('AuthInfoOrgIdUndefined'); diff --git a/test/unit/org/authInfoTest.ts b/test/unit/org/authInfoTest.ts index 5a027cdfa7..218eac4611 100644 --- a/test/unit/org/authInfoTest.ts +++ b/test/unit/org/authInfoTest.ts @@ -20,10 +20,10 @@ import { Transport } from 'jsforce/lib/transport'; import { JwtOAuth2Config, OAuth2 } from 'jsforce'; import { SinonSpy, SinonStub } from 'sinon'; -import { AuthFields, AuthInfo } from '../../../src/org'; +import { AuthFields, AuthInfo, Org } from '../../../src/org'; import { MockTestOrgData, shouldThrow, shouldThrowSync, TestContext } from '../../../src/testSetup'; import { OrgConfigProperties } from '../../../src/org/orgConfigProperties'; -import { AliasAccessor, OrgAccessor } from '../../../src/stateAggregator'; +import { AliasAccessor, OrgAccessor, StateAggregator } from '../../../src/stateAggregator'; import { Crypto } from '../../../src/crypto/crypto'; import { Config } from '../../../src/config/config'; import { SfdcUrl } from '../../../src/util/sfdcUrl'; @@ -1779,7 +1779,7 @@ describe('AuthInfo', () => { user1 = new MockTestOrgData(); }); - it('should not update org - no dev hubs', async () => { + it('should not update auth file - no dev hubs', async () => { await $$.stubAuths(adminTestData, user1); const authInfo = await AuthInfo.create({ @@ -1796,7 +1796,7 @@ describe('AuthInfo', () => { expect(authInfoSaveStub.callCount).to.be.equal(0); }); - it('should not update org - state already known', async () => { + it('should not update auth file - state already known', async () => { adminTestData.makeDevHub(); user1.isScratchOrg = true; user1.devHubUsername = adminTestData.username; @@ -1817,7 +1817,7 @@ describe('AuthInfo', () => { expect(authInfoSaveStub.callCount).to.be.equal(0); }); - it('should not update org - no fields.orgId', async () => { + it('should not update auth file - no fields.orgId', async () => { adminTestData.makeDevHub(); user1.isScratchOrg = true; // @ts-expect-error - operand must be optional @@ -1839,7 +1839,7 @@ describe('AuthInfo', () => { expect(authInfoSaveStub.callCount).to.be.equal(0); }); - it('should update org', async () => { + it('should update auth file as a scratch org', async () => { adminTestData.makeDevHub(); await $$.stubAuths(adminTestData, user1); @@ -1859,6 +1859,47 @@ describe('AuthInfo', () => { expect(queryScratchOrgStub.callCount).to.be.equal(1); expect(authInfoSaveStub.callCount).to.be.equal(1); }); + + it('should update auth file as a sandbox', async () => { + adminTestData.makeDevHub(); + + await $$.stubAuths(adminTestData, user1); + + const authInfo = await AuthInfo.create({ + username: user1.username, + }); + + const stateAggregator = await StateAggregator.getInstance(); + const stateAggregatorStub = stubMethod($$.SANDBOX, StateAggregator, 'getInstance'); + const sandboxSetStub = stubMethod($$.SANDBOX, stateAggregator.sandboxes, 'set'); + const sandboxWriteStub = stubMethod($$.SANDBOX, stateAggregator.sandboxes, 'write'); + stateAggregatorStub.resolves(stateAggregator); + const authInfoSaveStub = stubMethod($$.SANDBOX, AuthInfo.prototype, 'save').resolves(); + const queryScratchOrgStub = stubMethod($$.SANDBOX, AuthInfo, 'queryScratchOrg'); + const queryScratchOrgError = new Error('not a scratch org'); + queryScratchOrgError.name = 'SingleRecordQuery_NoRecords'; + queryScratchOrgStub.throws(queryScratchOrgError); + const sbxQueryStub = stubMethod($$.SANDBOX, Org.prototype, 'querySandboxProcessByOrgId'); + sbxQueryStub.resolves({ + Id: '0GRB0000000L0ZVOA0', + Status: 'Completed', + SandboxName: 'TestSandbox', + SandboxInfoId: '0GQB0000000PCOdOAO', + LicenseType: 'DEVELOPER', + CreatedDate: '2021-01-22T22:49:52.000+0000', + }); + + await AuthInfo.identifyPossibleScratchOrgs(authInfo.getFields(), authInfo); + + expect(authInfoSaveStub.calledTwice).to.be.true; + expect(authInfoSaveStub.secondCall.args[0]).to.have.property('isSandbox', true); + expect(authInfoSaveStub.secondCall.args[0]).to.have.property('isScratch', false); + expect(sandboxSetStub.calledOnce).to.be.true; + expect(sandboxSetStub.firstCall.args[0]).to.equal(authInfo.getFields().orgId); + expect(sandboxSetStub.firstCall.args[1]).to.have.property('prodOrgUsername', adminTestData.username); + expect(sandboxWriteStub.calledOnce).to.be.true; + expect(sandboxWriteStub.firstCall.args[0]).to.equal(authInfo.getFields().orgId); + }); }); describe('determineIfDevHub', () => { From 57c0bae9098f5d750a8ee4d90bc8bfa30a88f38b Mon Sep 17 00:00:00 2001 From: Steve Hetzel Date: Tue, 1 Aug 2023 11:46:07 -0600 Subject: [PATCH 2/4] fix: check devhubs and non-scratch orgs and sandboxes --- src/org/authInfo.ts | 116 +++++++++++++++++++++------------- test/unit/org/authInfoTest.ts | 63 ++++++++++++++++-- 2 files changed, 131 insertions(+), 48 deletions(-) diff --git a/src/org/authInfo.ts b/src/org/authInfo.ts index 81cc4a9bbb..005b2dc1e0 100644 --- a/src/org/authInfo.ts +++ b/src/org/authInfo.ts @@ -392,21 +392,29 @@ export class AuthInfo extends AsyncOptionalCreatable { // authInfo before it is necessary. const logger = await Logger.child('Common', { tag: 'identifyPossibleScratchOrgs' }); - // return if we already know the hub org we know it is a devhub or prod-like or no orgId present + // return if we already know the hub org, we know it is a devhub or prod-like, or no orgId present if (fields.isDevHub || fields.devHubUsername || !fields.orgId) return; - logger.debug('getting devHubs to identify scratch orgs and sandboxes'); + logger.debug('getting devHubs and prod orgs to identify scratch orgs and sandboxes'); // TODO: return if url is not sandbox-like to avoid constantly asking about production orgs // TODO: someday we make this easier by asking the org if it is a scratch org const hubAuthInfos = await AuthInfo.getDevHubAuthInfos(); + // Get a list of org auths that are known not to be devhubs, scratch orgs, or sandboxes. + const possibleProdOrgs = await AuthInfo.listAllAuthorizations( + (orgAuth) => orgAuth && !orgAuth.isDevHub && !orgAuth.isScratchOrg && !orgAuth.isSandbox + ); + logger.debug(`found ${hubAuthInfos.length} DevHubs`); - if (hubAuthInfos.length === 0) return; + logger.debug(`found ${possibleProdOrgs.length} possible prod orgs`); + if (hubAuthInfos.length === 0 && possibleProdOrgs.length === 0) { + return; + } // ask all those orgs if they know this orgId - await Promise.all( - hubAuthInfos.map(async (hubAuthInfo) => { + await Promise.all([ + ...hubAuthInfos.map(async (hubAuthInfo) => { try { const soi = await AuthInfo.queryScratchOrg(hubAuthInfo.username, fields.orgId as string); // if any return a result @@ -429,49 +437,16 @@ export class AuthInfo extends AsyncOptionalCreatable { } catch (error) { if (error instanceof Error && error.name === 'SingleRecordQuery_NoRecords' && fields.orgId) { // Not a scratch org owned by this hub. Check if it's a sandbox. - try { - const prodOrg = await Org.create({ aliasOrUsername: hubAuthInfo.username }); - const sbxProcess = await prodOrg.querySandboxProcessByOrgId(fields.orgId); - logger.debug(`${fields.orgId} is a sandbox of ${hubAuthInfo.username}`); - - try { - await orgAuthInfo.save({ - ...fields, - isScratch: false, - isSandbox: true, - }); - } catch (err) { - logger.debug(`error updating auth file for: ${orgAuthInfo.getUsername()}`, err); - } - - try { - // set the sandbox config value - const sfSandbox = { - sandboxUsername: fields.username, - sandboxOrgId: fields.orgId, - prodOrgUsername: hubAuthInfo.username, - sandboxName: sbxProcess.SandboxName, - sandboxProcessId: sbxProcess.Id, - sandboxInfoId: sbxProcess.SandboxInfoId, - timestamp: new Date().toISOString(), - } as SandboxFields; - - const stateAggregator = await StateAggregator.getInstance(); - stateAggregator.sandboxes.set(fields.orgId, sfSandbox); - logger.debug(`writing sandbox auth file for: ${orgAuthInfo.getUsername()} with ID: ${fields.orgId}`); - await stateAggregator.sandboxes.write(fields.orgId); - } catch (e) { - logger.debug(`error writing sandbox auth file for: ${orgAuthInfo.getUsername()}`, e); - } - } catch (err) { - logger.debug(`${fields.orgId} is not a sandbox of ${hubAuthInfo.username}`); - } + await AuthInfo.identifyPossibleSandbox(hubAuthInfo, fields, orgAuthInfo, logger); } else { logger.error(`Error connecting to devhub ${hubAuthInfo.username}`, error); } } - }) - ); + }), + ...possibleProdOrgs.map(async (pOrgAuthInfo) => { + await AuthInfo.identifyPossibleSandbox(pOrgAuthInfo, fields, orgAuthInfo, logger); + }), + ]); } /** @@ -481,6 +456,59 @@ export class AuthInfo extends AsyncOptionalCreatable { return AuthInfo.listAllAuthorizations((possibleHub) => possibleHub?.isDevHub ?? false); } + private static async identifyPossibleSandbox( + possibleProdOrg: OrgAuthorization, + fields: AuthFields, + orgAuthInfo: AuthInfo, + logger: Logger + ): Promise { + if (!fields.orgId) { + return; + } + + try { + const prodOrg = await Org.create({ aliasOrUsername: possibleProdOrg.username }); + const sbxProcess = await prodOrg.querySandboxProcessByOrgId(fields.orgId); + if (!sbxProcess?.SandboxInfoId) { + return; + } + logger.debug(`${fields.orgId} is a sandbox of ${possibleProdOrg.username}`); + + try { + await orgAuthInfo.save({ + ...fields, + isScratch: false, + isSandbox: true, + }); + } catch (err) { + logger.debug(`error updating auth file for: ${orgAuthInfo.getUsername()}`, err); + throw err; // rethrow; don't want a sandbox config file with an invalid auth file + } + + try { + // set the sandbox config value + const sfSandbox = { + sandboxUsername: fields.username, + sandboxOrgId: fields.orgId, + prodOrgUsername: possibleProdOrg.username, + sandboxName: sbxProcess.SandboxName, + sandboxProcessId: sbxProcess.Id, + sandboxInfoId: sbxProcess.SandboxInfoId, + timestamp: new Date().toISOString(), + } as SandboxFields; + + const stateAggregator = await StateAggregator.getInstance(); + stateAggregator.sandboxes.set(fields.orgId, sfSandbox); + logger.debug(`writing sandbox auth file for: ${orgAuthInfo.getUsername()} with ID: ${fields.orgId}`); + await stateAggregator.sandboxes.write(fields.orgId); + } catch (e) { + logger.debug(`error writing sandbox auth file for: ${orgAuthInfo.getUsername()}`, e); + } + } catch (err) { + logger.debug(`${fields.orgId} is not a sandbox of ${possibleProdOrg.username}`); + } + } + /** * Checks active scratch orgs to match by the ScratchOrg field (the 15-char org id) * if you pass an 18-char scratchOrgId, it will be trimmed to 15-char for query purposes diff --git a/test/unit/org/authInfoTest.ts b/test/unit/org/authInfoTest.ts index 7f6f7dd181..87c9b4ca9b 100644 --- a/test/unit/org/authInfoTest.ts +++ b/test/unit/org/authInfoTest.ts @@ -1788,8 +1788,10 @@ describe('AuthInfo', () => { }); const getDevHubAuthInfosStub = stubMethod($$.SANDBOX, AuthInfo, 'getDevHubAuthInfos').resolves([]); + stubMethod($$.SANDBOX, AuthInfo, 'listAllAuthorizations').resolves([]); const queryScratchOrgStub = stubMethod($$.SANDBOX, AuthInfo, 'queryScratchOrg'); const authInfoSaveStub = stubMethod($$.SANDBOX, AuthInfo.prototype, 'save'); + stubMethod($$.SANDBOX, Org.prototype, 'querySandboxProcessByOrgId').throws(); await AuthInfo.identifyPossibleScratchOrgs({ orgId: user1.orgId }, authInfo); expect(getDevHubAuthInfosStub.callCount).to.be.equal(1); @@ -1811,6 +1813,7 @@ describe('AuthInfo', () => { const getDevHubAuthInfosStub = stubMethod($$.SANDBOX, AuthInfo, 'getDevHubAuthInfos').resolves([]); const queryScratchOrgStub = stubMethod($$.SANDBOX, AuthInfo, 'queryScratchOrg'); const authInfoSaveStub = stubMethod($$.SANDBOX, AuthInfo.prototype, 'save'); + stubMethod($$.SANDBOX, Org.prototype, 'querySandboxProcessByOrgId').throws(); await AuthInfo.identifyPossibleScratchOrgs(authInfo.getFields(), authInfo); expect(getDevHubAuthInfosStub.callCount).to.be.equal(0); @@ -1833,6 +1836,7 @@ describe('AuthInfo', () => { const getDevHubAuthInfosSpy = spyMethod($$.SANDBOX, AuthInfo, 'getDevHubAuthInfos'); const queryScratchOrgStub = stubMethod($$.SANDBOX, AuthInfo, 'queryScratchOrg'); const authInfoSaveStub = stubMethod($$.SANDBOX, AuthInfo.prototype, 'save'); + stubMethod($$.SANDBOX, Org.prototype, 'querySandboxProcessByOrgId').throws(); await AuthInfo.identifyPossibleScratchOrgs(authInfo.getFields(), authInfo); expect(getDevHubAuthInfosSpy.callCount).to.be.equal(0); @@ -1849,19 +1853,67 @@ describe('AuthInfo', () => { username: user1.username, }); - const getDevHubAuthInfosSpy = spyMethod($$.SANDBOX, AuthInfo, 'getDevHubAuthInfos'); + const devhubAuths = await AuthInfo.getDevHubAuthInfos(); + const getDevHubAuthInfosStub = stubMethod($$.SANDBOX, AuthInfo, 'getDevHubAuthInfos').resolves(devhubAuths); + stubMethod($$.SANDBOX, AuthInfo, 'listAllAuthorizations').resolves([]); const queryScratchOrgStub = stubMethod($$.SANDBOX, AuthInfo, 'queryScratchOrg').resolves({ Id: '123', ExpirationDate: '2020-01-01', }); const authInfoSaveStub = stubMethod($$.SANDBOX, AuthInfo.prototype, 'save'); + stubMethod($$.SANDBOX, Org.prototype, 'querySandboxProcessByOrgId').throws(); + await AuthInfo.identifyPossibleScratchOrgs(authInfo.getFields(), authInfo); - expect(getDevHubAuthInfosSpy.callCount).to.be.equal(1); + expect(getDevHubAuthInfosStub.callCount).to.be.equal(1); expect(queryScratchOrgStub.callCount).to.be.equal(1); expect(authInfoSaveStub.callCount).to.be.equal(1); }); - it('should update auth file as a sandbox', async () => { + it('should update auth file as a sandbox from devhub', async () => { + adminTestData.makeDevHub(); + + await $$.stubAuths(adminTestData, user1); + + const authInfo = await AuthInfo.create({ + username: user1.username, + }); + + const stateAggregator = await StateAggregator.getInstance(); + const stateAggregatorStub = stubMethod($$.SANDBOX, StateAggregator, 'getInstance'); + const sandboxSetStub = stubMethod($$.SANDBOX, stateAggregator.sandboxes, 'set'); + const sandboxWriteStub = stubMethod($$.SANDBOX, stateAggregator.sandboxes, 'write'); + stateAggregatorStub.resolves(stateAggregator); + const devhubAuths = await AuthInfo.getDevHubAuthInfos(); + stubMethod($$.SANDBOX, AuthInfo, 'getDevHubAuthInfos').resolves(devhubAuths); + stubMethod($$.SANDBOX, AuthInfo, 'listAllAuthorizations').resolves([]); + const authInfoSaveStub = stubMethod($$.SANDBOX, AuthInfo.prototype, 'save').resolves(); + const queryScratchOrgStub = stubMethod($$.SANDBOX, AuthInfo, 'queryScratchOrg'); + const queryScratchOrgError = new Error('not a scratch org'); + queryScratchOrgError.name = 'SingleRecordQuery_NoRecords'; + queryScratchOrgStub.throws(queryScratchOrgError); + const sbxQueryStub = stubMethod($$.SANDBOX, Org.prototype, 'querySandboxProcessByOrgId'); + sbxQueryStub.resolves({ + Id: '0GRB0000000L0ZVOA0', + Status: 'Completed', + SandboxName: 'TestSandbox', + SandboxInfoId: '0GQB0000000PCOdOAO', + LicenseType: 'DEVELOPER', + CreatedDate: '2021-01-22T22:49:52.000+0000', + }); + + await AuthInfo.identifyPossibleScratchOrgs(authInfo.getFields(), authInfo); + + expect(authInfoSaveStub.callCount).to.be.equal(2); + expect(authInfoSaveStub.secondCall.args[0]).to.have.property('isSandbox', true); + expect(authInfoSaveStub.secondCall.args[0]).to.have.property('isScratch', false); + expect(sandboxSetStub.calledOnce).to.be.true; + expect(sandboxSetStub.firstCall.args[0]).to.equal(authInfo.getFields().orgId); + expect(sandboxSetStub.firstCall.args[1]).to.have.property('prodOrgUsername', adminTestData.username); + expect(sandboxWriteStub.calledOnce).to.be.true; + expect(sandboxWriteStub.firstCall.args[0]).to.equal(authInfo.getFields().orgId); + }); + + it('should update auth file as a sandbox from possible prod orgs', async () => { adminTestData.makeDevHub(); await $$.stubAuths(adminTestData, user1); @@ -1875,6 +1927,9 @@ describe('AuthInfo', () => { const sandboxSetStub = stubMethod($$.SANDBOX, stateAggregator.sandboxes, 'set'); const sandboxWriteStub = stubMethod($$.SANDBOX, stateAggregator.sandboxes, 'write'); stateAggregatorStub.resolves(stateAggregator); + const devhubAuths = await AuthInfo.getDevHubAuthInfos(); + stubMethod($$.SANDBOX, AuthInfo, 'getDevHubAuthInfos').resolves([]); + stubMethod($$.SANDBOX, AuthInfo, 'listAllAuthorizations').resolves(devhubAuths); const authInfoSaveStub = stubMethod($$.SANDBOX, AuthInfo.prototype, 'save').resolves(); const queryScratchOrgStub = stubMethod($$.SANDBOX, AuthInfo, 'queryScratchOrg'); const queryScratchOrgError = new Error('not a scratch org'); @@ -1892,7 +1947,7 @@ describe('AuthInfo', () => { await AuthInfo.identifyPossibleScratchOrgs(authInfo.getFields(), authInfo); - expect(authInfoSaveStub.calledTwice).to.be.true; + expect(authInfoSaveStub.callCount).to.be.equal(2); expect(authInfoSaveStub.secondCall.args[0]).to.have.property('isSandbox', true); expect(authInfoSaveStub.secondCall.args[0]).to.have.property('isScratch', false); expect(sandboxSetStub.calledOnce).to.be.true; From 9bf4154216dc0bd07438f8be34a3902fa0e64d8f Mon Sep 17 00:00:00 2001 From: Steve Hetzel Date: Tue, 1 Aug 2023 16:27:18 -0600 Subject: [PATCH 3/4] fix: better type for SandboxFields --- src/org/authInfo.ts | 4 ++-- src/org/org.ts | 7 ++++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/org/authInfo.ts b/src/org/authInfo.ts index 005b2dc1e0..190f433172 100644 --- a/src/org/authInfo.ts +++ b/src/org/authInfo.ts @@ -487,7 +487,7 @@ export class AuthInfo extends AsyncOptionalCreatable { try { // set the sandbox config value - const sfSandbox = { + const sfSandbox: SandboxFields = { sandboxUsername: fields.username, sandboxOrgId: fields.orgId, prodOrgUsername: possibleProdOrg.username, @@ -495,7 +495,7 @@ export class AuthInfo extends AsyncOptionalCreatable { sandboxProcessId: sbxProcess.Id, sandboxInfoId: sbxProcess.SandboxInfoId, timestamp: new Date().toISOString(), - } as SandboxFields; + }; const stateAggregator = await StateAggregator.getInstance(); stateAggregator.sandboxes.set(fields.orgId, sfSandbox); diff --git a/src/org/org.ts b/src/org/org.ts index a44d0a478f..9fc40b4e1d 100644 --- a/src/org/org.ts +++ b/src/org/org.ts @@ -134,6 +134,7 @@ export type SandboxFields = { sandboxUsername?: string; sandboxProcessId?: string; sandboxInfoId?: string; + timestamp?: string; }; /** @@ -1298,15 +1299,15 @@ export class Org extends AsyncOptionalCreatable { throw messages.createError('AuthInfoOrgIdUndefined'); } // set the sandbox config value - const sfSandbox = { + const sfSandbox: SandboxFields = { sandboxUsername: sandboxRes.authUserName, sandboxOrgId, - prodOrgUsername: this.getUsername(), + prodOrgUsername: this.getUsername() as string, sandboxName: sandboxProcessObj.SandboxName, sandboxProcessId: sandboxProcessObj.Id, sandboxInfoId: sandboxProcessObj.SandboxInfoId, timestamp: new Date().toISOString(), - } as SandboxFields; + }; await this.setSandboxConfig(sandboxOrgId, sfSandbox); await (await StateAggregator.getInstance()).sandboxes.write(sandboxOrgId); From 26d923e5ec34efd9236675ef8c0290341da48c40 Mon Sep 17 00:00:00 2001 From: Steve Hetzel Date: Wed, 2 Aug 2023 13:37:06 -0600 Subject: [PATCH 4/4] fix: query devhubs, non-scratch orgs, and non-sandboxes to identify sandboxes --- src/org/authInfo.ts | 9 ++++--- test/unit/org/authInfoTest.ts | 44 ----------------------------------- 2 files changed, 4 insertions(+), 49 deletions(-) diff --git a/src/org/authInfo.ts b/src/org/authInfo.ts index 190f433172..01517fb131 100644 --- a/src/org/authInfo.ts +++ b/src/org/authInfo.ts @@ -401,9 +401,9 @@ export class AuthInfo extends AsyncOptionalCreatable { // TODO: someday we make this easier by asking the org if it is a scratch org const hubAuthInfos = await AuthInfo.getDevHubAuthInfos(); - // Get a list of org auths that are known not to be devhubs, scratch orgs, or sandboxes. + // Get a list of org auths that are known not to be scratch orgs or sandboxes. const possibleProdOrgs = await AuthInfo.listAllAuthorizations( - (orgAuth) => orgAuth && !orgAuth.isDevHub && !orgAuth.isScratchOrg && !orgAuth.isSandbox + (orgAuth) => orgAuth && !orgAuth.isScratchOrg && !orgAuth.isSandbox ); logger.debug(`found ${hubAuthInfos.length} DevHubs`); @@ -435,9 +435,8 @@ export class AuthInfo extends AsyncOptionalCreatable { logger.debug(`error updating auth file for ${orgAuthInfo.getUsername()}`, error); } } catch (error) { - if (error instanceof Error && error.name === 'SingleRecordQuery_NoRecords' && fields.orgId) { - // Not a scratch org owned by this hub. Check if it's a sandbox. - await AuthInfo.identifyPossibleSandbox(hubAuthInfo, fields, orgAuthInfo, logger); + if (error instanceof Error && error.name === 'NoActiveScratchOrgFound') { + logger.error(`devhub ${hubAuthInfo.username} has no scratch orgs`, error); } else { logger.error(`Error connecting to devhub ${hubAuthInfo.username}`, error); } diff --git a/test/unit/org/authInfoTest.ts b/test/unit/org/authInfoTest.ts index 87c9b4ca9b..fe024a9d88 100644 --- a/test/unit/org/authInfoTest.ts +++ b/test/unit/org/authInfoTest.ts @@ -1869,50 +1869,6 @@ describe('AuthInfo', () => { expect(authInfoSaveStub.callCount).to.be.equal(1); }); - it('should update auth file as a sandbox from devhub', async () => { - adminTestData.makeDevHub(); - - await $$.stubAuths(adminTestData, user1); - - const authInfo = await AuthInfo.create({ - username: user1.username, - }); - - const stateAggregator = await StateAggregator.getInstance(); - const stateAggregatorStub = stubMethod($$.SANDBOX, StateAggregator, 'getInstance'); - const sandboxSetStub = stubMethod($$.SANDBOX, stateAggregator.sandboxes, 'set'); - const sandboxWriteStub = stubMethod($$.SANDBOX, stateAggregator.sandboxes, 'write'); - stateAggregatorStub.resolves(stateAggregator); - const devhubAuths = await AuthInfo.getDevHubAuthInfos(); - stubMethod($$.SANDBOX, AuthInfo, 'getDevHubAuthInfos').resolves(devhubAuths); - stubMethod($$.SANDBOX, AuthInfo, 'listAllAuthorizations').resolves([]); - const authInfoSaveStub = stubMethod($$.SANDBOX, AuthInfo.prototype, 'save').resolves(); - const queryScratchOrgStub = stubMethod($$.SANDBOX, AuthInfo, 'queryScratchOrg'); - const queryScratchOrgError = new Error('not a scratch org'); - queryScratchOrgError.name = 'SingleRecordQuery_NoRecords'; - queryScratchOrgStub.throws(queryScratchOrgError); - const sbxQueryStub = stubMethod($$.SANDBOX, Org.prototype, 'querySandboxProcessByOrgId'); - sbxQueryStub.resolves({ - Id: '0GRB0000000L0ZVOA0', - Status: 'Completed', - SandboxName: 'TestSandbox', - SandboxInfoId: '0GQB0000000PCOdOAO', - LicenseType: 'DEVELOPER', - CreatedDate: '2021-01-22T22:49:52.000+0000', - }); - - await AuthInfo.identifyPossibleScratchOrgs(authInfo.getFields(), authInfo); - - expect(authInfoSaveStub.callCount).to.be.equal(2); - expect(authInfoSaveStub.secondCall.args[0]).to.have.property('isSandbox', true); - expect(authInfoSaveStub.secondCall.args[0]).to.have.property('isScratch', false); - expect(sandboxSetStub.calledOnce).to.be.true; - expect(sandboxSetStub.firstCall.args[0]).to.equal(authInfo.getFields().orgId); - expect(sandboxSetStub.firstCall.args[1]).to.have.property('prodOrgUsername', adminTestData.username); - expect(sandboxWriteStub.calledOnce).to.be.true; - expect(sandboxWriteStub.firstCall.args[0]).to.equal(authInfo.getFields().orgId); - }); - it('should update auth file as a sandbox from possible prod orgs', async () => { adminTestData.makeDevHub();