diff --git a/messages/delete.md b/messages/delete.md index 9fe94b95..3c7de364 100644 --- a/messages/delete.md +++ b/messages/delete.md @@ -8,7 +8,8 @@ The force:org:delete command is deprecated. Use org:delete:scratch or org:delete # description -Salesforce CLI marks the org for deletion in either the Dev Hub org (for scratch orgs) or production org (for sandboxes) and then deletes all local references to the org from your computer. +Salesforce CLI marks the org for deletion in either the Dev Hub org (for scratch orgs) or production org (for sandboxes) +and then deletes all local references to the org from your computer. To mark the org for deletion without being prompted to confirm, specify --noprompt. @@ -22,6 +23,14 @@ To mark the org for deletion without being prompted to confirm, specify --noprom No prompt to confirm deletion. +# missingUsername + +Unable to determine the username of the org to delete. Specify the username with the --target-org | -o flag. + +# flags.target-org.summary + +Username or alias of the target org. + # flags.targetdevhubusername The targetdevhubusername flag exists only for backwards compatibility. It is not necessary and has no effect. @@ -48,4 +57,5 @@ Successfully marked scratch org %s for deletion # 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. +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. diff --git a/package.json b/package.json index c21679de..6c6108be 100644 --- a/package.json +++ b/package.json @@ -236,4 +236,4 @@ "output": [] } } -} \ No newline at end of file +} diff --git a/src/commands/force/org/delete.ts b/src/commands/force/org/delete.ts index 73959930..b01e6996 100644 --- a/src/commands/force/org/delete.ts +++ b/src/commands/force/org/delete.ts @@ -4,14 +4,8 @@ * 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 { - Flags, - SfCommand, - requiredOrgFlagWithDeprecations, - orgApiVersionFlagWithDeprecations, - loglevel, -} from '@salesforce/sf-plugins-core'; -import { Messages } from '@salesforce/core'; +import { Flags, loglevel, orgApiVersionFlagWithDeprecations, SfCommand } from '@salesforce/sf-plugins-core'; +import { AuthInfo, AuthRemover, Messages, Org, StateAggregator } from '@salesforce/core'; Messages.importMessagesDirectory(__dirname); const messages = Messages.loadMessages('@salesforce/plugin-org', 'delete'); @@ -30,7 +24,15 @@ export class Delete extends SfCommand { message: messages.getMessage('deprecation'), }; public static readonly flags = { - 'target-org': requiredOrgFlagWithDeprecations, + 'target-org': Flags.string({ + // not required because the user could be assuming the default config + aliases: ['targetusername', 'u'], + deprecateAliases: true, + // we're recreating the flag without all the validation + // eslint-disable-next-line sf-plugin/dash-o + char: 'o', + summary: messages.getMessage('flags.target-org.summary'), + }), targetdevhubusername: Flags.string({ summary: messages.getMessage('flags.targetdevhubusername'), char: 'v', @@ -52,24 +54,39 @@ export class Delete extends SfCommand { public async run(): Promise { const { flags } = await this.parse(Delete); - const username = flags['target-org'].getUsername() ?? 'unknown username'; - const orgId = flags['target-org'].getOrgId(); - // the connection version can be set before using it to isSandbox and delete - flags['target-org'].getConnection(flags['api-version']); - const isSandbox = await flags['target-org'].isSandbox(); + const resolvedUsername = + // from -o alias -> -o username -> [default username] + (await StateAggregator.getInstance()).aliases.getUsername(flags['target-org'] ?? '') ?? + flags['target-org'] ?? + (this.configAggregator.getPropertyValue('target-org') as string); + + if (!resolvedUsername) { + throw messages.createError('missingUsername'); + } + + const orgId = (await AuthInfo.create({ username: resolvedUsername })).getFields().orgId as string; + const isSandbox = await (await StateAggregator.getInstance()).sandboxes.hasFile(orgId); + // read the config file for the org to be deleted, if it has a PROD_ORG_USERNAME entry, it's a sandbox // we either need permission to proceed without a prompt OR get the user to confirm if ( flags['no-prompt'] || - (await this.confirm(messages.getMessage('confirmDelete', [isSandbox ? 'sandbox' : 'scratch', username]))) + (await this.confirm(messages.getMessage('confirmDelete', [isSandbox ? 'sandbox' : 'scratch', resolvedUsername]))) ) { let alreadyDeleted = false; let successMessageKey = 'commandSandboxSuccess'; try { + const org = await Org.create({ aliasOrUsername: resolvedUsername }); + // will determine if it's a scratch org or sandbox and will delete from the appropriate parent org (DevHub or Production) - await flags['target-org'].delete(); + await org.delete(); } catch (e) { - if (e instanceof Error && e.name === 'ScratchOrgNotFound') { + if (e instanceof Error && e.name === 'DomainNotFoundError') { + // the org has expired, so remote operations won't work + // let's clean up the files locally + const authRemover = await AuthRemover.create(); + await authRemover.removeAuth(resolvedUsername); + } else if (e instanceof Error && e.name === 'ScratchOrgNotFound') { alreadyDeleted = true; } else if (e instanceof Error && e.name === 'SandboxNotFound') { successMessageKey = 'sandboxConfigOnlySuccess'; @@ -80,12 +97,12 @@ export class Delete extends SfCommand { this.log( isSandbox - ? messages.getMessage(successMessageKey, [username]) + ? messages.getMessage(successMessageKey, [resolvedUsername]) : messages.getMessage(alreadyDeleted ? 'deleteOrgConfigOnlyCommandSuccess' : 'deleteOrgCommandSuccess', [ - username, + resolvedUsername, ]) ); } - return { username, orgId }; + return { username: resolvedUsername, orgId }; } } diff --git a/src/commands/org/delete/sandbox.ts b/src/commands/org/delete/sandbox.ts index d22eb976..5e6e9b1a 100644 --- a/src/commands/org/delete/sandbox.ts +++ b/src/commands/org/delete/sandbox.ts @@ -4,8 +4,8 @@ * 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 { Messages, SfError } from '@salesforce/core'; -import { SfCommand, Flags } from '@salesforce/sf-plugins-core'; +import { AuthInfo, AuthRemover, Messages, Org, SfError, StateAggregator } from '@salesforce/core'; +import { Flags, SfCommand } from '@salesforce/sf-plugins-core'; Messages.importMessagesDirectory(__dirname); const messages = Messages.loadMessages('@salesforce/plugin-org', 'delete_sandbox'); @@ -14,6 +14,7 @@ export interface SandboxDeleteResponse { orgId: string; username: string; } + export default class EnvDeleteSandbox extends SfCommand { public static readonly summary = messages.getMessage('summary'); public static readonly description = messages.getMessage('description'); @@ -21,9 +22,11 @@ export default class EnvDeleteSandbox extends SfCommand { public static readonly aliases = ['env:delete:sandbox']; public static readonly deprecateAliases = true; public static readonly flags = { - 'target-org': Flags.requiredOrg({ - summary: messages.getMessage('flags.target-org.summary'), + 'target-org': Flags.string({ + // we're recreating the flag without all the validation + // eslint-disable-next-line sf-plugin/dash-o char: 'o', + summary: messages.getMessage('flags.target-org.summary'), required: true, }), 'no-prompt': Flags.boolean({ @@ -34,28 +37,40 @@ export default class EnvDeleteSandbox extends SfCommand { public async run(): Promise { const flags = (await this.parse(EnvDeleteSandbox)).flags; - const org = flags['target-org']; - const username = org.getUsername(); + const username = // from -o alias -> -o username -> [default username] + (await StateAggregator.getInstance()).aliases.getUsername(flags['target-org'] ?? '') ?? + flags['target-org'] ?? + (this.configAggregator.getPropertyValue('target-org') as string); if (!username) { throw new SfError('The org does not have a username.'); } - if (!(await org.isSandbox())) { + const orgId = (await AuthInfo.create({ username })).getFields().orgId as string; + const isSandbox = await (await StateAggregator.getInstance()).sandboxes.hasFile(orgId); + + if (!isSandbox) { throw messages.createError('error.isNotSandbox', [username]); } if (flags['no-prompt'] || (await this.confirm(messages.getMessage('prompt.confirm', [username])))) { try { + const org = await Org.create({ aliasOrUsername: username }); await org.delete(); this.logSuccess(messages.getMessage('success', [username])); } catch (e) { - if (e instanceof Error && e.name === 'SandboxNotFound') { + if (e instanceof Error && e.name === 'DomainNotFoundError') { + // the org has expired, so remote operations won't work + // let's clean up the files locally + const authRemover = await AuthRemover.create(); + await authRemover.removeAuth(username); + this.logSuccess(messages.getMessage('success.Idempotent', [username])); + } else if (e instanceof Error && e.name === 'SandboxNotFound') { this.logSuccess(messages.getMessage('success.Idempotent', [username])); } else { throw e; } } } - return { username, orgId: org.getOrgId() }; + return { username, orgId }; } } diff --git a/src/commands/org/delete/scratch.ts b/src/commands/org/delete/scratch.ts index 096a6519..9d8f2bd8 100644 --- a/src/commands/org/delete/scratch.ts +++ b/src/commands/org/delete/scratch.ts @@ -5,7 +5,7 @@ * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ -import { Messages } from '@salesforce/core'; +import { AuthInfo, AuthRemover, Messages, Org, StateAggregator } from '@salesforce/core'; import { SfCommand, Flags } from '@salesforce/sf-plugins-core'; Messages.importMessagesDirectory(__dirname); @@ -23,10 +23,14 @@ export default class EnvDeleteScratch extends SfCommand { public static readonly aliases = ['env:delete:scratch']; public static readonly deprecateAliases = true; public static readonly flags = { - 'target-org': Flags.requiredOrg({ + 'target-org': Flags.string({ + // not required because the user could be assuming the default config + aliases: ['targetusername', 'u'], + deprecateAliases: true, + // we're recreating the flag without all the validation + // eslint-disable-next-line sf-plugin/dash-o char: 'o', summary: messages.getMessage('flags.target-org.summary'), - required: true, }), 'no-prompt': Flags.boolean({ char: 'p', @@ -36,20 +40,34 @@ export default class EnvDeleteScratch extends SfCommand { public async run(): Promise { const flags = (await this.parse(EnvDeleteScratch)).flags; - const org = flags['target-org']; + const resolvedUsername = + // from -o alias -> -o username -> [default username] + (await StateAggregator.getInstance()).aliases.getUsername(flags['target-org'] ?? '') ?? + flags['target-org'] ?? + (this.configAggregator.getPropertyValue('target-org') as string); + const orgId = (await AuthInfo.create({ username: resolvedUsername })).getFields().orgId as string; - if (flags['no-prompt'] || (await this.confirm(messages.getMessage('prompt.confirm', [org.getUsername()])))) { + if (flags['no-prompt'] || (await this.confirm(messages.getMessage('prompt.confirm', [resolvedUsername])))) { try { + const org = await Org.create({ aliasOrUsername: resolvedUsername }); + await org.delete(); this.logSuccess(messages.getMessage('success', [org.getUsername()])); + return { username: org.getUsername() as string, orgId: org.getOrgId() }; } catch (e) { - if (e instanceof Error && e.name === 'ScratchOrgNotFound') { - this.logSuccess(messages.getMessage('success.Idempotent', [org.getUsername()])); + if (e instanceof Error && e.name === 'DomainNotFoundError') { + // the org has expired, so remote operations won't work + // let's clean up the files locally + const authRemover = await AuthRemover.create(); + await authRemover.removeAuth(resolvedUsername); + this.logSuccess(messages.getMessage('success', [resolvedUsername])); + } else if (e instanceof Error && e.name === 'ScratchOrgNotFound') { + this.logSuccess(messages.getMessage('success.Idempotent', [resolvedUsername])); } else { throw e; } } } - return { username: org.getUsername() as string, orgId: org.getOrgId() }; + return { username: resolvedUsername, orgId }; } } diff --git a/test/nut/scratchDelete.nut.ts b/test/nut/scratchDelete.nut.ts index 3dac204f..cac90f5c 100644 --- a/test/nut/scratchDelete.nut.ts +++ b/test/nut/scratchDelete.nut.ts @@ -50,11 +50,6 @@ describe('env:delete:scratch NUTs', () => { } }); - it('should see default username in help', () => { - const output = execCmd('env:delete:scratch --help', { ensureExitCode: 0 }).shellOutput; - expect(output).to.include(session.orgs.get('default')?.username); - }); - it('should delete the 1st scratch org by alias', () => { const command = `env:delete:scratch --target-org ${scratchOrgAlias} --no-prompt --json`; const output = execCmd(command, { ensureExitCode: 0 }).jsonOutput?.result; diff --git a/test/unit/force/org/delete.test.ts b/test/unit/force/org/delete.test.ts index 8be201ad..b8f6001a 100644 --- a/test/unit/force/org/delete.test.ts +++ b/test/unit/force/org/delete.test.ts @@ -4,13 +4,17 @@ * 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 { Messages, Org, SfError } from '@salesforce/core'; +import { ConfigAggregator, Messages, Org, SfError } from '@salesforce/core'; import { MockTestOrgData, TestContext } from '@salesforce/core/lib/testSetup'; -import { expect } from 'chai'; +import { expect, config } from 'chai'; import { stubPrompter, stubSfCommandUx } from '@salesforce/sf-plugins-core'; +import { SandboxAccessor } from '@salesforce/core/lib/stateAggregator/accessors/sandboxAccessor'; +import { Config } from '@oclif/core'; import { Delete } from '../../../../src/commands/force/org/delete'; +config.truncateThreshold = 0; + Messages.importMessagesDirectory(__dirname); const messages = Messages.loadMessages('@salesforce/plugin-org', 'delete'); @@ -30,8 +34,26 @@ describe('org:delete', () => { sfCommandUxStubs = stubSfCommandUx($$.SANDBOX); }); + it('will throw an error when no default set', async () => { + const deleteCommand = new Delete([], {} as Config); + deleteCommand.configAggregator = await ConfigAggregator.create(); + $$.SANDBOX.stub(deleteCommand.configAggregator, 'getPropertyValue').onSecondCall().returns(undefined); + + try { + await deleteCommand.run(); + expect.fail('should have thrown an error'); + } catch (e) { + const err = e as SfError; + expect(err.name).to.equal('MissingUsernameError'); + expect(err.message).to.equal(messages.getMessage('missingUsername')); + } + }); + it('will prompt before attempting to delete', async () => { - const res = await Delete.run([]); + const deleteCommand = new Delete([], {} as Config); + deleteCommand.configAggregator = await ConfigAggregator.create(); + $$.SANDBOX.stub(deleteCommand.configAggregator, 'getPropertyValue').onSecondCall().returns(testOrg.username); + const res = await deleteCommand.run(); expect(prompterStubs.confirm.calledOnce).to.equal(true); expect(prompterStubs.confirm.firstCall.args[0]).to.equal( messages.getMessage('confirmDelete', ['scratch', testOrg.username]) @@ -40,8 +62,8 @@ describe('org:delete', () => { }); it('will determine sandbox vs scratch org and delete sandbox', async () => { - $$.SANDBOX.stub(Org.prototype, 'isSandbox').resolves(true); - const res = await Delete.run([]); + $$.SANDBOX.stub(SandboxAccessor.prototype, 'hasFile').resolves(true); + const res = await Delete.run(['--target-org', testOrg.username]); expect(prompterStubs.confirm.calledOnce).to.equal(true); expect(prompterStubs.confirm.firstCall.args[0]).to.equal( messages.getMessage('confirmDelete', ['sandbox', testOrg.username]) @@ -52,7 +74,7 @@ describe('org:delete', () => { it('will NOT prompt before deleting scratch org when flag is provided', async () => { $$.SANDBOX.stub(Org.prototype, 'isSandbox').resolves(false); $$.SANDBOX.stub(Org.prototype, 'delete').resolves(); - const res = await Delete.run(['--noprompt']); + const res = await Delete.run(['--noprompt', '--target-org', testOrg.username]); expect(prompterStubs.confirm.calledOnce).to.equal(false); expect(sfCommandUxStubs.log.callCount).to.equal(1); expect(sfCommandUxStubs.log.getCalls().flatMap((call) => call.args)).to.deep.include( @@ -62,9 +84,9 @@ describe('org:delete', () => { }); it('will NOT prompt before deleting sandbox when flag is provided', async () => { - $$.SANDBOX.stub(Org.prototype, 'isSandbox').resolves(true); + $$.SANDBOX.stub(SandboxAccessor.prototype, 'hasFile').resolves(true); $$.SANDBOX.stub(Org.prototype, 'delete').resolves(); - const res = await Delete.run(['--noprompt']); + const res = await Delete.run(['--noprompt', '--target-org', testOrg.username]); expect(prompterStubs.confirm.calledOnce).to.equal(false); expect(sfCommandUxStubs.log.callCount).to.equal(1); expect(sfCommandUxStubs.log.getCalls().flatMap((call) => call.args)).to.deep.include( @@ -76,7 +98,7 @@ describe('org:delete', () => { it('will catch the ScratchOrgNotFound and wrap correctly', async () => { $$.SANDBOX.stub(Org.prototype, 'isSandbox').resolves(false); $$.SANDBOX.stub(Org.prototype, 'delete').throws(new SfError('bah!', 'ScratchOrgNotFound')); - const res = await Delete.run(['--noprompt']); + const res = await Delete.run(['--noprompt', '--target-org', testOrg.username]); expect(prompterStubs.confirm.calledOnce).to.equal(false); expect(res).to.deep.equal({ orgId: testOrg.orgId, username: testOrg.username }); expect(sfCommandUxStubs.log.getCalls().flatMap((call) => call.args)).to.deep.include( @@ -85,13 +107,14 @@ describe('org:delete', () => { }); it('will catch the SandboxNotFound and wrap correctly', async () => { - $$.SANDBOX.stub(Org.prototype, 'isSandbox').resolves(true); + $$.SANDBOX.stub(SandboxAccessor.prototype, 'hasFile').resolves(true); $$.SANDBOX.stub(Org.prototype, 'delete').throws(new SfError('bah!', 'SandboxNotFound')); - const res = await Delete.run(['--noprompt']); + const res = await Delete.run(['--noprompt', '--target-org', testOrg.username]); expect(prompterStubs.confirm.called).to.equal(false); expect(res).to.deep.equal({ orgId: testOrg.orgId, username: testOrg.username }); - expect(sfCommandUxStubs.log.getCalls().flatMap((call) => call.args)).to.deep.include( - messages.getMessage('sandboxConfigOnlySuccess', [testOrg.username]) - ); + expect( + sfCommandUxStubs.log.getCalls().flatMap((call) => call.args), + JSON.stringify(sfCommandUxStubs.log.getCalls().flatMap((call) => call.args)) + ).to.deep.include(messages.getMessage('sandboxConfigOnlySuccess', [testOrg.username])); }); });