diff --git a/src/org/org.ts b/src/org/org.ts index 9fc40b4e1d..c061b89b3f 100644 --- a/src/org/org.ts +++ b/src/org/org.ts @@ -83,6 +83,7 @@ export enum SandboxEvents { EVENT_RESULT = 'result', EVENT_AUTH = 'auth', EVENT_RESUME = 'resume', + EVENT_MULTIPLE_SBX_PROCESSES = 'multipleMatchingSbxProcesses', } export interface SandboxUserAuthResponse { @@ -246,11 +247,11 @@ export class Org extends AsyncOptionalCreatable { } /** - * resume a sandbox creation from a production org - * 'this' needs to be a production org with sandbox licenses available + * Resume a sandbox creation from a production org. + * `this` needs to be a production org with sandbox licenses available. * * @param resumeSandboxRequest SandboxRequest options to create the sandbox with - * @param options Wait: The amount of time to wait (default: 30 minutes) before timing out, + * @param options Wait: The amount of time to wait (default: 0 minutes) before timing out, * Interval: The time interval (default: 30 seconds) between polling */ public async resumeSandbox( @@ -264,10 +265,23 @@ export class Org extends AsyncOptionalCreatable { this.logger.debug(resumeSandboxRequest, 'ResumeSandbox called with ResumeSandboxRequest'); let sandboxCreationProgress: SandboxProcessObject; // seed the sandboxCreationProgress via the resumeSandboxRequest options - if (resumeSandboxRequest.SandboxName) { - sandboxCreationProgress = await this.querySandboxProcessBySandboxName(resumeSandboxRequest.SandboxName); - } else if (resumeSandboxRequest.SandboxProcessObjId) { + if (resumeSandboxRequest.SandboxProcessObjId) { sandboxCreationProgress = await this.querySandboxProcessById(resumeSandboxRequest.SandboxProcessObjId); + } else if (resumeSandboxRequest.SandboxName) { + try { + // There can be multiple sandbox processes returned when querying by name. Use the most recent + // process and fire a warning event with all processes. + sandboxCreationProgress = await this.querySandboxProcessBySandboxName(resumeSandboxRequest.SandboxName); + } catch (err) { + if (err instanceof SfError && err.name === 'SingleRecordQuery_MultipleRecords' && err.data) { + const sbxProcesses = err.data as SandboxProcessObject[]; + // 0 index will always be the most recently created process since the query sorts on created date desc. + sandboxCreationProgress = sbxProcesses[0]; + await Lifecycle.getInstance().emit(SandboxEvents.EVENT_MULTIPLE_SBX_PROCESSES, sbxProcesses); + } else { + throw err; + } + } } else { throw messages.createError('sandboxNotFound', [ resumeSandboxRequest.SandboxName ?? resumeSandboxRequest.SandboxProcessObjId, @@ -1332,9 +1346,7 @@ export class Org extends AsyncOptionalCreatable { let waitingOnAuth = false; const pollingClient = await PollingClient.create({ poll: async (): Promise => { - const sandboxProcessObj = await this.querySandboxProcessBySandboxInfoId( - options.sandboxProcessObj.SandboxInfoId - ); + const sandboxProcessObj = await this.querySandboxProcessById(options.sandboxProcessObj.Id); // check to see if sandbox can authenticate via sandboxAuth endpoint const sandboxInfo = await this.sandboxSignupComplete(sandboxProcessObj); if (sandboxInfo) { @@ -1377,10 +1389,19 @@ export class Org extends AsyncOptionalCreatable { * @private */ private async querySandboxProcess(where: string): Promise { - const queryStr = `SELECT Id, Status, SandboxName, SandboxInfoId, LicenseType, CreatedDate, CopyProgress, SandboxOrganization, SourceId, Description, EndDate FROM SandboxProcess WHERE ${where} AND Status != 'D'`; - return this.connection.singleRecordQuery(queryStr, { - tooling: true, - }); + const soql = `SELECT Id, Status, SandboxName, SandboxInfoId, LicenseType, CreatedDate, CopyProgress, SandboxOrganization, SourceId, Description, EndDate FROM SandboxProcess WHERE ${where} ORDER BY CreatedDate DESC`; + const result = (await this.connection.tooling.query(soql)).records.filter( + (item) => !item.Status.startsWith('Del') + ); + if (result.length === 0) { + throw new SfError(`No record found for ${soql}`, SingleRecordQueryErrors.NoRecords); + } + if (result.length > 1) { + const err = new SfError('The query returned more than 1 record', SingleRecordQueryErrors.MultipleRecords); + err.data = result; + throw err; + } + return result[0]; } /** * determines if the sandbox has successfully been created diff --git a/test/unit/org/orgTest.ts b/test/unit/org/orgTest.ts index f72f8dfb98..76f468d03d 100644 --- a/test/unit/org/orgTest.ts +++ b/test/unit/org/orgTest.ts @@ -8,17 +8,19 @@ import { deepStrictEqual, fail } from 'assert'; import * as fs from 'fs'; import { constants as fsConstants } from 'fs'; import { join as pathJoin } from 'path'; +import { format } from 'node:util'; import { Duration, set } from '@salesforce/kit'; -import { stubMethod } from '@salesforce/ts-sinon'; +import { spyMethod, stubMethod } from '@salesforce/ts-sinon'; import { AnyJson, ensureJsonArray, ensureJsonMap, ensureString, JsonMap, Optional } from '@salesforce/ts-types'; import { assert, expect, config as chaiConfig } from 'chai'; import { OAuth2 } from 'jsforce'; import { Transport } from 'jsforce/lib/transport'; -import { SinonStub } from 'sinon'; +import { SinonSpy, SinonStub } from 'sinon'; import { AuthInfo, Connection, Org, + SandboxEvents, SandboxProcessObject, SandboxUserAuthResponse, SingleRecordQueryErrors, @@ -34,6 +36,7 @@ import { StateAggregator } from '../../../src/stateAggregator'; import { OrgConfigProperties } from '../../../src/org/orgConfigProperties'; import { Messages } from '../../../src/messages'; import { SfError } from '../../../src/sfError'; +import { Lifecycle } from '../../../src/lifecycleEvents'; /* eslint-disable no-await-in-loop */ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ @@ -440,167 +443,6 @@ describe('Org Tests', () => { }); }); - describe('createSandbox', () => { - let prod: Org; - let createStub: SinonStub; - let querySandboxProcessStub: SinonStub; - let pollStatusAndAuthStub: SinonStub; - - beforeEach(async () => { - const prodTestData = new MockTestOrgData(); - prod = await createOrgViaAuthInfo(prodTestData.username); - createStub = stubMethod($$.SANDBOX, prod.getConnection().tooling, 'create').resolves({ - id: '0GQ4p000000U6nFGAS', - success: true, - }); - querySandboxProcessStub = stubMethod($$.SANDBOX, prod, 'querySandboxProcess').resolves(); - pollStatusAndAuthStub = stubMethod($$.SANDBOX, prod, 'pollStatusAndAuth').resolves(); - }); - - it('will create the SandboxInfo sObject correctly', async () => { - await prod.createSandbox({ SandboxName: 'testSandbox' }, { wait: Duration.seconds(30) }); - expect(createStub.calledOnce).to.be.true; - expect(querySandboxProcessStub.calledOnce).to.be.true; - expect(pollStatusAndAuthStub.calledOnce).to.be.true; - }); - - it('will throw an error if it fails to create SandboxInfo', async () => { - createStub.restore(); - createStub = stubMethod($$.SANDBOX, prod.getConnection().tooling, 'create').resolves({ - error: 'duplicate value found: SandboxName duplicates value on record with id: 0GQ4p000000U6rv', - success: false, - }); - try { - await shouldThrow(prod.createSandbox({ SandboxName: 'testSandbox' }, { wait: Duration.seconds(30) })); - } catch (e) { - expect(createStub.calledOnce).to.be.true; - expect((e as Error).message).to.include('The sandbox org creation failed with a result of'); - expect((e as Error).message).to.include( - 'duplicate value found: SandboxName duplicates value on record with id: 0GQ4p000000U6rv' - ); - expect((e as SfError).exitCode).to.equal(1); - } - }); - - it('will auth sandbox user correctly', async () => { - const sandboxResponse = { - SandboxName: 'test', - EndDate: '2021-19-06T20:25:46.000+0000', - } as SandboxProcessObject; - const requestStub = stubMethod($$.SANDBOX, prod.getConnection().tooling, 'request').resolves(); - const instanceUrl = 'http://instance.123.salesforce.com.services/data/v50.0/tooling/'; - stubMethod($$.SANDBOX, prod.getConnection().tooling, '_baseUrl').returns(instanceUrl); - - // @ts-expect-error because private method - await prod.sandboxSignupComplete(sandboxResponse); - expect(requestStub.firstCall.args).to.deep.include({ - body: JSON.stringify({ - clientId: prod.getConnection().getAuthInfoFields().clientId, - sandboxName: sandboxResponse.SandboxName, - callbackUrl: 'http://localhost:1717/OauthRedirect', - }), - headers: { - 'Content-Type': 'application/json', - }, - method: 'POST', - url: `${instanceUrl}/sandboxAuth`, - }); - }); - - it('will fail to auth sandbox user correctly - but will swallow the error', async () => { - // @ts-expect-error because private member - const logStub = stubMethod($$.SANDBOX, prod.logger, 'debug'); - const sandboxResponse = { - SandboxName: 'test', - EndDate: '2021-19-06T20:25:46.000+0000', - } as SandboxProcessObject; - // @ts-expect-error - type not assignable - stubMethod($$.SANDBOX, prod.getConnection().tooling, 'request').throws({ - name: 'INVALID_STATUS', - }); - - // @ts-expect-error because private method - await prod.sandboxSignupComplete(sandboxResponse); - expect(logStub.callCount).to.equal(3); - // error swallowed - expect(logStub.thirdCall.args[0]).to.equal('Error while authenticating the user'); - }); - }); - - describe('cloneSandbox', () => { - let prod: Org; - let createStub: sinon.SinonStub; - let querySandboxProcessStub: sinon.SinonStub; - let pollStatusAndAuthStub: sinon.SinonStub; - let devHubQueryStub: sinon.SinonStub; - - const orgId = '0GQ4p000000U6nFGAS'; - - beforeEach(async () => { - const prodTestData = new MockTestOrgData(); - prod = await createOrgViaAuthInfo(prodTestData.username); - createStub = stubMethod($$.SANDBOX, prod.getConnection().tooling, 'create').resolves({ - id: orgId, - success: true, - }); - querySandboxProcessStub = stubMethod($$.SANDBOX, prod, 'querySandboxProcess').resolves({ - Id: '00D56000000CDsAKJS', - }); - pollStatusAndAuthStub = stubMethod($$.SANDBOX, prod, 'pollStatusAndAuth').resolves(); - devHubQueryStub = stubMethod($$.SANDBOX, prod.getConnection().tooling, 'query').resolves({ - records: [ - { - Id: orgId, - }, - ], - }); - - // SourceSandbox - await prod.createSandbox({ SandboxName: 'testSandbox' }, { wait: Duration.seconds(30) }); - - // reset the history of these stubs so we only look at what happens with `cloneSandbox()` - createStub.resetHistory(); - pollStatusAndAuthStub.resetHistory(); - querySandboxProcessStub.resetHistory(); - devHubQueryStub.resetHistory(); - }); - - it('will clone the sandbox given a SandBoxName', async () => { - await prod.cloneSandbox({ SandboxName: 'testSandbox' }, 'testSandbox', { wait: Duration.seconds(30) }); - expect(createStub.calledOnce).to.be.true; - expect(querySandboxProcessStub.calledTwice).to.be.true; - expect(pollStatusAndAuthStub.calledOnce).to.be.true; - }); - - it('fails to get sanboxInfo from tooling.query', async () => { - querySandboxProcessStub.restore(); - devHubQueryStub.restore(); - devHubQueryStub = stubMethod($$.SANDBOX, prod.getConnection().tooling, 'query').throws(); - try { - await prod.cloneSandbox({ SandboxName: 'testSandbox' }, 'testSandbox', { wait: Duration.seconds(30) }); - fail('the above should throw an error'); - } catch (e) { - expect(devHubQueryStub.calledOnce).to.be.true; - expect(createStub.called).to.be.false; - expect(pollStatusAndAuthStub.called).to.be.false; - } - }); - - it('when creating sandbox tooling create rejects', async () => { - createStub.restore(); - createStub = stubMethod($$.SANDBOX, prod.getConnection().tooling, 'create').rejects(); - try { - await prod.cloneSandbox({ SandboxName: 'testSandbox' }, 'testSandbox', { wait: Duration.seconds(30) }); - fail('the above should throw an error'); - } catch (e) { - expect(createStub.calledOnce).to.be.true; - expect(querySandboxProcessStub.calledOnce).to.be.true; - expect(pollStatusAndAuthStub.called).to.be.false; - expect(devHubQueryStub.called).to.be.false; - } - }); - }); - it('should remove all assets associated with the org', async () => { const org = await createOrgViaAuthInfo(); @@ -1043,72 +885,421 @@ describe('Org Tests', () => { }); }); - describe('sandboxStatus', () => { - let prod: Org; - let queryStub: SinonStub; - let pollStatusAndAuthStub: SinonStub; - const sandboxNameIn = 'test-sandbox'; - const queryStr = `SELECT Id, Status, SandboxName, SandboxInfoId, LicenseType, CreatedDate, CopyProgress, SandboxOrganization, SourceId, Description, EndDate FROM SandboxProcess WHERE SandboxName='${sandboxNameIn}' AND Status != 'D' ORDER BY CreatedDate DESC LIMIT 1`; - + describe('Sandboxes', () => { const statusResult = { records: [ { - Id: '00D1u000001QQZz', - Status: 'Active', + Id: '0GR1Q000000LVf8WAG', SandboxName: 'test-sandbox', - SandboxInfoId: '00D1u000001QQZz', - LicenseType: 'Developer', - CreatedDate: '2022-01-01', + Status: 'Pending', + LicenseType: 'DEVELOPER', + SandboxInfoId: '0GQB0000000TVobOAG', + CreatedDate: '2023-09-27T20:50:26.000+0000', + SandboxOrg: '00D1u000001QQZz', }, ], }; + + let prodTestData: MockTestOrgData; + let prod: Org; + beforeEach(async () => { - const prodTestData = new MockTestOrgData(); + prodTestData = new MockTestOrgData(); prod = await createOrgViaAuthInfo(prodTestData.username); - queryStub = stubMethod($$.SANDBOX, prod.getConnection().tooling, 'query').resolves(statusResult); - pollStatusAndAuthStub = stubMethod($$.SANDBOX, prod, 'pollStatusAndAuth').resolves(statusResult.records[0]); }); - it('should return sandbox status', async () => { - const result = await prod.sandboxStatus(sandboxNameIn, { wait: Duration.minutes(10) }); - expect(queryStub.calledOnce).to.be.true; - expect(queryStub.firstCall.firstArg).to.be.equal(queryStr); - expect(pollStatusAndAuthStub.calledOnce).to.be.true; - expect(result).to.be.equal(statusResult.records[0]); + describe('createSandbox', () => { + let createStub: SinonStub; + let querySandboxProcessStub: SinonStub; + let pollStatusAndAuthStub: SinonStub; + + beforeEach(async () => { + createStub = stubMethod($$.SANDBOX, prod.getConnection().tooling, 'create').resolves({ + id: '0GQ4p000000U6nFGAS', + success: true, + }); + querySandboxProcessStub = stubMethod($$.SANDBOX, prod, 'querySandboxProcess').resolves(); + pollStatusAndAuthStub = stubMethod($$.SANDBOX, prod, 'pollStatusAndAuth').resolves(); + }); + + it('will create the SandboxInfo sObject correctly', async () => { + await prod.createSandbox({ SandboxName: 'testSandbox' }, { wait: Duration.seconds(30) }); + expect(createStub.calledOnce).to.be.true; + expect(querySandboxProcessStub.calledOnce).to.be.true; + expect(pollStatusAndAuthStub.calledOnce).to.be.true; + }); + + it('will throw an error if it fails to create SandboxInfo', async () => { + createStub.restore(); + createStub = stubMethod($$.SANDBOX, prod.getConnection().tooling, 'create').resolves({ + error: 'duplicate value found: SandboxName duplicates value on record with id: 0GQ4p000000U6rv', + success: false, + }); + try { + await shouldThrow(prod.createSandbox({ SandboxName: 'testSandbox' }, { wait: Duration.seconds(30) })); + } catch (e) { + expect(createStub.calledOnce).to.be.true; + expect((e as Error).message).to.include('The sandbox org creation failed with a result of'); + expect((e as Error).message).to.include( + 'duplicate value found: SandboxName duplicates value on record with id: 0GQ4p000000U6rv' + ); + expect((e as SfError).exitCode).to.equal(1); + } + }); + + it('will auth sandbox user correctly', async () => { + const sandboxResponse = { + SandboxName: 'test', + EndDate: '2021-19-06T20:25:46.000+0000', + } as SandboxProcessObject; + const requestStub = stubMethod($$.SANDBOX, prod.getConnection().tooling, 'request').resolves(); + const instanceUrl = 'http://instance.123.salesforce.com.services/data/v50.0/tooling/'; + stubMethod($$.SANDBOX, prod.getConnection().tooling, '_baseUrl').returns(instanceUrl); + + // @ts-expect-error because private method + await prod.sandboxSignupComplete(sandboxResponse); + expect(requestStub.firstCall.args).to.deep.include({ + body: JSON.stringify({ + clientId: prod.getConnection().getAuthInfoFields().clientId, + sandboxName: sandboxResponse.SandboxName, + callbackUrl: 'http://localhost:1717/OauthRedirect', + }), + headers: { + 'Content-Type': 'application/json', + }, + method: 'POST', + url: `${instanceUrl}/sandboxAuth`, + }); + }); + + it('will fail to auth sandbox user correctly - but will swallow the error', async () => { + // @ts-expect-error because private member + const logStub = stubMethod($$.SANDBOX, prod.logger, 'debug'); + const sandboxResponse = { + SandboxName: 'test', + EndDate: '2021-19-06T20:25:46.000+0000', + } as SandboxProcessObject; + // @ts-expect-error - type not assignable + stubMethod($$.SANDBOX, prod.getConnection().tooling, 'request').throws({ + name: 'INVALID_STATUS', + }); + + // @ts-expect-error because private method + await prod.sandboxSignupComplete(sandboxResponse); + expect(logStub.callCount).to.equal(3); + // error swallowed + expect(logStub.thirdCall.args[0]).to.equal('Error while authenticating the user'); + }); }); - it('should fail when query returns empty records for the sandbox', async () => { - queryStub.restore(); - queryStub = stubMethod($$.SANDBOX, prod.getConnection().tooling, 'query').resolves({ - records: [], + describe('cloneSandbox', () => { + let createStub: sinon.SinonStub; + let querySandboxProcessStub: sinon.SinonStub; + let pollStatusAndAuthStub: sinon.SinonStub; + let devHubQueryStub: sinon.SinonStub; + + const orgId = '0GQ4p000000U6nFGAS'; + + beforeEach(async () => { + createStub = stubMethod($$.SANDBOX, prod.getConnection().tooling, 'create').resolves({ + id: orgId, + success: true, + }); + querySandboxProcessStub = stubMethod($$.SANDBOX, prod, 'querySandboxProcess').resolves({ + Id: '00D56000000CDsAKJS', + }); + pollStatusAndAuthStub = stubMethod($$.SANDBOX, prod, 'pollStatusAndAuth').resolves(); + devHubQueryStub = stubMethod($$.SANDBOX, prod.getConnection().tooling, 'query').resolves({ + records: [ + { + Id: orgId, + }, + ], + }); + + // SourceSandbox + await prod.createSandbox({ SandboxName: 'testSandbox' }, { wait: Duration.seconds(30) }); + + // reset the history of these stubs so we only look at what happens with `cloneSandbox()` + createStub.resetHistory(); + pollStatusAndAuthStub.resetHistory(); + querySandboxProcessStub.resetHistory(); + devHubQueryStub.resetHistory(); }); - try { - await shouldThrow(prod.sandboxStatus(sandboxNameIn, { wait: Duration.minutes(10) })); - } catch (e) { - expect((e as Error).message).to.be.equal( - messages.getMessage('SandboxProcessNotFoundBySandboxName', [sandboxNameIn]) - ); + + it('will clone the sandbox given a SandBoxName', async () => { + await prod.cloneSandbox({ SandboxName: 'testSandbox' }, 'testSandbox', { wait: Duration.seconds(30) }); + expect(createStub.calledOnce).to.be.true; + expect(querySandboxProcessStub.calledTwice).to.be.true; + expect(pollStatusAndAuthStub.calledOnce).to.be.true; + }); + + it('fails to get sanboxInfo from tooling.query', async () => { + querySandboxProcessStub.restore(); + devHubQueryStub.restore(); + devHubQueryStub = stubMethod($$.SANDBOX, prod.getConnection().tooling, 'query').throws(); + try { + await prod.cloneSandbox({ SandboxName: 'testSandbox' }, 'testSandbox', { wait: Duration.seconds(30) }); + fail('the above should throw an error'); + } catch (e) { + expect(devHubQueryStub.calledOnce).to.be.true; + expect(createStub.called).to.be.false; + expect(pollStatusAndAuthStub.called).to.be.false; + } + }); + + it('when creating sandbox tooling create rejects', async () => { + createStub.restore(); + createStub = stubMethod($$.SANDBOX, prod.getConnection().tooling, 'create').rejects(); + try { + await prod.cloneSandbox({ SandboxName: 'testSandbox' }, 'testSandbox', { wait: Duration.seconds(30) }); + fail('the above should throw an error'); + } catch (e) { + expect(createStub.calledOnce).to.be.true; + expect(querySandboxProcessStub.calledOnce).to.be.true; + expect(pollStatusAndAuthStub.called).to.be.false; + expect(devHubQueryStub.called).to.be.false; + } + }); + }); + + describe('resumeSandbox', () => { + const expectedSoql = + 'SELECT Id, Status, SandboxName, SandboxInfoId, LicenseType, CreatedDate, CopyProgress, SandboxOrganization, SourceId, Description, EndDate FROM SandboxProcess WHERE %s ORDER BY CreatedDate DESC'; + let lifecycleSpy: SinonSpy; + let queryStub: SinonStub; + let pollStatusAndAuthSpy: SinonSpy; + + beforeEach(async () => { + queryStub = stubMethod($$.SANDBOX, prod.getConnection().tooling, 'query'); + pollStatusAndAuthSpy = spyMethod($$.SANDBOX, prod, 'pollStatusAndAuth'); + lifecycleSpy = spyMethod($$.SANDBOX, Lifecycle.prototype, 'emit'); + }); + + it('should resume a sandbox process by SandboxProcess ID', async () => { + queryStub.resolves(statusResult); + const sbxProcessId = statusResult.records[0].Id; + + try { + await shouldThrow(prod.resumeSandbox({ SandboxProcessObjId: sbxProcessId })); + } catch (err) { + // Expect a "SandboxCreateNotCompleteError" since the status is Pending + const error = err as SfError; + expect(error.name).to.equal('SandboxCreateNotCompleteError'); + expect(queryStub.firstCall.firstArg).to.equal(format(expectedSoql, `Id='${sbxProcessId}'`)); + expect(pollStatusAndAuthSpy.called).to.be.false; + expect(lifecycleSpy.calledWith(SandboxEvents.EVENT_RESUME, statusResult.records[0])).to.be.true; + expect(lifecycleSpy.calledWith(SandboxEvents.EVENT_ASYNC_RESULT, statusResult.records[0])).to.be.true; + } + }); + + it('should resume a sandbox process by SandboxName', async () => { + queryStub.resolves(statusResult); + const sbxName = statusResult.records[0].SandboxName; + + try { + await shouldThrow(prod.resumeSandbox({ SandboxName: sbxName })); + } catch (err) { + // Expect a "SandboxCreateNotCompleteError" since the status is Pending + const error = err as SfError; + expect(error.name).to.equal('SandboxCreateNotCompleteError'); + expect(queryStub.firstCall.firstArg).to.equal(format(expectedSoql, `SandboxName='${sbxName}'`)); + expect(pollStatusAndAuthSpy.called).to.be.false; + expect(lifecycleSpy.calledWith(SandboxEvents.EVENT_RESUME, statusResult.records[0])).to.be.true; + expect(lifecycleSpy.calledWith(SandboxEvents.EVENT_ASYNC_RESULT, statusResult.records[0])).to.be.true; + } + }); + + it('should resume a sandbox process by SandboxName that returns multiple SandboxProcesses', async () => { + const completedSbxProcess = Object.assign({}, statusResult.records[0], { + CreatedDate: '2023-09-25T20:50:26.000+0000', + Status: 'Completed', + }); + const pendingSbxProcess = Object.assign({}, statusResult.records[0]); + queryStub.resolves({ records: [pendingSbxProcess, completedSbxProcess] }); + const sbxName = statusResult.records[0].SandboxName; + + try { + await shouldThrow(prod.resumeSandbox({ SandboxName: sbxName })); + } catch (err) { + // Expect a "SandboxCreateNotCompleteError" since the status is Pending + const error = err as SfError; + expect(error.name).to.equal('SandboxCreateNotCompleteError'); + expect(queryStub.firstCall.firstArg).to.equal(format(expectedSoql, `SandboxName='${sbxName}'`)); + expect(pollStatusAndAuthSpy.called).to.be.false; + expect(lifecycleSpy.calledWith(SandboxEvents.EVENT_RESUME, pendingSbxProcess)).to.be.true; + expect(lifecycleSpy.calledWith(SandboxEvents.EVENT_ASYNC_RESULT, pendingSbxProcess)).to.be.true; + + // Ensure this event is fired with the correct payload, since it communicates all the + // SandboxProcesses that can be resumed to any listeners, such as the `org resume sandbox` command. + expect( + lifecycleSpy.calledWith(SandboxEvents.EVENT_MULTIPLE_SBX_PROCESSES, [ + pendingSbxProcess, + completedSbxProcess, + ]) + ).to.be.true; + } + }); + + it('should resume a sandbox process by ID and poll by ID', async () => { + queryStub.resolves(statusResult); + const querySbxProcessIdSpy = spyMethod($$.SANDBOX, prod, 'querySandboxProcessById'); + const sbxProcessId = statusResult.records[0].Id; + + try { + await shouldThrow( + prod.resumeSandbox( + { SandboxProcessObjId: sbxProcessId }, + { wait: Duration.milliseconds(500), interval: Duration.milliseconds(100) } + ) + ); + } catch (err) { + // Expect a client timed out error + const error = err as SfError; + expect(error.name).to.equal('PollingClientTimeout'); + expect(queryStub.firstCall.firstArg).to.equal(format(expectedSoql, `Id='${sbxProcessId}'`)); + expect(queryStub.secondCall.firstArg).to.equal(format(expectedSoql, `Id='${sbxProcessId}'`)); + expect(pollStatusAndAuthSpy.called).to.be.true; + expect(querySbxProcessIdSpy.called).to.be.true; + expect(querySbxProcessIdSpy.firstCall.firstArg).to.equal(statusResult.records[0].Id); + expect(lifecycleSpy.calledWith(SandboxEvents.EVENT_RESUME, statusResult.records[0])).to.be.true; + expect(lifecycleSpy.calledWith(SandboxEvents.EVENT_STATUS)).to.be.true; + } + }); + + it('should resume a sandbox process by Name and poll by ID', async () => { + queryStub.resolves(statusResult); + const querySbxProcessIdSpy = spyMethod($$.SANDBOX, prod, 'querySandboxProcessById'); + const sbxProcessId = statusResult.records[0].Id; + const sbxName = statusResult.records[0].SandboxName; + + try { + await shouldThrow( + prod.resumeSandbox( + { SandboxName: sbxName }, + { wait: Duration.milliseconds(500), interval: Duration.milliseconds(100) } + ) + ); + } catch (err) { + // Expect a client timed out error + const error = err as SfError; + expect(error.name).to.equal('PollingClientTimeout'); + expect(queryStub.firstCall.firstArg).to.equal(format(expectedSoql, `SandboxName='${sbxName}'`)); + expect(queryStub.secondCall.firstArg).to.equal(format(expectedSoql, `Id='${sbxProcessId}'`)); + expect(pollStatusAndAuthSpy.called).to.be.true; + expect(querySbxProcessIdSpy.called).to.be.true; + expect(querySbxProcessIdSpy.firstCall.firstArg).to.equal(statusResult.records[0].Id); + expect(lifecycleSpy.calledWith(SandboxEvents.EVENT_RESUME, statusResult.records[0])).to.be.true; + expect(lifecycleSpy.calledWith(SandboxEvents.EVENT_STATUS)).to.be.true; + } + }); + }); + + describe('sandboxStatus', () => { + let queryStub: SinonStub; + let pollStatusAndAuthStub: SinonStub; + const sandboxNameIn = 'test-sandbox'; + const queryStr = `SELECT Id, Status, SandboxName, SandboxInfoId, LicenseType, CreatedDate, CopyProgress, SandboxOrganization, SourceId, Description, EndDate FROM SandboxProcess WHERE SandboxName='${sandboxNameIn}' AND Status != 'D' ORDER BY CreatedDate DESC LIMIT 1`; + + beforeEach(async () => { + queryStub = stubMethod($$.SANDBOX, prod.getConnection().tooling, 'query'); + pollStatusAndAuthStub = stubMethod($$.SANDBOX, prod, 'pollStatusAndAuth').resolves(statusResult.records[0]); + }); + + it('should return sandbox status', async () => { + queryStub.resolves(statusResult); + const result = await prod.sandboxStatus(sandboxNameIn, { wait: Duration.minutes(10) }); expect(queryStub.calledOnce).to.be.true; expect(queryStub.firstCall.firstArg).to.be.equal(queryStr); - expect(pollStatusAndAuthStub.called).to.be.false; - } + expect(pollStatusAndAuthStub.calledOnce).to.be.true; + expect(result).to.be.equal(statusResult.records[0]); + }); + + it('should fail when query returns empty records for the sandbox', async () => { + queryStub.resolves({ records: [] }); + try { + await shouldThrow(prod.sandboxStatus(sandboxNameIn, { wait: Duration.minutes(10) })); + } catch (e) { + expect((e as Error).message).to.be.equal( + messages.getMessage('SandboxProcessNotFoundBySandboxName', [sandboxNameIn]) + ); + expect(queryStub.calledOnce).to.be.true; + expect(queryStub.firstCall.firstArg).to.be.equal(queryStr); + expect(pollStatusAndAuthStub.called).to.be.false; + } + }); + + it('should fail when query returns multiple records for the sandbox', async () => { + queryStub.resolves({ records: [...statusResult.records, ...statusResult.records] }); + try { + await shouldThrow(prod.sandboxStatus(sandboxNameIn, { wait: Duration.minutes(10) })); + } catch (e) { + expect((e as Error).message).to.be.equal( + messages.getMessage('MultiSandboxProcessNotFoundBySandboxName', [sandboxNameIn]) + ); + expect(queryStub.calledOnce).to.be.true; + expect(queryStub.firstCall.firstArg).to.be.equal(queryStr); + expect(pollStatusAndAuthStub.called).to.be.false; + } + }); }); - it('should fail when query returns multiple records for the sandbox', async () => { - queryStub.restore(); - queryStub = stubMethod($$.SANDBOX, prod.getConnection().tooling, 'query').resolves({ - records: [...statusResult.records, ...statusResult.records], + describe('querySandboxProcess', () => { + let queryStub: SinonStub; + + beforeEach(async () => { + queryStub = stubMethod($$.SANDBOX, prod.getConnection().tooling, 'query'); }); - try { - await shouldThrow(prod.sandboxStatus(sandboxNameIn, { wait: Duration.minutes(10) })); - } catch (e) { - expect((e as Error).message).to.be.equal( - messages.getMessage('MultiSandboxProcessNotFoundBySandboxName', [sandboxNameIn]) - ); + + it('removes SandboxProcesses of Deleting or Deleted', async () => { + const deletingSbxProcess = Object.assign({}, statusResult.records[0], { Status: 'Deleting' }); + const deletedSbxProcess = Object.assign({}, statusResult.records[0], { Status: 'Deleted' }); + queryStub.resolves({ records: [deletingSbxProcess, statusResult.records[0], deletedSbxProcess] }); + const where = 'name="foo"'; + const expectedSoql = `SELECT Id, Status, SandboxName, SandboxInfoId, LicenseType, CreatedDate, CopyProgress, SandboxOrganization, SourceId, Description, EndDate FROM SandboxProcess WHERE ${where} ORDER BY CreatedDate DESC`; + + // @ts-ignore Testing a private method + const sbxProcess = await prod.querySandboxProcess(where); + expect(sbxProcess).to.deep.equal(statusResult.records[0]); expect(queryStub.calledOnce).to.be.true; - expect(queryStub.firstCall.firstArg).to.be.equal(queryStr); - expect(pollStatusAndAuthStub.called).to.be.false; - } + expect(queryStub.firstCall.firstArg).to.equal(expectedSoql); + }); + + it('should throw error when no records found', async () => { + queryStub.resolves({ records: [] }); + try { + // @ts-ignore Testing a private method + await shouldThrow(prod.querySandboxProcess('')); + } catch (e) { + expect(queryStub.calledOnce).to.be.true; + const err = e as SfError; + expect(err.name).to.equal(SingleRecordQueryErrors.NoRecords); + expect(err.message).to.include('No record found for'); + } + }); + + it('should throw error when multiple found and provide the SbxProcesses on err.data, sorting DESC', async () => { + const sbxProcess0925 = Object.assign({}, statusResult.records[0], { + CreatedDate: '2023-09-25T20:50:26.000+0000', + }); + const sbxProcess0927 = Object.assign({}, statusResult.records[0]); + const sbxProcess0930 = Object.assign({}, statusResult.records[0], { + CreatedDate: '2023-09-30T20:50:26.000+0000', + }); + const expectedSbxProcesses = [sbxProcess0930, sbxProcess0927, sbxProcess0925]; + queryStub.resolves({ records: expectedSbxProcesses }); + + try { + // @ts-ignore Testing a private method + await shouldThrow(prod.querySandboxProcess('')); + } catch (e) { + expect(queryStub.calledOnce).to.be.true; + const err = e as SfError; + expect(err.name).to.equal(SingleRecordQueryErrors.MultipleRecords); + expect(err.message).to.equal('The query returned more than 1 record'); + expect(err.data).to.deep.equal(expectedSbxProcesses); + } + }); }); });