Skip to content

Commit ab4b7a2

Browse files
WillieRuemmelemshanemccristiand391
authored
fix: org:delete scratch org + sandboxes (#228)
* fix: scratch org delete logic & tests * chore: update for sandboxes * refactor: use isSandbox on coreOrg * chore: bump ts version * test: remove unused beforeEach * refactor: update errorNames, stub isSandbox * chore: bump core for org delete, fix return any error * chore: clean up after delete NUT Co-authored-by: mshanemc <[email protected]> Co-authored-by: Cristian Dominguez <[email protected]>
1 parent fb650b3 commit ab4b7a2

File tree

8 files changed

+297
-11
lines changed

8 files changed

+297
-11
lines changed

command-snapshot.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
11
[
2+
{
3+
"command": "force:org:delete",
4+
"plugin": "@salesforce/plugin-org",
5+
"flags": ["apiversion", "json", "loglevel", "noprompt", "targetdevhubusername", "targetusername"]
6+
},
27
{
38
"command": "force:org:display",
49
"plugin": "@salesforce/plugin-org",
@@ -12,6 +17,6 @@
1217
{
1318
"command": "force:org:open",
1419
"plugin": "@salesforce/plugin-org",
15-
"flags": ["path", "urlonly", "json", "loglevel", "targetusername", "apiversion", "browser"]
20+
"flags": ["apiversion", "browser", "json", "loglevel", "path", "targetusername", "urlonly"]
1621
}
1722
]

messages/delete.json

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"description": "mark a scratch or sandbox org for deletion \nTo mark the org for deletion without being prompted to confirm, specify --noprompt.",
3+
"examples": ["$ sfdx force:org:delete -u [email protected]", "$ sfdx force:org:delete -u MyOrgAlias -p"],
4+
"flags": {
5+
"noprompt": "no prompt to confirm deletion"
6+
},
7+
"confirmDelete": "Enqueue %s org with name: %s for deletion? Are you sure (y/n)?",
8+
"sandboxConfigOnlySuccess": "Successfully deleted sandbox org %s.",
9+
"ScratchOrgNotFound": "Attempting to delete an expired or deleted org",
10+
"deleteOrgConfigOnlyCommandSuccess": "Successfully deleted scratch org %s.'",
11+
"deleteOrgCommandSuccess": "Successfully marked scratch org %s for deletion",
12+
"commandSandboxSuccess": "The sandbox org %s has been successfully removed from your list of CLI authorized orgs. If you created the sandbox with one of the force:org commands, it has also been marked for deletion."
13+
}

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
"dependencies": {
88
"@oclif/config": "^1.17.1",
99
"@salesforce/command": "^4.1.3",
10-
"@salesforce/core": "^2.28.0",
10+
"@salesforce/core": "^2.30.0",
1111
"@salesforce/kit": "^1.5.17",
1212
"open": "8.4.0",
1313
"tslib": "^2"
@@ -45,7 +45,7 @@
4545
"shx": "0.3.3",
4646
"sinon": "10.0.0",
4747
"ts-node": "^10.0.0",
48-
"typescript": "^4.1.3"
48+
"typescript": "^4.4.4"
4949
},
5050
"config": {
5151
"commitizen": {

src/commands/force/org/delete.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
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 * as os from 'os';
8+
import { flags, FlagsConfig, SfdxCommand } from '@salesforce/command';
9+
import { Messages } from '@salesforce/core';
10+
11+
Messages.importMessagesDirectory(__dirname);
12+
const messages = Messages.loadMessages('@salesforce/plugin-org', 'delete');
13+
14+
type DeleteResult = {
15+
orgId: string;
16+
username: string;
17+
};
18+
19+
export class Delete extends SfdxCommand {
20+
public static readonly requiresUsername = true;
21+
public static readonly supportsDevhubUsername = true;
22+
public static readonly description = messages.getMessage('description');
23+
public static readonly examples = messages.getMessage('examples').split(os.EOL);
24+
public static readonly flagsConfig: FlagsConfig = {
25+
noprompt: flags.boolean({
26+
char: 'p',
27+
description: messages.getMessage('flags.noprompt'),
28+
}),
29+
};
30+
31+
public async run(): Promise<DeleteResult> {
32+
const username = this.org.getUsername();
33+
const orgId = this.org.getOrgId();
34+
const isSandbox = await this.org.isSandbox();
35+
// read the config file for the org to be deleted, if it has a PROD_ORG_USERNAME entry, it's a sandbox
36+
// we either need permission to proceed without a prompt OR get the user to confirm
37+
if (
38+
this.flags.noprompt ||
39+
(await this.ux.confirm(messages.getMessage('confirmDelete', [isSandbox ? 'sandbox' : 'scratch', username])))
40+
) {
41+
let alreadyDeleted = false;
42+
let successMessageKey = 'commandSandboxSuccess';
43+
try {
44+
// will determine if it's a scratch org or sandbox and will delete from the appropriate parent org (DevHub or Production)
45+
await this.org.delete();
46+
} catch (e) {
47+
if (e instanceof Error && e.name === 'ScratchOrgNotFound') {
48+
alreadyDeleted = true;
49+
} else if (e instanceof Error && e.name === 'SandboxNotFound') {
50+
successMessageKey = 'sandboxConfigOnlySuccess';
51+
} else {
52+
throw e;
53+
}
54+
}
55+
56+
this.ux.log(
57+
isSandbox
58+
? messages.getMessage(successMessageKey, [username])
59+
: messages.getMessage(alreadyDeleted ? 'deleteOrgConfigOnlyCommandSuccess' : 'deleteOrgCommandSuccess', [
60+
username,
61+
])
62+
);
63+
}
64+
return { username, orgId };
65+
}
66+
}

src/shared/orgListUtil.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -320,14 +320,14 @@ export class OrgListUtil {
320320
const logger = await OrgListUtil.retrieveLogger();
321321
logger.trace(`error refreshing auth for org: ${org.getUsername()}`);
322322
logger.trace(error);
323-
return error.code ?? error.message;
323+
return (error.code ?? error.message) as string;
324324
}
325325
} catch (err) {
326326
const error = err as SfdxError;
327327
const logger = await OrgListUtil.retrieveLogger();
328328
logger.trace(`error refreshing auth for org: ${username}`);
329329
logger.trace(error);
330-
return error.code ?? error.message ?? 'Unknown';
330+
return (error.code ?? error.message ?? 'Unknown') as string;
331331
}
332332
}
333333
}
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
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 { Messages, Org, SfdxProject } from '@salesforce/core';
8+
import { fromStub, stubInterface, stubMethod } from '@salesforce/ts-sinon';
9+
import { IConfig } from '@oclif/config';
10+
import * as sinon from 'sinon';
11+
import { expect } from '@salesforce/command/lib/test';
12+
import { UX } from '@salesforce/command';
13+
import { Delete } from '../../../../src/commands/force/org/delete';
14+
15+
Messages.importMessagesDirectory(__dirname);
16+
const messages = Messages.loadMessages('@salesforce/plugin-org', 'delete');
17+
18+
describe('org:delete', () => {
19+
const sandbox = sinon.createSandbox();
20+
const username = '[email protected]';
21+
const orgId = '00D54000000KDltEAG';
22+
const oclifConfigStub = fromStub(stubInterface<IConfig>(sandbox));
23+
24+
// stubs
25+
let resolveProjectConfigStub: sinon.SinonStub;
26+
let uxConfirmStub: sinon.SinonStub;
27+
let uxLogStub: sinon.SinonStub;
28+
let cmd: TestDelete;
29+
30+
class TestDelete extends Delete {
31+
public async runIt() {
32+
await this.init();
33+
return this.run();
34+
}
35+
public setOrg(org: Org) {
36+
this.org = org;
37+
}
38+
public setProject(project: SfdxProject) {
39+
this.project = project;
40+
}
41+
}
42+
43+
const runDeleteCommand = async (
44+
params: string[],
45+
options: { isSandbox?: boolean; deleteScratchOrg?: string } = { isSandbox: false }
46+
) => {
47+
cmd = new TestDelete(params, oclifConfigStub);
48+
stubMethod(sandbox, cmd, 'assignProject').callsFake(() => {
49+
const sfdxProjectStub = fromStub(
50+
stubInterface<SfdxProject>(sandbox, {
51+
resolveProjectConfig: resolveProjectConfigStub,
52+
})
53+
);
54+
cmd.setProject(sfdxProjectStub);
55+
});
56+
stubMethod(sandbox, cmd, 'assignOrg').callsFake(() => {
57+
const orgStubOptions = {
58+
getSandboxOrgConfigField: () => {},
59+
delete: () => {},
60+
getUsername: () => username,
61+
getOrgId: () => orgId,
62+
isSandbox: () => options.isSandbox,
63+
};
64+
65+
if (options.deleteScratchOrg) {
66+
orgStubOptions.delete = () => {
67+
const e = new Error();
68+
e.name = options.deleteScratchOrg;
69+
throw e;
70+
};
71+
}
72+
73+
const orgStub = fromStub(stubInterface<Org>(sandbox, orgStubOptions));
74+
cmd.setOrg(orgStub);
75+
});
76+
uxConfirmStub = stubMethod(sandbox, UX.prototype, 'confirm');
77+
uxLogStub = stubMethod(sandbox, UX.prototype, 'log');
78+
79+
return cmd.runIt();
80+
};
81+
82+
it('will prompt before attempting to delete', async () => {
83+
const res = await runDeleteCommand([]);
84+
expect(uxConfirmStub.calledOnce).to.equal(true);
85+
expect(uxConfirmStub.firstCall.args[0]).to.equal(messages.getMessage('confirmDelete', ['scratch', username]));
86+
expect(res).to.deep.equal({ orgId, username });
87+
});
88+
89+
it('will determine sandbox vs scratch org and delete sandbox', async () => {
90+
const res = await runDeleteCommand([], { isSandbox: true });
91+
expect(uxConfirmStub.calledOnce).to.equal(true);
92+
expect(uxConfirmStub.firstCall.args[0]).to.equal(messages.getMessage('confirmDelete', ['sandbox', username]));
93+
expect(res).to.deep.equal({ orgId, username });
94+
});
95+
96+
it('will NOT prompt before deleting scratch org when flag is provided', async () => {
97+
const res = await runDeleteCommand(['--noprompt']);
98+
expect(uxConfirmStub.called).to.equal(false);
99+
expect(uxLogStub.callCount).to.equal(1);
100+
expect(uxLogStub.firstCall.args[0]).to.equal(messages.getMessage('deleteOrgCommandSuccess', [username]));
101+
expect(res).to.deep.equal({ orgId, username });
102+
});
103+
104+
it('will NOT prompt before deleting sandbox when flag is provided', async () => {
105+
const res = await runDeleteCommand(['--noprompt'], { isSandbox: true });
106+
expect(uxConfirmStub.called).to.equal(false);
107+
expect(uxLogStub.callCount).to.equal(1);
108+
expect(uxLogStub.firstCall.args[0]).to.equal(messages.getMessage('commandSandboxSuccess', [username]));
109+
expect(res).to.deep.equal({ orgId, username });
110+
});
111+
112+
it('will catch the ScratchOrgNotFound and wrap correctly', async () => {
113+
const res = await runDeleteCommand(['--noprompt'], { deleteScratchOrg: 'ScratchOrgNotFound' });
114+
expect(uxConfirmStub.called).to.equal(false);
115+
expect(res).to.deep.equal({ orgId, username });
116+
expect(uxLogStub.firstCall.args[0]).to.equal(messages.getMessage('deleteOrgConfigOnlyCommandSuccess', [username]));
117+
});
118+
119+
it('will catch the SandboxNotFound and wrap correctly', async () => {
120+
const res = await runDeleteCommand(['--noprompt'], {
121+
deleteScratchOrg: 'SandboxNotFound',
122+
isSandbox: true,
123+
});
124+
expect(uxConfirmStub.called).to.equal(false);
125+
expect(res).to.deep.equal({ orgId, username });
126+
expect(uxLogStub.firstCall.args[0]).to.equal(messages.getMessage('sandboxConfigOnlySuccess', [username]));
127+
});
128+
129+
afterEach(() => {
130+
sandbox.restore();
131+
});
132+
});
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
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 { execCmd, TestSession } from '@salesforce/cli-plugins-testkit';
8+
import { AnyJson, getString, isArray } from '@salesforce/ts-types';
9+
import { expect } from '@salesforce/command/lib/test';
10+
// these NUTs are separated from org.nuts.ts because deleting orgs may interfere with the other NUTs
11+
describe('Delete Orgs', () => {
12+
let session: TestSession;
13+
let defaultUsername: string;
14+
let aliasedUsername: string;
15+
let defaultUserOrgId: string;
16+
let aliasUserOrgId: string;
17+
18+
// create our own orgs to delete to avoid interfering with other NUTs/cleanup
19+
before(async () => {
20+
session = await TestSession.create({
21+
project: { name: 'forceOrgList' },
22+
setupCommands: [
23+
'sfdx force:org:create -f config/project-scratch-def.json --setdefaultusername --wait 10',
24+
'sfdx force:org:create -f config/project-scratch-def.json --setalias anAlias --wait 10',
25+
],
26+
});
27+
28+
if (isArray<AnyJson>(session.setup)) {
29+
defaultUsername = getString(session.setup[0], 'result.username');
30+
defaultUserOrgId = getString(session.setup[0], 'result.orgId');
31+
aliasedUsername = getString(session.setup[1], 'result.username');
32+
aliasUserOrgId = getString(session.setup[1], 'result.orgId');
33+
}
34+
});
35+
36+
after(async () => {
37+
try {
38+
await session?.clean();
39+
} catch (e) {
40+
// do nothing, session?.clean() will try to remove files already removed by the org:delete and throw an error
41+
// it will also unwrap other stubbed methods
42+
}
43+
});
44+
45+
it('delete scratch orgs via config', () => {
46+
const result = execCmd('force:org:delete --noprompt --json', {
47+
ensureExitCode: 0,
48+
}).jsonOutput.result;
49+
expect(result).to.be.ok;
50+
expect(result).to.deep.equal({ orgId: defaultUserOrgId, username: defaultUsername });
51+
});
52+
53+
it('delete scratch orgs via alias', () => {
54+
const result = execCmd('force:org:delete --targetusername anAlias --noprompt --json', {
55+
ensureExitCode: 0,
56+
}).jsonOutput.result;
57+
expect(result).to.be.ok;
58+
expect(result).to.deep.equal({ orgId: aliasUserOrgId, username: aliasedUsername });
59+
});
60+
61+
describe.skip('sandbox', () => {
62+
// TODO: figure out how to test sandboxes in NUTs
63+
});
64+
});

