Skip to content

Commit

Permalink
Merge pull request #823 from salesforcecli/sh/improved-resume-sandbox
Browse files Browse the repository at this point in the history
Sh/improved resume sandbox
  • Loading branch information
shetzel authored Oct 5, 2023
2 parents 2ddebde + 1d5e388 commit afd2c57
Show file tree
Hide file tree
Showing 8 changed files with 174 additions and 44 deletions.
6 changes: 5 additions & 1 deletion messages/sandboxbase.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,8 @@ If the org is ready, checking the status also authorizes the org for use with Sa

# warning.ClientTimeoutWaitingForSandboxCreate

The wait time for the sandbox creation has been exhausted. Please see the results below for more information.
The wait time for the sandbox creation has been exhausted. See the results below for more information.

# warning.MultipleMatchingSandboxProcesses

We found multiple sandbox processes for "%s" in a resumable state. We're ignoring the sandbox process ID(s) "%s" in status(es) "%s" and using the most recent process ID "%s". To resume a different sandbox process, use that unique sandbox process ID with the command. For example, "sf org resume sandbox --job-id %s -o %s".
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"bugs": "https://github.com/forcedotcom/cli/issues",
"dependencies": {
"@oclif/core": "^2.15.0",
"@salesforce/core": "^5.2.0",
"@salesforce/core": "^5.3.4",
"@salesforce/kit": "^3.0.9",
"@salesforce/sf-plugins-core": "^3.1.22",
"@salesforce/source-deploy-retrieve": "^9.7.13",
Expand Down
6 changes: 3 additions & 3 deletions src/commands/org/resume/sandbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ export default class ResumeSandbox extends SandboxCommandBase<SandboxProcessObje
if (latestEntry) {
const [, sandboxRequestData] = latestEntry;
if (sandboxRequestData) {
return { SandboxName: sandboxRequestData.sandboxProcessObject?.SandboxName };
return { SandboxProcessObjId: sandboxRequestData.sandboxProcessObject?.Id };
}
}
}
Expand Down Expand Up @@ -130,7 +130,7 @@ export default class ResumeSandbox extends SandboxCommandBase<SandboxProcessObje
(await this.verifyIfAuthExists({
prodOrg: this.prodOrg,
sandboxName: this.sandboxRequestData.sandboxProcessObject.SandboxName,
jobId: this.flags['job-id'],
jobId: this.flags['job-id'] ?? this.sandboxRequestData.sandboxProcessObject.Id,
lifecycle,
}))
) {
Expand Down Expand Up @@ -228,7 +228,7 @@ const getSandboxProcessObject = async (
sandboxName?: string,
jobId?: string
): Promise<SandboxProcessObject> => {
const where = sandboxName ? `SandboxName='${sandboxName}'` : `Id='${jobId}'`;
const where = jobId ? `Id='${jobId}'` : `SandboxName='${sandboxName}'`;
const queryStr = `SELECT Id, Status, SandboxName, SandboxInfoId, LicenseType, CreatedDate, CopyProgress, SandboxOrganization, SourceId, Description, EndDate FROM SandboxProcess WHERE ${where} AND Status != 'D'`;
try {
return await prodOrg.getConnection().singleRecordQuery(queryStr, {
Expand Down
32 changes: 32 additions & 0 deletions src/shared/sandboxCommandBase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,24 @@ export abstract class SandboxCommandBase<T> extends SfCommand<T> {
this.updateProgress(results, options.isAsync);
this.reportResults(results);
});

lifecycle.on(SandboxEvents.EVENT_MULTIPLE_SBX_PROCESSES, async (results: SandboxProcessObject[]) => {
const [resumingProcess, ...otherSbxProcesses] = results;
const sbxProcessIds = otherSbxProcesses.map((sbxProcess) => sbxProcess.Id);
const sbxProcessStatuses = otherSbxProcesses.map((sbxProcess) => sbxProcess.Status);

this.warn(
messages.getMessage('warning.MultipleMatchingSandboxProcesses', [
otherSbxProcesses[0].SandboxName,
sbxProcessIds.toString(),
sbxProcessStatuses.toString(),
resumingProcess.Id,
sbxProcessIds[0],
this.prodOrg?.getUsername(),
])
);
return Promise.resolve();
});
}

protected reportResults(results: ResultEvent): void {
Expand Down Expand Up @@ -204,6 +222,20 @@ export abstract class SandboxCommandBase<T> extends SfCommand<T> {
}
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
protected async finally(_: Error | undefined): Promise<any> {
const lifecycle = Lifecycle.getInstance();
lifecycle.removeAllListeners('POLLING_TIME_OUT');
lifecycle.removeAllListeners(SandboxEvents.EVENT_RESUME);
lifecycle.removeAllListeners(SandboxEvents.EVENT_ASYNC_RESULT);
lifecycle.removeAllListeners(SandboxEvents.EVENT_STATUS);
lifecycle.removeAllListeners(SandboxEvents.EVENT_AUTH);
lifecycle.removeAllListeners(SandboxEvents.EVENT_RESULT);
lifecycle.removeAllListeners(SandboxEvents.EVENT_MULTIPLE_SBX_PROCESSES);

return super.finally(_);
}

private removeSandboxProgressConfig(): void {
if (this.latestSandboxProgressObj?.SandboxName) {
this.sandboxRequestConfig.unset(this.latestSandboxProgressObj.SandboxName);
Expand Down
5 changes: 1 addition & 4 deletions test/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,6 @@
"skipLibCheck": true,
"strictNullChecks": true,
"baseUrl": "../",
"sourceMap": true,
"paths": {
"@salesforce/kit": ["node_modules/@salesforce/kit"]
}
"sourceMap": true
}
}
23 changes: 8 additions & 15 deletions test/unit/org/createSandbox.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,6 @@ const fakeOrg: AuthFields = {

describe('org:create:sandbox', () => {
beforeEach(() => {
// stubMethod(sandbox, OrgAccessor.prototype, 'read').callsFake(async (): Promise<AuthFields> => fakeOrg);
// stubMethod(sandbox, OrgAccessor.prototype, 'write').callsFake(async (): Promise<AuthFields> => fakeOrg);
stubMethod(sandbox, OrgAccessor.prototype, 'read').resolves(fakeOrg);
stubMethod(sandbox, OrgAccessor.prototype, 'write').resolves(fakeOrg);
sfCommandUxStubs = stubSfCommandUx(sandbox);
Expand All @@ -53,23 +51,18 @@ describe('org:create:sandbox', () => {
it('will print the correct message for asyncResult lifecycle event', async () => {
stubMethod(sandbox, Org, 'create').resolves(Org.prototype);
stubMethod(sandbox, Org.prototype, 'getUsername').returns('testProdOrg');
const createStub = stubMethod(sandbox, Org.prototype, 'createSandbox').callsFake(async () =>
(async () => {})().catch()
);
const createStub = stubMethod(sandbox, Org.prototype, 'createSandbox').callsFake(async () => {
await Lifecycle.getInstance().emit(SandboxEvents.EVENT_ASYNC_RESULT, sandboxProcessObj);
});

await CreateSandbox.run(['-o', 'testProdOrg', '--name', 'mysandboxx', '--no-prompt']);

expect(createStub.firstCall.args[0].SandboxName).includes('mysandboxx');
expect(createStub.firstCall.args[0].SandboxName.length).equals(10);

Lifecycle.getInstance().on(SandboxEvents.EVENT_ASYNC_RESULT, async (result) => {
expect(result).to.deep.equal(sandboxProcessObj);
expect(sfCommandUxStubs.info.firstCall.firstArg).to.include(sandboxProcessObj.Id);
return Promise.resolve();
expect(createStub.firstCall.firstArg).to.deep.equal({
SandboxName: 'mysandboxx',
LicenseType: 'Developer',
});

await Lifecycle.getInstance().emit(SandboxEvents.EVENT_ASYNC_RESULT, sandboxProcessObj);
Lifecycle.getInstance().removeAllListeners(SandboxEvents.EVENT_ASYNC_RESULT);
const expectedInfoMsg = `org resume sandbox --job-id ${sandboxProcessObj.Id} -o testProdOrg`;
expect(sfCommandUxStubs.info.firstCall.firstArg).to.include(expectedInfoMsg);
});
});

Expand Down
117 changes: 117 additions & 0 deletions test/unit/org/resumeSandbox.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
/*
* Copyright (c) 2020, 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
*/
import {
Lifecycle,
Messages,
Org,
SandboxEvents,
SandboxProcessObject,
AuthFields,
SandboxRequestCacheEntry,
SfError,
} from '@salesforce/core';
import { stubMethod } from '@salesforce/ts-sinon';
import * as sinon from 'sinon';
import { expect, config } from 'chai';
import { OrgAccessor } from '@salesforce/core/lib/stateAggregator';
import { stubSfCommandUx, stubSpinner, stubUx } from '@salesforce/sf-plugins-core';
import ResumeSandbox from '../../../src/commands/org/resume/sandbox';

config.truncateThreshold = 0;

const prodOrgUsername = '[email protected]';
const sandboxName = 'TestSbx';
const fakeOrg: AuthFields = {
orgId: '00Dsomefakeorg1',
instanceUrl: 'https://some.fake.org',
username: prodOrgUsername,
};

const sandboxProcessObj: SandboxProcessObject = {
Id: '0GR4p000000U8EMXXX',
Status: 'Completed',
SandboxName: sandboxName,
SandboxInfoId: '0GQ4p000000U6sKXXX',
LicenseType: 'DEVELOPER',
CreatedDate: '2021-12-07T16:20:21.000+0000',
CopyProgress: 100,
SandboxOrganization: '00D2f0000008XXX',
SourceId: '123',
Description: 'sandbox description',
ApexClassId: '123',
EndDate: '2021-12-07T16:38:47.000+0000',
};

const sandboxRequestData: SandboxRequestCacheEntry = {
prodOrgUsername,
sandboxRequest: {},
sandboxProcessObject: {
SandboxName: sandboxName,
},
setDefault: false,
};

describe('[org resume sandbox]', () => {
Messages.importMessagesDirectory(__dirname);
const messages = Messages.loadMessages('@salesforce/plugin-org', 'sandboxbase');

const sandbox = sinon.createSandbox();

beforeEach(() => {
stubMethod(sandbox, OrgAccessor.prototype, 'read').resolves(fakeOrg);
stubMethod(sandbox, OrgAccessor.prototype, 'write').resolves(fakeOrg);
sfCommandUxStubs = stubSfCommandUx(sandbox);
stubUx(sandbox);
stubSpinner(sandbox);
});

let sfCommandUxStubs: ReturnType<typeof stubSfCommandUx>;

it('will warn when multiple sandboxes are in a resumable state', async () => {
stubMethod(sandbox, ResumeSandbox.prototype, 'getSandboxRequestConfig').resolves();
stubMethod(sandbox, ResumeSandbox.prototype, 'buildSandboxRequestCacheEntry').returns(sandboxRequestData);
stubMethod(sandbox, ResumeSandbox.prototype, 'createResumeSandboxRequest').returns({
SandboxName: sandboxName,
});
stubMethod(sandbox, Org, 'create').resolves(Org.prototype);
stubMethod(sandbox, Org.prototype, 'getUsername').returns(prodOrgUsername);
const inProgSandboxProcessObj = Object.assign({}, sandboxProcessObj, {
Status: 'In Progress',
Id: '0GR4p000000U8EMZZZ',
CopyProgress: 25,
CreatedDate: '2022-12-07T16:20:21.000+0000',
});
stubMethod(sandbox, Org.prototype, 'resumeSandbox').callsFake(async () => {
await Lifecycle.getInstance().emit(SandboxEvents.EVENT_MULTIPLE_SBX_PROCESSES, [
inProgSandboxProcessObj,
sandboxProcessObj,
]);
throw new SfError('sbx create not complete', 'sandboxCreateNotComplete');
});

try {
await ResumeSandbox.run(['-o', prodOrgUsername, '--name', sandboxName]);
expect(false, 'ResumeSandbox should have thrown sandboxCreateNotComplete');
} catch (err: unknown) {
const warningMsg = messages.getMessage('warning.MultipleMatchingSandboxProcesses', [
sandboxName,
sandboxProcessObj.Id,
sandboxProcessObj.Status,
inProgSandboxProcessObj.Id,
sandboxProcessObj.Id,
prodOrgUsername,
]);
expect(sfCommandUxStubs.warn.calledWith(warningMsg)).to.be.true;
const error = err as SfError;
expect(error.name).to.equal('sandboxCreateNotComplete');
}
});

afterEach(() => {
sandbox.restore();
});
});
27 changes: 7 additions & 20 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -885,14 +885,14 @@
strip-ansi "6.0.1"
ts-retry-promise "^0.7.1"

"@salesforce/core@^5.2.0", "@salesforce/core@^5.2.10", "@salesforce/core@^5.2.7", "@salesforce/core@^5.2.9", "@salesforce/core@^5.3.1":
version "5.3.2"
resolved "https://registry.yarnpkg.com/@salesforce/core/-/core-5.3.2.tgz#7c930bd1f2d69980b458bff1323379daf26f0a8f"
integrity sha512-PhaboMJkitqJsKrp+VfAHEp9/q57/n9zqKDdO7ML2qHb6oiRgxhyBWC9N02sOxPiFKreiKKSbR1tnsk40T+oAw==
"@salesforce/core@^5.2.0", "@salesforce/core@^5.2.10", "@salesforce/core@^5.2.7", "@salesforce/core@^5.2.9", "@salesforce/core@^5.3.1", "@salesforce/core@^5.3.4":
version "5.3.4"
resolved "https://registry.yarnpkg.com/@salesforce/core/-/core-5.3.4.tgz#bbf87238a4a6f31c07c8d01d4913c552fe0d0443"
integrity sha512-FV5QG0+W/15IaWtaZCxzAZ1K6bonlzkv5v9vuicH2VeEltruGUYYf8Bq7hkoul1xHk3Kl4+9cXx7aGp8Z0kGzQ==
dependencies:
"@salesforce/kit" "^3.0.12"
"@salesforce/schemas" "^1.6.0"
"@salesforce/ts-types" "^2.0.7"
"@salesforce/ts-types" "^2.0.8"
"@types/semver" "^7.5.2"
ajv "^8.12.0"
change-case "^4.1.2"
Expand Down Expand Up @@ -1356,12 +1356,7 @@
dependencies:
"@types/node" "*"

"@types/semver@^7.3.12":
version "7.5.0"
resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.0.tgz#591c1ce3a702c45ee15f47a42ade72c2fd78978a"
integrity sha512-G8hZ6XJiHnuhQKR7ZmysCeJWE08o8T0AXtk5darsCaTVsYZhhgUrq53jizaR2FvsoeCwJhlmwTjkXBY5Pn/ZHw==

"@types/semver@^7.5.2":
"@types/semver@^7.3.12", "@types/semver@^7.5.2":
version "7.5.2"
resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.2.tgz#31f6eec1ed7ec23f4f05608d3a2d381df041f564"
integrity sha512-7aqorHYgdNO4DM36stTiGO3DvKoex9TQRwsJU6vMaFGyqpBA1MNZkz+PG3gaNUPpTAOYhT1WR7M1JyA3fbS9Cw==
Expand Down Expand Up @@ -6044,15 +6039,7 @@ pify@^4.0.1:
resolved "https://registry.yarnpkg.com/pify/-/pify-4.0.1.tgz#4b2cd25c50d598735c50292224fd8c6df41e3231"
integrity sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==

pino-abstract-transport@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/pino-abstract-transport/-/pino-abstract-transport-1.0.0.tgz#cc0d6955fffcadb91b7b49ef220a6cc111d48bb3"
integrity sha512-c7vo5OpW4wIS42hUVcT5REsL8ZljsUfBjqV/e2sFxmFEFZiq1XLUp5EYLtuDH6PEHq9W1egWqRbnLUP5FuZmOA==
dependencies:
readable-stream "^4.0.0"
split2 "^4.0.0"

[email protected]:
pino-abstract-transport@^1.0.0, [email protected]:
version "1.1.0"
resolved "https://registry.yarnpkg.com/pino-abstract-transport/-/pino-abstract-transport-1.1.0.tgz#083d98f966262164504afb989bccd05f665937a8"
integrity sha512-lsleG3/2a/JIWUtf9Q5gUNErBqwIu1tUKTT3dUzaf5DySw9ra1wcqKjJjLX1VTY64Wk1eEOYsVGSaGfCK85ekA==
Expand Down

0 comments on commit afd2c57

Please sign in to comment.