Skip to content

Commit 60a001d

Browse files
authored
feat: allow sandbox name and license-type overrides (#991)
* feat: allow sandbox name and license-type overrides * test: remove unnecessary type
1 parent 3703f65 commit 60a001d

File tree

3 files changed

+188
-9
lines changed

3 files changed

+188
-9
lines changed

src/commands/org/create/sandbox.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@ export default class CreateSandbox extends SandboxCommandBase<SandboxProcessObje
3535
char: 'f',
3636
summary: messages.getMessage('flags.definitionFile.summary'),
3737
description: messages.getMessage('flags.definitionFile.description'),
38-
exclusive: ['name', 'license-type'],
3938
}),
4039
'set-default': Flags.boolean({
4140
char: 's',
@@ -74,7 +73,6 @@ export default class CreateSandbox extends SandboxCommandBase<SandboxProcessObje
7473
char: 'n',
7574
summary: messages.getMessage('flags.name.summary'),
7675
description: messages.getMessage('flags.name.description'),
77-
exclusive: ['definition-file'],
7876
parse: (name: string): Promise<string> => {
7977
if (name.length > 10) {
8078
throw messages.createError('error.SandboxNameLength', [name]);
@@ -93,7 +91,7 @@ export default class CreateSandbox extends SandboxCommandBase<SandboxProcessObje
9391
})({
9492
char: 'l',
9593
summary: messages.getMessage('flags.licenseType.summary'),
96-
exclusive: ['definition-file', 'clone'],
94+
exclusive: ['clone'],
9795
}),
9896
'target-org': Flags.requiredOrg({
9997
char: 'o',

test/nut/sandboxCreate.nut.ts

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
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+
8+
import fs from 'node:fs';
9+
import path from 'node:path';
10+
import { assert, expect } from 'chai';
11+
import sinon from 'sinon';
12+
import { TestSession, genUniqueString } from '@salesforce/cli-plugins-testkit';
13+
import { SandboxRequestCache } from '@salesforce/core';
14+
import { stubSfCommandUx, stubSpinner, stubUx } from '@salesforce/sf-plugins-core';
15+
import CreateSandbox from '../../src/commands/org/create/sandbox.js';
16+
import {
17+
deleteSandboxCacheFile,
18+
getSandboxInfo,
19+
getSandboxProcess,
20+
getSandboxProcessSoql,
21+
readSandboxCacheFile,
22+
stubProdOrgConnection,
23+
stubToolingCreate,
24+
stubToolingQuery,
25+
} from '../shared/sandboxMockUtils.js';
26+
27+
describe('Sandbox Create', () => {
28+
let session: TestSession;
29+
let hubOrgUsername: string;
30+
let cacheFilePath: string;
31+
let sandboxDefFilePath: string;
32+
let sfCommandUxStubs: ReturnType<typeof stubSfCommandUx>;
33+
34+
const sinonSandbox = sinon.createSandbox();
35+
36+
let sandboxProcessSoql: string;
37+
38+
before(async () => {
39+
const uid = genUniqueString('sbxCreate_%s');
40+
session = await TestSession.create({
41+
project: { name: 'sandboxCreate' },
42+
devhubAuthStrategy: 'AUTH_URL',
43+
sessionDir: path.join(process.cwd(), `test_session_${uid}`),
44+
});
45+
assert(session.hubOrg.username);
46+
hubOrgUsername = session.hubOrg.username;
47+
cacheFilePath = path.join(session.dir, '.sf', SandboxRequestCache.getFileName());
48+
sandboxDefFilePath = path.join(session.project.dir, 'sandboxDef.json');
49+
50+
// add a sandbox definition file to the project
51+
const { SandboxName, LicenseType, Id } = getSandboxInfo();
52+
sandboxProcessSoql = getSandboxProcessSoql({ sandboxInfoId: Id });
53+
fs.writeFileSync(sandboxDefFilePath, JSON.stringify({ SandboxName, LicenseType }));
54+
});
55+
56+
beforeEach(() => {
57+
sfCommandUxStubs = stubSfCommandUx(sinonSandbox);
58+
stubUx(sinonSandbox);
59+
stubSpinner(sinonSandbox);
60+
});
61+
62+
after(async () => {
63+
await session?.clean();
64+
});
65+
66+
afterEach(() => {
67+
try {
68+
deleteSandboxCacheFile(cacheFilePath);
69+
} catch (err) {
70+
// ignore since there isn't always a cache file written
71+
}
72+
sinonSandbox.restore();
73+
});
74+
75+
//
76+
// NOTE: These tests use stubbed server responses since sandbox refresh
77+
// takes a very long time.
78+
// Stubs:
79+
// 1. Connection.singleRecordQuery - used to query for the SandboxInfo
80+
// 2. Connection.tooling.update - used to update the SandboxInfo
81+
// 3. Connection.tooling.query - used to query for the SandboxProcess
82+
//
83+
84+
it('should return a SandboxProcessObject without polling (--async) using --name', async () => {
85+
const sbxInfo = getSandboxInfo();
86+
const sbxName = sbxInfo.SandboxName;
87+
const sbxLicenseType = 'Developer';
88+
const sbxProcess = getSandboxProcess();
89+
const connection = await stubProdOrgConnection(sinonSandbox, hubOrgUsername);
90+
91+
const toolingCreateStub = stubToolingCreate({ sinonSandbox, connection });
92+
const toolingQueryStub = stubToolingQuery({ sinonSandbox, connection, sandboxProcessSoql, sbxProcess });
93+
94+
const result = await CreateSandbox.run(['--name', sbxName, '-o', hubOrgUsername, '--async', '--json']);
95+
96+
expect(result).to.deep.equal(sbxProcess);
97+
expect(toolingCreateStub.calledOnce, 'toolingCreateStub').to.be.true;
98+
expect(toolingCreateStub.firstCall.args[1]).to.deep.equal({
99+
SandboxName: sbxName,
100+
LicenseType: sbxLicenseType,
101+
});
102+
expect(toolingQueryStub.calledOnce, 'toolingQueryStub').to.be.true;
103+
104+
// check the sandbox cache entry
105+
const cache = readSandboxCacheFile(cacheFilePath);
106+
expect(cache).to.have.property(sbxName);
107+
expect(cache[sbxName]).to.have.property('action', 'Create');
108+
expect(cache[sbxName]).to.have.property('prodOrgUsername', hubOrgUsername);
109+
expect(cache[sbxName]).to.have.deep.property('sandboxProcessObject', sbxProcess);
110+
expect(cache[sbxName]).to.have.deep.property('sandboxRequest', {
111+
SandboxName: sbxName,
112+
LicenseType: sbxLicenseType,
113+
});
114+
expect(sfCommandUxStubs).to.be.ok;
115+
});
116+
117+
// This test uses a sandbox definition file and flags to override the name and LicenseType.
118+
it('should override existing SandboxInfo with definition-file values', async () => {
119+
const sbxName = 'OvrSbxName';
120+
const sbxLicenseType = 'Partial';
121+
const sbxProcess = getSandboxProcess({ SandboxName: sbxName, LicenseType: sbxLicenseType });
122+
const connection = await stubProdOrgConnection(sinonSandbox, hubOrgUsername);
123+
124+
const toolingCreateStub = stubToolingCreate({ sinonSandbox, connection });
125+
const toolingQueryStub = stubToolingQuery({ sinonSandbox, connection, sandboxProcessSoql, sbxProcess });
126+
127+
const result = await CreateSandbox.run([
128+
'--definition-file',
129+
sandboxDefFilePath,
130+
'--name',
131+
sbxName,
132+
'--license-type',
133+
sbxLicenseType,
134+
'-o',
135+
hubOrgUsername,
136+
'--async',
137+
'--json',
138+
]);
139+
140+
expect(result).to.deep.equal(sbxProcess);
141+
expect(toolingCreateStub.calledOnce).to.be.true;
142+
expect(toolingCreateStub.firstCall.args[1]).to.deep.equal({
143+
SandboxName: sbxName,
144+
LicenseType: sbxLicenseType,
145+
});
146+
expect(toolingQueryStub.calledOnce).to.be.true;
147+
148+
// check the sandbox cache entry
149+
const cache = readSandboxCacheFile(cacheFilePath);
150+
expect(cache).to.have.property(sbxName);
151+
expect(cache[sbxName]).to.have.property('action', 'Create');
152+
expect(cache[sbxName]).to.have.property('prodOrgUsername', hubOrgUsername);
153+
expect(cache[sbxName]).to.have.deep.property('sandboxProcessObject', sbxProcess);
154+
expect(cache[sbxName]).to.have.deep.property('sandboxRequest', {
155+
SandboxName: sbxName,
156+
LicenseType: sbxLicenseType,
157+
});
158+
});
159+
});

test/shared/sandboxMockUtils.ts

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ const sandboxProcessObject: SandboxProcessObject = {
2121
CreatedDate: '2024-02-21T23:06:58.000+0000',
2222
CopyProgress: 0,
2323
SandboxOrganization: '00DDX000000QT3W',
24-
Description: 'testing sandbox refresh',
24+
Description: 'testing sandbox create and refresh',
2525
};
2626

2727
const sandboxInfo: SandboxInfo = {
@@ -87,10 +87,15 @@ export const getSandboxProcess = (overrides?: Partial<SandboxProcessObject>): Sa
8787
Object.assign({}, sandboxProcessObject, overrides);
8888
export const getSandboxInfoSoql = (sandboxName = sbxName) =>
8989
`SELECT ${sandboxInfoFields.join(',')} FROM SandboxInfo WHERE SandboxName='${sandboxName}'`;
90-
export const getSandboxProcessSoql = (sandboxName = sbxName) =>
91-
`SELECT ${sandboxProcessFields.join(
92-
','
93-
)} FROM SandboxProcess WHERE SandboxName='${sandboxName}' ORDER BY CreatedDate DESC`;
90+
export const getSandboxProcessSoql = (cfg?: SbxProcessSqlConfig) => {
91+
const whereClause = cfg?.sandboxName
92+
? `SandboxName='${cfg.sandboxName}'`
93+
: cfg?.sandboxInfoId
94+
? `SandboxInfoId='${cfg.sandboxInfoId}'`
95+
: `SandboxName='${sbxName}'`;
96+
97+
return `SELECT ${sandboxProcessFields.join(',')} FROM SandboxProcess WHERE ${whereClause} ORDER BY CreatedDate DESC`;
98+
};
9499

95100
/**
96101
* Decorates the `Org.create()` call for the given username to use a stubbed connection so that
@@ -159,14 +164,31 @@ export const stubToolingUpdate = (config: ToolingUpdateStubConfig): sinon.SinonS
159164
return sinonSandbox.stub(connection.tooling, 'update').resolves(response);
160165
};
161166

167+
/**
168+
* Stubs the call to create the SandboxInfo
169+
*
170+
* @param config
171+
* @returns sinon.SinonStub
172+
*/
173+
export const stubToolingCreate = (config: ToolingUpdateStubConfig): sinon.SinonStub => {
174+
const { sinonSandbox, connection, response = updateSuccessResponse } = config;
175+
return sinonSandbox.stub(connection.tooling, 'create').resolves(response);
176+
};
177+
162178
type ToolingQueryStubConfig = {
163179
sinonSandbox: sinon.SinonSandbox;
164180
connection: Connection;
165181
sandboxProcessSoql: string;
166182
sbxProcess: SandboxProcessObject;
167183
};
184+
185+
type SbxProcessSqlConfig = {
186+
sandboxName?: string;
187+
sandboxInfoId?: string;
188+
};
168189
/**
169-
* Stubs the query for the newly created SandboxProcess from the update
190+
* Stubs the query for the newly created SandboxProcess from a sandbox update.
191+
* Stubs the query for a cloned sandbox process (see `CreateSandbox.getSourceId()`).
170192
*
171193
* @param config
172194
* @returns sinon.SinonStub

0 commit comments

Comments
 (0)