yarn.lock

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -682,24 +682,25 @@
682682
chalk "^2.4.2"
683683
cli-ux "^4.9.3"
684684

685-
"@salesforce/core@^2.2.0", "@salesforce/core@^2.23.4", "@salesforce/core@^2.24.0", "@salesforce/core@^2.28.0":
686-
version "2.28.2"
687-
resolved "https://registry.yarnpkg.com/@salesforce/core/-/core-2.28.2.tgz#02fed89297c8d373d7c0784390abbc0791599268"
688-
integrity sha512-ohKNx4v1QL5LMESavn9Bs0ZNA4cw3IGL+dOvP601SphKPCOMiDfZYzfqhtET4R4uTAnJKvtUsuyvQUwL2479jg==
685+
"@salesforce/core@^2.2.0", "@salesforce/core@^2.23.4", "@salesforce/core@^2.24.0", "@salesforce/core@^2.28.0", "@salesforce/core@^2.30.0":
686+
version "2.30.0"
687+
resolved "https://registry.yarnpkg.com/@salesforce/core/-/core-2.30.0.tgz#0cf52378d87f18a55d438a492fd13b4579f01105"
688+
integrity sha512-Uh1XCEL9qnf7mBuKrKTzK3TIXlkZn+ZQyYOGW0taa2uanVu6BrVHC+2E689RFr/9vK6zv1NsJklg6B3zguekVw==
689689
dependencies:
690690
"@salesforce/bunyan" "^2.0.0"
691691
"@salesforce/kit" "^1.5.0"
692692
"@salesforce/schemas" "^1.0.1"
693693
"@salesforce/ts-types" "^1.5.13"
694694
"@types/graceful-fs" "^4.1.5"
695-
"@types/jsforce" "^1.9.29"
695+
"@types/jsforce" "^1.9.35"
696696
"@types/mkdirp" "^1.0.1"
697697
debug "^3.1.0"
698698
graceful-fs "^4.2.4"
699699
jsen "0.6.6"
700700
jsforce "^1.10.1"
701701
jsonwebtoken "8.5.0"
702702
mkdirp "1.0.4"
703+
semver "^7.3.5"
703704
sfdx-faye "^1.0.9"
704705
ts-retry-promise "^0.6.0"
705706

