From 9dcc3e7bc1be13d1dcb0df13b3f7710407f9e977 Mon Sep 17 00:00:00 2001 From: Willie Ruemmele Date: Tue, 16 Nov 2021 14:53:43 -0700 Subject: [PATCH] feat: add mdapi:deploy:cancel command, refactor base classes to support MDAPI and SOURCE stash keys (#301) * fix: add mdapi:deploy:cancel command, refactor base classes to support MDAPI and SOURCE stash keys * chore: remove helpers from kit * chore: address QA issues --- command-snapshot.json | 25 ++-- messages/cancel.json | 3 +- messages/deploy.json | 3 +- messages/md.cancel.json | 20 +++ src/commands/force/mdapi/deploy/cancel.ts | 85 +++++++++++++ src/commands/force/source/delete.ts | 2 +- src/commands/force/source/deploy.ts | 2 +- src/commands/force/source/deploy/cancel.ts | 32 +++-- src/commands/force/source/deploy/report.ts | 21 ++-- src/commands/force/source/retrieve.ts | 8 +- src/deployCommand.ts | 47 +++++-- src/sourceCommand.ts | 2 +- test/commands/mdapi/cancel.test.ts | 139 +++++++++++++++++++++ test/commands/source/cancel.test.ts | 17 +-- test/nuts/mdapi.nut.ts | 36 +++++- 15 files changed, 382 insertions(+), 60 deletions(-) create mode 100644 messages/md.cancel.json create mode 100644 src/commands/force/mdapi/deploy/cancel.ts create mode 100644 test/commands/mdapi/cancel.test.ts diff --git a/command-snapshot.json b/command-snapshot.json index 907667612..03bbd70a7 100644 --- a/command-snapshot.json +++ b/command-snapshot.json @@ -1,4 +1,19 @@ [ + { + "command": "force:mdapi:deploy:cancel", + "plugin": "@salesforce/plugin-source", + "flags": ["apiversion", "jobid", "json", "loglevel", "targetusername", "wait"] + }, + { + "command": "force:mdapi:describemetadata", + "plugin": "@salesforce/plugin-source", + "flags": ["apiversion", "filterknown", "json", "loglevel", "resultfile", "targetusername"] + }, + { + "command": "force:mdapi:listmetadata", + "plugin": "@salesforce/plugin-source", + "flags": ["apiversion", "folder", "json", "loglevel", "metadatatype", "resultfile", "targetusername"] + }, { "command": "force:source:beta:pull", "plugin": "@salesforce/plugin-source", @@ -105,15 +120,5 @@ "verbose", "wait" ] - }, - { - "command": "force:mdapi:listmetadata", - "plugin": "@salesforce/plugin-source", - "flags": ["apiversion", "json", "loglevel", "resultfile", "targetusername", "metadatatype", "folder"] - }, - { - "command": "force:mdapi:describemetadata", - "plugin": "@salesforce/plugin-source", - "flags": ["apiversion", "json", "loglevel", "resultfile", "targetusername", "filterknown"] } ] diff --git a/messages/cancel.json b/messages/cancel.json index 48e65ed41..a3136babe 100644 --- a/messages/cancel.json +++ b/messages/cancel.json @@ -10,5 +10,6 @@ }, "flagsLong": { "wait": "Number of minutes to wait for the command to complete and display results to the terminal window. If the command continues to run after the wait period, the CLI returns control of the terminal window to you. " - } + }, + "CancelFailed": "The cancel command failed due to: %s" } diff --git a/messages/deploy.json b/messages/deploy.json index b51623c92..5336d3aa2 100644 --- a/messages/deploy.json +++ b/messages/deploy.json @@ -81,5 +81,6 @@ "deployFailed": "Deploy failed.", "asyncDeployQueued": "Deploy has been queued.", "asyncDeployCancel": "Run sfdx force:source:deploy:cancel -i %s to cancel the deploy.", - "asyncDeployReport": "Run sfdx force:source:deploy:report -i %s to get the latest status." + "asyncDeployReport": "Run sfdx force:source:deploy:report -i %s to get the latest status.", + "invalidDeployId": "The provided ID is invalid, deploy IDs must start with '0Af'" } diff --git a/messages/md.cancel.json b/messages/md.cancel.json new file mode 100644 index 000000000..a45009542 --- /dev/null +++ b/messages/md.cancel.json @@ -0,0 +1,20 @@ +{ + "description": "cancel a metadata deployment \n Use this command to cancel a specified asynchronous metadata deployment. You can also specify a wait time (in minutes) to check for updates to the canceled deploy status.", + "longDescription": "Cancels an asynchronous metadata deployment.", + "flags": { + "wait": "wait time for command to finish in minutes", + "waitLong": "Number of minutes to wait for the command to complete and display results to the terminal window. If the command continues to run after the wait period, the CLI returns control of the terminal window to you. The default is 33 minutes.", + "jobid": "job ID of the deployment you want to cancel; defaults to your most recent CLI deployment if not specified" + }, + + "examples": [ + "Deploy a directory of files to the org", + " $ sfdx force:mdapi:deploy -d ", + "Now cancel this deployment and wait two minutes", + " $ sfdx force:mdapi:deploy:cancel -w 2", + "If you have multiple deployments in progress and want to cancel a specific one, specify the job ID", + " $ sfdx force:mdapi:deploy:cancel -i ", + "Check the status of the cancel job", + " $ sfdx force:mdapi:deploy:report" + ] +} diff --git a/src/commands/force/mdapi/deploy/cancel.ts b/src/commands/force/mdapi/deploy/cancel.ts new file mode 100644 index 000000000..ea7919810 --- /dev/null +++ b/src/commands/force/mdapi/deploy/cancel.ts @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2020, salesforce.com, inc. + * All rights reserved. + * 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 * as os from 'os'; +import { flags, FlagsConfig } from '@salesforce/command'; +import { Messages, SfdxError } from '@salesforce/core'; +import { Duration } from '@salesforce/kit'; +import { RequestStatus } from '@salesforce/source-deploy-retrieve'; +import { DeployCommand } from '../../../../deployCommand'; +import { + DeployCancelCommandResult, + DeployCancelResultFormatter, +} from '../../../../formatters/deployCancelResultFormatter'; + +Messages.importMessagesDirectory(__dirname); +const messages = Messages.loadMessages('@salesforce/plugin-source', 'md.cancel'); + +export class Cancel extends DeployCommand { + public static readonly description = messages.getMessage('description'); + public static readonly examples = messages.getMessage('examples').split(os.EOL); + public static readonly requiresUsername = true; + public static readonly flagsConfig: FlagsConfig = { + wait: flags.minutes({ + char: 'w', + default: Duration.minutes(DeployCommand.DEFAULT_WAIT_MINUTES), + min: Duration.minutes(1), + description: messages.getMessage('flags.wait'), + longDescription: messages.getMessage('flags.waitLong'), + }), + jobid: flags.id({ + char: 'i', + description: messages.getMessage('flags.jobid'), + validate: (val) => { + if (val.startsWith('0Af')) { + return true; + } else { + throw SfdxError.create('@salesforce/plugin-source', 'deploy', 'invalidDeployId'); + } + }, + }), + }; + // The most important difference between this and source:deploy:cancel + public isSourceStash = false; + + public async run(): Promise { + await this.cancel(); + this.resolveSuccess(); + return this.formatResult(); + } + + protected async cancel(): Promise { + const deployId = this.resolveDeployId(this.getFlag('jobid')); + try { + const deploy = this.createDeploy(deployId); + await deploy.cancel(); + + this.deployResult = await this.poll(deployId); + } catch (e) { + if (e instanceof Error) { + throw SfdxError.create('@salesforce/plugin-source', 'cancel', 'CancelFailed', [e.message]); + } else { + throw SfdxError.wrap(e); + } + } + } + + protected resolveSuccess(): void { + const status = this.deployResult.response.status; + if (status !== RequestStatus.Canceled) { + this.setExitCode(1); + } + } + + protected formatResult(): DeployCancelCommandResult { + const formatter = new DeployCancelResultFormatter(this.logger, this.ux, this.deployResult); + if (!this.isJsonOutput()) { + formatter.display(); + } + return formatter.getJson(); + } +} diff --git a/src/commands/force/source/delete.ts b/src/commands/force/source/delete.ts index 3e24f25d5..e422f7f33 100644 --- a/src/commands/force/source/delete.ts +++ b/src/commands/force/source/delete.ts @@ -44,7 +44,7 @@ export class Delete extends DeployCommand { }), wait: flags.minutes({ char: 'w', - default: Duration.minutes(Delete.DEFAULT_SRC_WAIT_MINUTES), + default: Duration.minutes(Delete.DEFAULT_WAIT_MINUTES), min: Duration.minutes(1), description: messages.getMessage('flags.wait'), longDescription: messages.getMessage('flagsLong.wait'), diff --git a/src/commands/force/source/deploy.ts b/src/commands/force/source/deploy.ts index f8688f426..42f3aeb17 100644 --- a/src/commands/force/source/deploy.ts +++ b/src/commands/force/source/deploy.ts @@ -42,7 +42,7 @@ export class Deploy extends DeployCommand { }), wait: flags.minutes({ char: 'w', - default: Duration.minutes(Deploy.DEFAULT_SRC_WAIT_MINUTES), + default: Duration.minutes(Deploy.DEFAULT_WAIT_MINUTES), min: Duration.minutes(0), // wait=0 means deploy is asynchronous description: messages.getMessage('flags.wait'), longDescription: messages.getMessage('flagsLong.wait'), diff --git a/src/commands/force/source/deploy/cancel.ts b/src/commands/force/source/deploy/cancel.ts index a6bf87d83..39005ef93 100644 --- a/src/commands/force/source/deploy/cancel.ts +++ b/src/commands/force/source/deploy/cancel.ts @@ -7,9 +7,8 @@ import * as os from 'os'; import { flags, FlagsConfig } from '@salesforce/command'; -import { Messages } from '@salesforce/core'; +import { Messages, SfdxError } from '@salesforce/core'; import { Duration } from '@salesforce/kit'; -import { getString } from '@salesforce/ts-types'; import { RequestStatus } from '@salesforce/source-deploy-retrieve'; import { DeployCommand } from '../../../../deployCommand'; import { @@ -27,7 +26,7 @@ export class Cancel extends DeployCommand { public static readonly flagsConfig: FlagsConfig = { wait: flags.minutes({ char: 'w', - default: Duration.minutes(DeployCommand.DEFAULT_SRC_WAIT_MINUTES), + default: Duration.minutes(DeployCommand.DEFAULT_WAIT_MINUTES), min: Duration.minutes(1), description: messages.getMessage('flags.wait'), longDescription: messages.getMessage('flagsLong.wait'), @@ -35,6 +34,13 @@ export class Cancel extends DeployCommand { jobid: flags.id({ char: 'i', description: messages.getMessage('flags.jobid'), + validate: (val) => { + if (val.startsWith('0Af')) { + return true; + } else { + throw SfdxError.create('@salesforce/plugin-source', 'deploy', 'invalidDeployId'); + } + }, }), }; @@ -46,18 +52,22 @@ export class Cancel extends DeployCommand { protected async cancel(): Promise { const deployId = this.resolveDeployId(this.getFlag('jobid')); + try { + const deploy = this.createDeploy(deployId); + await deploy.cancel(); - // TODO: update to use SDRL. This matches the toolbelt implementation. - // eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-assignment - await this.org.getConnection().metadata['_invoke']('cancelDeploy', { - deployId, - }); - - this.deployResult = await this.poll(deployId); + this.deployResult = await this.poll(deployId); + } catch (e) { + if (e instanceof Error) { + throw SfdxError.create('@salesforce/plugin-source', 'cancel', 'CancelFailed', [e.message]); + } else { + throw SfdxError.wrap(e); + } + } } protected resolveSuccess(): void { - const status = getString(this.deployResult, 'response.status'); + const status = this.deployResult.response.status; if (status !== RequestStatus.Canceled) { this.setExitCode(1); } diff --git a/src/commands/force/source/deploy/report.ts b/src/commands/force/source/deploy/report.ts index 4cfa6334a..287654b6c 100644 --- a/src/commands/force/source/deploy/report.ts +++ b/src/commands/force/source/deploy/report.ts @@ -6,10 +6,9 @@ */ import * as os from 'os'; -import { Messages, SfdxProject } from '@salesforce/core'; +import { Messages, SfdxError, SfdxProject } from '@salesforce/core'; import { flags, FlagsConfig } from '@salesforce/command'; import { Duration, env } from '@salesforce/kit'; -import { MetadataApiDeploy } from '@salesforce/source-deploy-retrieve'; import { DeployCommand } from '../../../../deployCommand'; import { DeployReportCommandResult, @@ -30,7 +29,7 @@ export class Report extends DeployCommand { public static readonly flagsConfig: FlagsConfig = { wait: flags.minutes({ char: 'w', - default: Duration.minutes(DeployCommand.DEFAULT_SRC_WAIT_MINUTES), + default: Duration.minutes(DeployCommand.DEFAULT_WAIT_MINUTES), min: Duration.minutes(1), description: messages.getMessage('flags.wait'), longDescription: messages.getMessage('flagsLong.wait'), @@ -39,6 +38,13 @@ export class Report extends DeployCommand { char: 'i', description: messages.getMessage('flags.jobid'), longDescription: messages.getMessage('flagsLong.jobid'), + validate: (val) => { + if (val.startsWith('0Af')) { + return true; + } else { + throw SfdxError.create('@salesforce/plugin-source', 'deploy', 'invalidDeployId'); + } + }, }), verbose: flags.builtin({ description: messages.getMessage('flags.verbose'), @@ -50,15 +56,6 @@ export class Report extends DeployCommand { return this.formatResult(); } - /** - * This method is here to provide a workaround to stubbing a constructor in the tests. - * - * @param id - */ - public createDeploy(id?: string): MetadataApiDeploy { - return new MetadataApiDeploy({ usernameOrConnection: this.org.getUsername(), id }); - } - protected async doReport(): Promise { const deployId = this.resolveDeployId(this.getFlag('jobid')); diff --git a/src/commands/force/source/retrieve.ts b/src/commands/force/source/retrieve.ts index 1e2f9027d..fbce92584 100644 --- a/src/commands/force/source/retrieve.ts +++ b/src/commands/force/source/retrieve.ts @@ -10,12 +10,12 @@ import { join } from 'path'; import { flags, FlagsConfig } from '@salesforce/command'; import { Messages, SfdxProject } from '@salesforce/core'; import { Duration } from '@salesforce/kit'; -import { RetrieveResult, ComponentSet, RequestStatus } from '@salesforce/source-deploy-retrieve'; +import { ComponentSet, RequestStatus, RetrieveResult } from '@salesforce/source-deploy-retrieve'; import { SourceCommand } from '../../../sourceCommand'; import { - RetrieveResultFormatter, - RetrieveCommandResult, PackageRetrieval, + RetrieveCommandResult, + RetrieveResultFormatter, } from '../../../formatters/retrieveResultFormatter'; import { ComponentSetBuilder } from '../../../componentSetBuilder'; @@ -41,7 +41,7 @@ export class Retrieve extends SourceCommand { }), wait: flags.minutes({ char: 'w', - default: Duration.minutes(SourceCommand.DEFAULT_SRC_WAIT_MINUTES), + default: Duration.minutes(SourceCommand.DEFAULT_WAIT_MINUTES), min: Duration.minutes(1), description: messages.getMessage('flags.wait'), longDescription: messages.getMessage('flagsLong.wait'), diff --git a/src/deployCommand.ts b/src/deployCommand.ts index 0832a225f..85de4aa53 100644 --- a/src/deployCommand.ts +++ b/src/deployCommand.ts @@ -6,9 +6,14 @@ */ import * as fs from 'fs'; -import { ComponentSet, DeployResult, MetadataApiDeployStatus } from '@salesforce/source-deploy-retrieve'; +import { + ComponentSet, + DeployResult, + MetadataApiDeploy, + MetadataApiDeployStatus, +} from '@salesforce/source-deploy-retrieve'; import { ConfigAggregator, ConfigFile, PollingClient, SfdxError, StatusResult } from '@salesforce/core'; -import { AnyJson, asString, getBoolean } from '@salesforce/ts-types'; +import { AnyJson, getBoolean } from '@salesforce/ts-types'; import { Duration, once } from '@salesforce/kit'; import { SourceCommand } from './sourceCommand'; @@ -18,17 +23,19 @@ interface StashFile { } export abstract class DeployCommand extends SourceCommand { - protected static readonly STASH_KEY = 'SOURCE_DEPLOY'; + protected static readonly SOURCE_STASH_KEY = 'SOURCE_DEPLOY'; + protected static readonly MDAPI_STASH_KEY = 'MDAPI_DEPLOY'; protected displayDeployId = once((id: string) => { if (!this.isJsonOutput()) { this.ux.log(`Deploy ID: ${id}`); } }); - + // used to determine the correct stash.json key + protected isSourceStash = true; protected deployResult: DeployResult; /** - * Request a report of an in-progess or completed deployment. + * Request a report of an in-progress or completed deployment. * * @param id the Deploy ID of a deployment request * @returns DeployResult @@ -47,10 +54,25 @@ export abstract class DeployCommand extends SourceCommand { protected setStash(deployId: string): void { const file = this.getStash(); this.logger.debug(`Stashing deploy ID: ${deployId} in ${file.getPath()}`); - file.writeSync({ [DeployCommand.STASH_KEY]: { jobid: deployId } }); + file.writeSync({ + [this.getStashKey()]: { jobid: deployId }, + }); + } + + /** + * This method is here to provide a workaround to stubbing a constructor in the tests. + * + * @param id + */ + protected createDeploy(id?: string): MetadataApiDeploy { + return new MetadataApiDeploy({ usernameOrConnection: this.org.getUsername(), id }); + } + + protected getStashKey(): string { + return this.isSourceStash ? DeployCommand.SOURCE_STASH_KEY : DeployCommand.MDAPI_STASH_KEY; } - protected resolveDeployId(id: string): string { + protected resolveDeployId(id?: string): string { let stash: ConfigFile; if (id) { return id; @@ -58,13 +80,17 @@ export abstract class DeployCommand extends SourceCommand { try { stash = this.getStash(); stash.readSync(true); - const deployId = asString((stash.get(DeployCommand.STASH_KEY) as { jobid: string }).jobid); + const deployId = ( + stash.get(this.getStashKey()) as { + jobid: string; + } + ).jobid; this.logger.debug(`Using deploy ID: ${deployId} from ${stash.getPath()}`); return deployId; } catch (err: unknown) { const error = err as Error & { code: string }; if (error.name === 'JsonParseError') { - const stashFilePath = stash.getPath(); + const stashFilePath = stash?.getPath(); const corruptFilePath = `${stashFilePath}_corrupted_${Date.now()}`; fs.renameSync(stashFilePath, corruptFilePath); const invalidStashErr = SfdxError.create('@salesforce/plugin-source', 'deploy', 'InvalidStashFile', [ @@ -74,7 +100,8 @@ export abstract class DeployCommand extends SourceCommand { invalidStashErr.stack = `${invalidStashErr.stack}\nDue to:\n${error.stack}`; throw invalidStashErr; } - if (error.code === 'ENOENT') { + if (error.code === 'ENOENT' || !stash?.get(this.getStashKey())) { + // if the file doesn't exist, or the key doesn't exist in the stash throw SfdxError.create('@salesforce/plugin-source', 'deploy', 'MissingDeployId'); } throw SfdxError.wrap(error); diff --git a/src/sourceCommand.ts b/src/sourceCommand.ts index a8941fdf3..0b7326bc8 100644 --- a/src/sourceCommand.ts +++ b/src/sourceCommand.ts @@ -20,7 +20,7 @@ export type ProgressBar = { }; export abstract class SourceCommand extends SfdxCommand { - public static readonly DEFAULT_SRC_WAIT_MINUTES = 33; + public static readonly DEFAULT_WAIT_MINUTES = 33; protected xorFlags: string[] = []; protected progressBar?: ProgressBar; diff --git a/test/commands/mdapi/cancel.test.ts b/test/commands/mdapi/cancel.test.ts new file mode 100644 index 000000000..59cc13fe6 --- /dev/null +++ b/test/commands/mdapi/cancel.test.ts @@ -0,0 +1,139 @@ +/* + * Copyright (c) 2020, salesforce.com, inc. + * All rights reserved. + * 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 { join } from 'path'; +import * as sinon from 'sinon'; +import { expect } from 'chai'; +import { fromStub, spyMethod, stubInterface, stubMethod } from '@salesforce/ts-sinon'; +import { ConfigFile, Org, SfdxProject } from '@salesforce/core'; +import { IConfig } from '@oclif/config'; +import { UX } from '@salesforce/command'; +import { MetadataApiDeploy } from '@salesforce/source-deploy-retrieve'; +import { Cancel } from '../../../src/commands/force/mdapi/deploy/cancel'; +import { DeployCancelResultFormatter } from '../../../src/formatters/deployCancelResultFormatter'; +import { DeployCommandResult } from '../../../src/formatters/deployResultFormatter'; +import { getDeployResult } from '../source/deployResponses'; + +describe('force:source:mdapi:cancel', () => { + const sandbox = sinon.createSandbox(); + const username = 'cancel-test@org.com'; + const defaultDir = join('my', 'default', 'package'); + const stashedDeployId = 'IMA000STASHID'; + + const deployResult = getDeployResult('canceled'); + const expectedResults = deployResult.response as DeployCommandResult; + expectedResults.deployedSource = deployResult.getFileResponses(); + expectedResults.outboundFiles = []; + expectedResults.deploys = [deployResult.response]; + + // Stubs + const oclifConfigStub = fromStub(stubInterface(sandbox)); + let checkDeployStatusStub: sinon.SinonStub; + let cancelStub: sinon.SinonStub; + let uxLogStub: sinon.SinonStub; + + class TestCancel extends Cancel { + public async runIt() { + await this.init(); + return this.run(); + } + public setOrg(org: Org) { + this.org = org; + } + public setProject(project: SfdxProject) { + this.project = project; + } + + public createDeploy(): MetadataApiDeploy { + cancelStub = sandbox.stub(MetadataApiDeploy.prototype, 'cancel'); + return MetadataApiDeploy.prototype; + } + } + + const runCancelCmd = async (params: string[]) => { + const cmd = new TestCancel(params, oclifConfigStub); + stubMethod(sandbox, cmd, 'assignProject').callsFake(() => { + const sfdxProjectStub = fromStub( + stubInterface(sandbox, { + getUniquePackageDirectories: () => [{ fullPath: defaultDir }], + }) + ); + cmd.setProject(sfdxProjectStub); + }); + stubMethod(sandbox, cmd, 'assignOrg').callsFake(() => { + const orgStub = fromStub( + stubInterface(sandbox, { + getUsername: () => username, + getConnection: () => ({ + metadata: { + checkDeployStatus: checkDeployStatusStub, + }, + }), + }) + ); + cmd.setOrg(orgStub); + }); + uxLogStub = stubMethod(sandbox, UX.prototype, 'log'); + stubMethod(sandbox, ConfigFile.prototype, 'readSync'); + stubMethod(sandbox, ConfigFile.prototype, 'get').returns({ jobid: stashedDeployId }); + checkDeployStatusStub = sandbox.stub().resolves(expectedResults); + + return cmd.runIt(); + }; + + afterEach(() => { + sandbox.restore(); + }); + + it('should use stashed deploy ID', async () => { + const getStashSpy = spyMethod(sandbox, Cancel.prototype, 'getStash'); + const result = await runCancelCmd(['--json']); + expect(result).to.deep.equal(expectedResults); + expect(getStashSpy.called).to.equal(true); + expect(checkDeployStatusStub.firstCall.args[0]).to.equal(stashedDeployId); + expect(cancelStub.calledOnce).to.equal(true); + }); + + it('should display stashed deploy ID', async () => { + const result = await runCancelCmd([]); + expect(result).to.deep.equal(expectedResults); + expect(uxLogStub.firstCall.args[0]).to.contain(stashedDeployId); + }); + + it('should use the jobid flag', async () => { + const getStashSpy = spyMethod(sandbox, Cancel.prototype, 'getStash'); + const result = await runCancelCmd(['--json', '--jobid', expectedResults.id]); + expect(result).to.deep.equal(expectedResults); + expect(getStashSpy.called).to.equal(false); + expect(checkDeployStatusStub.firstCall.args[0]).to.equal(expectedResults.id); + expect(cancelStub.calledOnce).to.equal(true); + }); + + it('should display the jobid flag', async () => { + const result = await runCancelCmd(['--jobid', expectedResults.id]); + expect(result).to.deep.equal(expectedResults); + expect(uxLogStub.firstCall.args[0]).to.contain(expectedResults.id); + }); + + it('should display output with no --json', async () => { + const displayStub = sandbox.stub(DeployCancelResultFormatter.prototype, 'display'); + const getJsonStub = sandbox.stub(DeployCancelResultFormatter.prototype, 'getJson'); + await runCancelCmd([]); + expect(displayStub.calledOnce).to.equal(true); + expect(getJsonStub.calledOnce).to.equal(true); + expect(uxLogStub.called).to.equal(true); + }); + + it('should NOT display output with --json', async () => { + const displayStub = sandbox.stub(DeployCancelResultFormatter.prototype, 'display'); + const getJsonStub = sandbox.stub(DeployCancelResultFormatter.prototype, 'getJson'); + await runCancelCmd(['--json']); + expect(displayStub.calledOnce).to.equal(false); + expect(getJsonStub.calledOnce).to.equal(true); + expect(uxLogStub.called).to.equal(false); + }); +}); diff --git a/test/commands/source/cancel.test.ts b/test/commands/source/cancel.test.ts index 0b1e7f4a8..4cfdcffd8 100644 --- a/test/commands/source/cancel.test.ts +++ b/test/commands/source/cancel.test.ts @@ -12,12 +12,13 @@ import { fromStub, spyMethod, stubInterface, stubMethod } from '@salesforce/ts-s import { ConfigFile, Org, SfdxProject } from '@salesforce/core'; import { IConfig } from '@oclif/config'; import { UX } from '@salesforce/command'; +import { MetadataApiDeploy } from '@salesforce/source-deploy-retrieve'; import { Cancel } from '../../../src/commands/force/source/deploy/cancel'; import { DeployCancelResultFormatter } from '../../../src/formatters/deployCancelResultFormatter'; import { DeployCommandResult } from '../../../src/formatters/deployResultFormatter'; import { getDeployResult } from './deployResponses'; -describe('force:source:cancel', () => { +describe('force:source:deploy:cancel', () => { const sandbox = sinon.createSandbox(); const username = 'cancel-test@org.com'; const defaultDir = join('my', 'default', 'package'); @@ -32,7 +33,7 @@ describe('force:source:cancel', () => { // Stubs const oclifConfigStub = fromStub(stubInterface(sandbox)); let checkDeployStatusStub: sinon.SinonStub; - let invokeStub: sinon.SinonStub; + let cancelStub: sinon.SinonStub; let uxLogStub: sinon.SinonStub; class TestCancel extends Cancel { @@ -46,6 +47,11 @@ describe('force:source:cancel', () => { public setProject(project: SfdxProject) { this.project = project; } + + public createDeploy(): MetadataApiDeploy { + cancelStub = sandbox.stub(MetadataApiDeploy.prototype, 'cancel'); + return MetadataApiDeploy.prototype; + } } const runCancelCmd = async (params: string[]) => { @@ -65,8 +71,6 @@ describe('force:source:cancel', () => { getConnection: () => ({ metadata: { checkDeployStatus: checkDeployStatusStub, - // TODO: FIX FOR SDR CHANGES WHEN THEY ARE PUBLISHED - _invoke: invokeStub, }, }), }) @@ -77,7 +81,6 @@ describe('force:source:cancel', () => { stubMethod(sandbox, ConfigFile.prototype, 'readSync'); stubMethod(sandbox, ConfigFile.prototype, 'get').returns({ jobid: stashedDeployId }); checkDeployStatusStub = sandbox.stub().resolves(expectedResults); - invokeStub = sandbox.stub().resolves(); return cmd.runIt(); }; @@ -92,7 +95,7 @@ describe('force:source:cancel', () => { expect(result).to.deep.equal(expectedResults); expect(getStashSpy.called).to.equal(true); expect(checkDeployStatusStub.firstCall.args[0]).to.equal(stashedDeployId); - expect(invokeStub.firstCall.args[1]).to.deep.equal({ deployId: stashedDeployId }); + expect(cancelStub.calledOnce).to.equal(true); }); it('should display stashed deploy ID', async () => { @@ -107,7 +110,7 @@ describe('force:source:cancel', () => { expect(result).to.deep.equal(expectedResults); expect(getStashSpy.called).to.equal(false); expect(checkDeployStatusStub.firstCall.args[0]).to.equal(expectedResults.id); - expect(invokeStub.firstCall.args[1]).to.deep.equal({ deployId: expectedResults.id }); + expect(cancelStub.calledOnce).to.equal(true); }); it('should display the jobid flag', async () => { diff --git a/test/nuts/mdapi.nut.ts b/test/nuts/mdapi.nut.ts index f52f333da..462d4b290 100644 --- a/test/nuts/mdapi.nut.ts +++ b/test/nuts/mdapi.nut.ts @@ -5,8 +5,10 @@ * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ import { expect } from 'chai'; -import { TestSession, execCmd } from '@salesforce/cli-plugins-testkit'; +import { execCmd, TestSession } from '@salesforce/cli-plugins-testkit'; import { DescribeMetadataResult } from 'jsforce'; +import { exec } from 'shelljs'; +import { DeployCancelCommandResult } from '../../src/formatters/deployCancelResultFormatter'; let session: TestSession; @@ -58,6 +60,38 @@ describe('mdapi NUTs', () => { }); }); + describe('mdapi:deploy:cancel', () => { + it('will cancel an mdapi deploy via the stash.json', () => { + execCmd('force:source:convert --outputdir mdapi'); + // TODO: once mdapi:deploy is migrated switch to execCmd + const deploy = JSON.parse(exec('sfdx force:mdapi:deploy -d mdapi -w 0 --json', { silent: true })) as { + result: { id: string }; + }; + const result = execCmd('force:mdapi:deploy:cancel --json'); + expect(result.jsonOutput.status).to.equal(0); + const json = result.jsonOutput.result; + expect(json).to.have.property('canceledBy'); + expect(json).to.have.property('status'); + expect(json.status).to.equal('Canceled'); + expect(json.id).to.equal(deploy.result.id); + }); + + it('will cancel an mdapi deploy via the specified deploy id', () => { + execCmd('force:source:convert --outputdir mdapi'); + // TODO: once mdapi:deploy is migrated switch to execCmd + const deploy = JSON.parse(exec('sfdx force:mdapi:deploy -d mdapi -w 0 --json', { silent: true })) as { + result: { id: string }; + }; + const result = execCmd(`force:mdapi:deploy:cancel --json --jobid ${deploy.result.id}`); + expect(result.jsonOutput.status).to.equal(0); + const json = result.jsonOutput.result; + expect(json).to.have.property('canceledBy'); + expect(json).to.have.property('status'); + expect(json.status).to.equal('Canceled'); + expect(json.id).to.equal(deploy.result.id); + }); + }); + // *** More NUTs will be added/uncommented here as commands are moved from toolbelt to // this plugin. Keeping these toolbelt tests here for reference.