Skip to content

Commit afd2c57

Browse files
authored
Merge pull request #823 from salesforcecli/sh/improved-resume-sandbox
Sh/improved resume sandbox
2 parents 2ddebde + 1d5e388 commit afd2c57

File tree

8 files changed

+174
-44
lines changed

8 files changed

+174
-44
lines changed

messages/sandboxbase.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,8 @@ If the org is ready, checking the status also authorizes the org for use with Sa
1414

1515
# warning.ClientTimeoutWaitingForSandboxCreate
1616

17-
The wait time for the sandbox creation has been exhausted. Please see the results below for more information.
17+
The wait time for the sandbox creation has been exhausted. See the results below for more information.
18+
19+
# warning.MultipleMatchingSandboxProcesses
20+
21+
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".

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
"bugs": "https://github.com/forcedotcom/cli/issues",
88
"dependencies": {
99
"@oclif/core": "^2.15.0",
10-
"@salesforce/core": "^5.2.0",
10+
"@salesforce/core": "^5.3.4",
1111
"@salesforce/kit": "^3.0.9",
1212
"@salesforce/sf-plugins-core": "^3.1.22",
1313
"@salesforce/source-deploy-retrieve": "^9.7.13",

src/commands/org/resume/sandbox.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ export default class ResumeSandbox extends SandboxCommandBase<SandboxProcessObje
9393
if (latestEntry) {
9494
const [, sandboxRequestData] = latestEntry;
9595
if (sandboxRequestData) {
96-
return { SandboxName: sandboxRequestData.sandboxProcessObject?.SandboxName };
96+
return { SandboxProcessObjId: sandboxRequestData.sandboxProcessObject?.Id };
9797
}
9898
}
9999
}
@@ -130,7 +130,7 @@ export default class ResumeSandbox extends SandboxCommandBase<SandboxProcessObje
130130
(await this.verifyIfAuthExists({
131131
prodOrg: this.prodOrg,
132132
sandboxName: this.sandboxRequestData.sandboxProcessObject.SandboxName,
133-
jobId: this.flags['job-id'],
133+
jobId: this.flags['job-id'] ?? this.sandboxRequestData.sandboxProcessObject.Id,
134134
lifecycle,
135135
}))
136136
) {
@@ -228,7 +228,7 @@ const getSandboxProcessObject = async (
228228
sandboxName?: string,
229229
jobId?: string
230230
): Promise<SandboxProcessObject> => {
231-
const where = sandboxName ? `SandboxName='${sandboxName}'` : `Id='${jobId}'`;
231+
const where = jobId ? `Id='${jobId}'` : `SandboxName='${sandboxName}'`;
232232
const queryStr = `SELECT Id, Status, SandboxName, SandboxInfoId, LicenseType, CreatedDate, CopyProgress, SandboxOrganization, SourceId, Description, EndDate FROM SandboxProcess WHERE ${where} AND Status != 'D'`;
233233
try {
234234
return await prodOrg.getConnection().singleRecordQuery(queryStr, {

src/shared/sandboxCommandBase.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,24 @@ export abstract class SandboxCommandBase<T> extends SfCommand<T> {
150150
this.updateProgress(results, options.isAsync);
151151
this.reportResults(results);
152152
});
153+
154+
lifecycle.on(SandboxEvents.EVENT_MULTIPLE_SBX_PROCESSES, async (results: SandboxProcessObject[]) => {
155+
const [resumingProcess, ...otherSbxProcesses] = results;
156+
const sbxProcessIds = otherSbxProcesses.map((sbxProcess) => sbxProcess.Id);
157+
const sbxProcessStatuses = otherSbxProcesses.map((sbxProcess) => sbxProcess.Status);
158+
159+
this.warn(
160+
messages.getMessage('warning.MultipleMatchingSandboxProcesses', [
161+
otherSbxProcesses[0].SandboxName,
162+
sbxProcessIds.toString(),
163+
sbxProcessStatuses.toString(),
164+
resumingProcess.Id,
165+
sbxProcessIds[0],
166+
this.prodOrg?.getUsername(),
167+
])
168+
);
169+
return Promise.resolve();
170+
});
153171
}
154172

155173
protected reportResults(results: ResultEvent): void {
@@ -204,6 +222,20 @@ export abstract class SandboxCommandBase<T> extends SfCommand<T> {
204222
}
205223
}
206224

225+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
226+
protected async finally(_: Error | undefined): Promise<any> {
227+
const lifecycle = Lifecycle.getInstance();
228+
lifecycle.removeAllListeners('POLLING_TIME_OUT');
229+
lifecycle.removeAllListeners(SandboxEvents.EVENT_RESUME);
230+
lifecycle.removeAllListeners(SandboxEvents.EVENT_ASYNC_RESULT);
231+
lifecycle.removeAllListeners(SandboxEvents.EVENT_STATUS);
232+
lifecycle.removeAllListeners(SandboxEvents.EVENT_AUTH);
233+
lifecycle.removeAllListeners(SandboxEvents.EVENT_RESULT);
234+
lifecycle.removeAllListeners(SandboxEvents.EVENT_MULTIPLE_SBX_PROCESSES);
235+
236+
return super.finally(_);
237+
}
238+
207239
private removeSandboxProgressConfig(): void {
208240
if (this.latestSandboxProgressObj?.SandboxName) {
209241
this.sandboxRequestConfig.unset(this.latestSandboxProgressObj.SandboxName);

test/tsconfig.json

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,6 @@
55
"skipLibCheck": true,
66
"strictNullChecks": true,
77
"baseUrl": "../",
8-
"sourceMap": true,
9-
"paths": {
10-
"@salesforce/kit": ["node_modules/@salesforce/kit"]
11-
}
8+
"sourceMap": true
129
}
1310
}

test/unit/org/createSandbox.test.ts

Lines changed: 8 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,6 @@ const fakeOrg: AuthFields = {
3737

3838
describe('org:create:sandbox', () => {
3939
beforeEach(() => {
40-
// stubMethod(sandbox, OrgAccessor.prototype, 'read').callsFake(async (): Promise<AuthFields> => fakeOrg);
41-
// stubMethod(sandbox, OrgAccessor.prototype, 'write').callsFake(async (): Promise<AuthFields> => fakeOrg);
4240
stubMethod(sandbox, OrgAccessor.prototype, 'read').resolves(fakeOrg);
4341
stubMethod(sandbox, OrgAccessor.prototype, 'write').resolves(fakeOrg);
4442
sfCommandUxStubs = stubSfCommandUx(sandbox);
@@ -53,23 +51,18 @@ describe('org:create:sandbox', () => {
5351
it('will print the correct message for asyncResult lifecycle event', async () => {
5452
stubMethod(sandbox, Org, 'create').resolves(Org.prototype);
5553
stubMethod(sandbox, Org.prototype, 'getUsername').returns('testProdOrg');
56-
const createStub = stubMethod(sandbox, Org.prototype, 'createSandbox').callsFake(async () =>
57-
(async () => {})().catch()
58-
);
54+
const createStub = stubMethod(sandbox, Org.prototype, 'createSandbox').callsFake(async () => {
55+
await Lifecycle.getInstance().emit(SandboxEvents.EVENT_ASYNC_RESULT, sandboxProcessObj);
56+
});
5957

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

62-
expect(createStub.firstCall.args[0].SandboxName).includes('mysandboxx');
63-
expect(createStub.firstCall.args[0].SandboxName.length).equals(10);
64-
65-
Lifecycle.getInstance().on(SandboxEvents.EVENT_ASYNC_RESULT, async (result) => {
66-
expect(result).to.deep.equal(sandboxProcessObj);
67-
expect(sfCommandUxStubs.info.firstCall.firstArg).to.include(sandboxProcessObj.Id);
68-
return Promise.resolve();
60+
expect(createStub.firstCall.firstArg).to.deep.equal({
61+
SandboxName: 'mysandboxx',
62+
LicenseType: 'Developer',
6963
});
70-
71-
await Lifecycle.getInstance().emit(SandboxEvents.EVENT_ASYNC_RESULT, sandboxProcessObj);
72-
Lifecycle.getInstance().removeAllListeners(SandboxEvents.EVENT_ASYNC_RESULT);
64+
const expectedInfoMsg = `org resume sandbox --job-id ${sandboxProcessObj.Id} -o testProdOrg`;
65+
expect(sfCommandUxStubs.info.firstCall.firstArg).to.include(expectedInfoMsg);
7366
});
7467
});
7568

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
/*
2+
* Copyright (c) 2020, salesforce.com, inc.
3+
* All rights reserved.
4+
* Licensed under the BSD 3-Clause license.
5+
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
6+
*/
7+
import {
8+
Lifecycle,
9+
Messages,
10+
Org,
11+
SandboxEvents,
12+
SandboxProcessObject,
13+
AuthFields,
14+
SandboxRequestCacheEntry,
15+
SfError,
16+
} from '@salesforce/core';
17+
import { stubMethod } from '@salesforce/ts-sinon';
18+
import * as sinon from 'sinon';
19+
import { expect, config } from 'chai';
20+
import { OrgAccessor } from '@salesforce/core/lib/stateAggregator';
21+
import { stubSfCommandUx, stubSpinner, stubUx } from '@salesforce/sf-plugins-core';
22+
import ResumeSandbox from '../../../src/commands/org/resume/sandbox';
23+
24+
config.truncateThreshold = 0;
25+
26+
const prodOrgUsername = '[email protected]';
27+
const sandboxName = 'TestSbx';
28+
const fakeOrg: AuthFields = {
29+
orgId: '00Dsomefakeorg1',
30+
instanceUrl: 'https://some.fake.org',
31+
username: prodOrgUsername,
32+
};
33+
34+
const sandboxProcessObj: SandboxProcessObject = {
35+
Id: '0GR4p000000U8EMXXX',
36+
Status: 'Completed',
37+
SandboxName: sandboxName,
38+
SandboxInfoId: '0GQ4p000000U6sKXXX',
39+
LicenseType: 'DEVELOPER',
40+
CreatedDate: '2021-12-07T16:20:21.000+0000',
41+
CopyProgress: 100,
42+
SandboxOrganization: '00D2f0000008XXX',
43+
SourceId: '123',
44+
Description: 'sandbox description',
45+
ApexClassId: '123',
46+
EndDate: '2021-12-07T16:38:47.000+0000',
47+
};
48+
49+
const sandboxRequestData: SandboxRequestCacheEntry = {
50+
prodOrgUsername,
51+
sandboxRequest: {},
52+
sandboxProcessObject: {
53+
SandboxName: sandboxName,
54+
},
55+
setDefault: false,
56+
};
57+
58+
describe('[org resume sandbox]', () => {
59+
Messages.importMessagesDirectory(__dirname);
60+
const messages = Messages.loadMessages('@salesforce/plugin-org', 'sandboxbase');
61+
62+
const sandbox = sinon.createSandbox();
63+
64+
beforeEach(() => {
65+
stubMethod(sandbox, OrgAccessor.prototype, 'read').resolves(fakeOrg);
66+
stubMethod(sandbox, OrgAccessor.prototype, 'write').resolves(fakeOrg);
67+
sfCommandUxStubs = stubSfCommandUx(sandbox);
68+
stubUx(sandbox);
69+
stubSpinner(sandbox);
70+
});
71+
72+
let sfCommandUxStubs: ReturnType<typeof stubSfCommandUx>;
73+
74+
it('will warn when multiple sandboxes are in a resumable state', async () => {
75+
stubMethod(sandbox, ResumeSandbox.prototype, 'getSandboxRequestConfig').resolves();
76+
stubMethod(sandbox, ResumeSandbox.prototype, 'buildSandboxRequestCacheEntry').returns(sandboxRequestData);
77+
stubMethod(sandbox, ResumeSandbox.prototype, 'createResumeSandboxRequest').returns({
78+
SandboxName: sandboxName,
79+
});
80+
stubMethod(sandbox, Org, 'create').resolves(Org.prototype);
81+
stubMethod(sandbox, Org.prototype, 'getUsername').returns(prodOrgUsername);
82+
const inProgSandboxProcessObj = Object.assign({}, sandboxProcessObj, {
83+
Status: 'In Progress',
84+
Id: '0GR4p000000U8EMZZZ',
85+
CopyProgress: 25,
86+
CreatedDate: '2022-12-07T16:20:21.000+0000',
87+
});
88+
stubMethod(sandbox, Org.prototype, 'resumeSandbox').callsFake(async () => {
89+
await Lifecycle.getInstance().emit(SandboxEvents.EVENT_MULTIPLE_SBX_PROCESSES, [
90+
inProgSandboxProcessObj,
91+
sandboxProcessObj,
92+
]);
93+
throw new SfError('sbx create not complete', 'sandboxCreateNotComplete');
94+
});
95+
96+
try {
97+
await ResumeSandbox.run(['-o', prodOrgUsername, '--name', sandboxName]);
98+
expect(false, 'ResumeSandbox should have thrown sandboxCreateNotComplete');
99+
} catch (err: unknown) {
100+
const warningMsg = messages.getMessage('warning.MultipleMatchingSandboxProcesses', [
101+
sandboxName,
102+
sandboxProcessObj.Id,
103+
sandboxProcessObj.Status,
104+
inProgSandboxProcessObj.Id,
105+
sandboxProcessObj.Id,
106+
prodOrgUsername,
107+
]);
108+
expect(sfCommandUxStubs.warn.calledWith(warningMsg)).to.be.true;
109+
const error = err as SfError;
110+
expect(error.name).to.equal('sandboxCreateNotComplete');
111+
}
112+
});
113+
114+
afterEach(() => {
115+
sandbox.restore();
116+
});
117+
});

yarn.lock

Lines changed: 7 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -885,14 +885,14 @@
885885
strip-ansi "6.0.1"
886886
ts-retry-promise "^0.7.1"
887887

888-
"@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":
889-
version "5.3.2"
890-
resolved "https://registry.yarnpkg.com/@salesforce/core/-/core-5.3.2.tgz#7c930bd1f2d69980b458bff1323379daf26f0a8f"
891-
integrity sha512-PhaboMJkitqJsKrp+VfAHEp9/q57/n9zqKDdO7ML2qHb6oiRgxhyBWC9N02sOxPiFKreiKKSbR1tnsk40T+oAw==
888+
"@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":
889+
version "5.3.4"
890+
resolved "https://registry.yarnpkg.com/@salesforce/core/-/core-5.3.4.tgz#bbf87238a4a6f31c07c8d01d4913c552fe0d0443"
891+
integrity sha512-FV5QG0+W/15IaWtaZCxzAZ1K6bonlzkv5v9vuicH2VeEltruGUYYf8Bq7hkoul1xHk3Kl4+9cXx7aGp8Z0kGzQ==
892892
dependencies:
893893
"@salesforce/kit" "^3.0.12"
894894
"@salesforce/schemas" "^1.6.0"
895-
"@salesforce/ts-types" "^2.0.7"
895+
"@salesforce/ts-types" "^2.0.8"
896896
"@types/semver" "^7.5.2"
897897
ajv "^8.12.0"
898898
change-case "^4.1.2"
@@ -1356,12 +1356,7 @@
13561356
dependencies:
13571357
"@types/node" "*"
13581358

1359-
"@types/semver@^7.3.12":
1360-
version "7.5.0"
1361-
resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.0.tgz#591c1ce3a702c45ee15f47a42ade72c2fd78978a"
1362-
integrity sha512-G8hZ6XJiHnuhQKR7ZmysCeJWE08o8T0AXtk5darsCaTVsYZhhgUrq53jizaR2FvsoeCwJhlmwTjkXBY5Pn/ZHw==
1363-
1364-
"@types/semver@^7.5.2":
1359+
"@types/semver@^7.3.12", "@types/semver@^7.5.2":
13651360
version "7.5.2"
13661361
resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.2.tgz#31f6eec1ed7ec23f4f05608d3a2d381df041f564"
13671362
integrity sha512-7aqorHYgdNO4DM36stTiGO3DvKoex9TQRwsJU6vMaFGyqpBA1MNZkz+PG3gaNUPpTAOYhT1WR7M1JyA3fbS9Cw==
@@ -6044,15 +6039,7 @@ pify@^4.0.1:
60446039
resolved "https://registry.yarnpkg.com/pify/-/pify-4.0.1.tgz#4b2cd25c50d598735c50292224fd8c6df41e3231"
60456040
integrity sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==
60466041

6047-
pino-abstract-transport@^1.0.0:
6048-
version "1.0.0"
6049-
resolved "https://registry.yarnpkg.com/pino-abstract-transport/-/pino-abstract-transport-1.0.0.tgz#cc0d6955fffcadb91b7b49ef220a6cc111d48bb3"
6050-
integrity sha512-c7vo5OpW4wIS42hUVcT5REsL8ZljsUfBjqV/e2sFxmFEFZiq1XLUp5EYLtuDH6PEHq9W1egWqRbnLUP5FuZmOA==
6051-
dependencies:
6052-
readable-stream "^4.0.0"
6053-
split2 "^4.0.0"
6054-
6055-
6042+
pino-abstract-transport@^1.0.0, [email protected]:
60566043
version "1.1.0"
60576044
resolved "https://registry.yarnpkg.com/pino-abstract-transport/-/pino-abstract-transport-1.1.0.tgz#083d98f966262164504afb989bccd05f665937a8"
60586045
integrity sha512-lsleG3/2a/JIWUtf9Q5gUNErBqwIu1tUKTT3dUzaf5DySw9ra1wcqKjJjLX1VTY64Wk1eEOYsVGSaGfCK85ekA==

0 commit comments

Comments
 (0)