@@ -905,7 +906,7 @@
905906
dependencies:
906907
"@types/node" "*"
907908

908-
"@types/jsforce@^1.9.29":
909+
"@types/jsforce@^1.9.35":
909910
version "1.9.35"
910911
resolved "https://registry.npmjs.org/@types/jsforce/-/jsforce-1.9.35.tgz#abda87c796910d286420b07a97382c782b576ca8"
911912
integrity sha512-GEb1iMAK8raElbBbozR7aVEuMl47jrPYLNkeh+3h2vLJIG21uQ/kOk4kPgMiMFwCmgfX4/tPDugnjb7qrMav4A==
@@ -6002,6 +6003,11 @@ typescript@^4.1.3:
60026003
resolved "https://registry.npmjs.org/typescript/-/typescript-4.4.3.tgz#bdc5407caa2b109efd4f82fe130656f977a29324"
60036004
integrity sha512-4xfscpisVgqqDfPaJo5vkd+Qd/ItkoagnHpufr+i2QCHBsNYp+G7UAoyFl8aPtx879u38wPV65rZ8qbGZijalA==
60046005

6006+
typescript@^4.4.4:
6007+
version "4.4.4"
6008+
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.4.4.tgz#2cd01a1a1f160704d3101fd5a58ff0f9fcb8030c"
6009+
integrity sha512-DqGhF5IKoBl8WNf8C1gu8q0xZSInh9j1kJJMqT3a94w1JzVaBU4EXOSMrz9yDqMT0xt3selp83fuFMQ0uzv6qA==
6010+
60056011
typescript@~4.3.2:
60066012
version "4.3.5"
60076013
resolved "https://registry.npmjs.org/typescript/-/typescript-4.3.5.tgz#4d1c37cc16e893973c45a06886b7113234f119f4"

0 commit comments

Comments
 (0)