Skip to content

Commit 6782771

Browse files
committed
fix: handle multiple sandbox processes in a resumable state
1 parent ce1df55 commit 6782771

File tree

4 files changed

+146
-3
lines changed

4 files changed

+146
-3
lines changed

messages/sandboxbase.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,7 @@ If the org is ready, checking the status also authorizes the org for use with Sa
1515
# warning.ClientTimeoutWaitingForSandboxCreate
1616

1717
The wait time for the sandbox creation has been exhausted. Please see the results below for more information.
18+
19+
# warning.MultipleMatchingSandboxProcesses
20+
21+
There were multiple sandbox processes found for "%s" in a resumable state. Ignoring sandbox process ID(s) "%s" in status(es) "%s" and using the most recent process ID "%s". To resume a different sandbox process please use that unique sandbox process ID with the command. E.g, "sf org resume sandbox --job-id %s -o %s"

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: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,27 @@ 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 = results.shift() as SandboxProcessObject;
156+
const sbxProcessIds: string[] = [];
157+
const sbxProcessStatuses: string[] = [];
158+
results.map((sbxProc) => {
159+
sbxProcessIds.push(sbxProc.Id);
160+
sbxProcessStatuses.push(sbxProc.Status);
161+
});
162+
this.warn(
163+
messages.getMessage('warning.MultipleMatchingSandboxProcesses', [
164+
results[0].SandboxName,
165+
sbxProcessIds.toString(),
166+
sbxProcessStatuses.toString(),
167+
resumingProcess.Id,
168+
sbxProcessIds[0],
169+
this.prodOrg?.getUsername(),
170+
])
171+
);
172+
return Promise.resolve();
173+
});
153174
}
154175

155176
protected reportResults(results: ResultEvent): void {
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
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 sbxCreateNotComplete = new SfError('sbx create not complete', 'sandboxCreateNotComplete');
83+
const inProgSandboxProcessObj = Object.assign({}, sandboxProcessObj, {
84+
Status: 'In Progress',
85+
Id: '0GR4p000000U8EMZZZ',
86+
CopyProgress: 25,
87+
CreatedDate: '2022-12-07T16:20:21.000+0000',
88+
});
89+
stubMethod(sandbox, Org.prototype, 'resumeSandbox').callsFake(async () => {
90+
await Lifecycle.getInstance().emit(SandboxEvents.EVENT_MULTIPLE_SBX_PROCESSES, [
91+
inProgSandboxProcessObj,
92+
sandboxProcessObj,
93+
]);
94+
throw sbxCreateNotComplete;
95+
});
96+
97+
try {
98+
await ResumeSandbox.run(['-o', prodOrgUsername, '--name', sandboxName]);
99+
expect(false, 'ResumeSandbox should have thrown sandboxCreateNotComplete');
100+
} catch (err: unknown) {
101+
const warningMsg = messages.getMessage('warning.MultipleMatchingSandboxProcesses', [
102+
sandboxName,
103+
sandboxProcessObj.Id,
104+
sandboxProcessObj.Status,
105+
inProgSandboxProcessObj.Id,
106+
sandboxProcessObj.Id,
107+
prodOrgUsername,
108+
]);
109+
expect(sfCommandUxStubs.warn.calledWith(warningMsg)).to.be.true;
110+
const error = err as SfError;
111+
expect(error.name).to.equal('sandboxCreateNotComplete');
112+
}
113+
});
114+
115+
afterEach(() => {
116+
sandbox.restore();
117+
});
118+
});

0 commit comments

Comments
 (0)