diff --git a/test/commands/devops/pipeline/activate.nut.ts b/test/commands/devops/pipeline/activate.nut.ts new file mode 100644 index 00000000..ae0b648d --- /dev/null +++ b/test/commands/devops/pipeline/activate.nut.ts @@ -0,0 +1,86 @@ +/* + * Copyright 2026, Salesforce, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { execCmd, TestSession, genUniqueString } from '@salesforce/cli-plugins-testkit'; +import { expect } from 'chai'; +import type { ActivatePipelineResult } from '../../../../src/utils/activatePipeline.js'; + +const REAL_ORG = Boolean(process.env.TESTKIT_HUB_USERNAME ?? process.env.TESTKIT_ORG_USERNAME); + +const GITHUB_REPO = 'https://github.com/salesforcecli/plugin-devops-center'; + +describe('devops pipeline activate NUTs', () => { + let session: TestSession; + let orgFlag: string; + // Pipeline created (with at least one stage) so activate can succeed + let pipelineId: string; + + before(async () => { + session = await TestSession.create({ devhubAuthStrategy: 'AUTO' }); + orgFlag = `--target-org ${session.hubOrg?.username ?? ''}`; + + if (REAL_ORG) { + const name = genUniqueString('NUT-activate-%s'); + const pipeline = execCmd<{ pipelineId: string }>( + `devops pipeline create --name "${name}" --repo ${GITHUB_REPO} --repo-type github --json ${orgFlag}`, + { ensureExitCode: 0 } + ); + pipelineId = pipeline.jsonOutput!.result.pipelineId!; + // pipeline create seeds default stages; no additional setup needed before activate + } + }); + + after(async () => { + await session?.clean(); + }); + + // ── flag-validation tests ───────────────────────────────────────────────── + + it('displays help text', () => { + const result = execCmd('devops pipeline activate --help', { ensureExitCode: 0 }); + expect(result.shellOutput.stdout).to.include('Activate a DevOps Center pipeline'); + }); + + it('errors when --pipeline-id is an invalid Salesforce ID format', () => { + const result = execCmd('devops pipeline activate --pipeline-id not-an-id', { ensureExitCode: 1 }); + expect(result.shellOutput.stderr).to.include('15 or 18 characters'); + }); + + it('errors when --target-org is missing (valid pipeline-id supplied)', () => { + const result = execCmd('devops pipeline activate --pipeline-id 0XB000000000001AAA', { ensureExitCode: 1 }); + expect(result.shellOutput.stderr).to.include('target-org'); + }); + + // ── real-org tests ──────────────────────────────────────────────────────── + + (REAL_ORG ? it : it.skip)('activates a pipeline and returns structured JSON', () => { + const result = execCmd( + `devops pipeline activate --pipeline-id ${pipelineId} --json ${orgFlag}`, + { ensureExitCode: 0 } + ); + const output = result.jsonOutput; + expect(output?.status).to.equal(0); + expect(output?.result.success).to.be.true; + expect(output?.result.pipelineId).to.equal(pipelineId); + expect(output?.result.status).to.equal('Active'); + }); + + (REAL_ORG ? it : it.skip)('errors when activating an already-active pipeline', () => { + // Pipeline was activated in the previous test; re-activating should error + const result = execCmd(`devops pipeline activate --pipeline-id ${pipelineId} ${orgFlag}`, { ensureExitCode: 1 }); + expect(result.shellOutput.stderr).to.include('already active'); + }); +}); diff --git a/test/commands/devops/pipeline/attach-project.nut.ts b/test/commands/devops/pipeline/attach-project.nut.ts new file mode 100644 index 00000000..229b246e --- /dev/null +++ b/test/commands/devops/pipeline/attach-project.nut.ts @@ -0,0 +1,106 @@ +/* + * Copyright 2026, Salesforce, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { execCmd, TestSession, genUniqueString } from '@salesforce/cli-plugins-testkit'; +import { expect } from 'chai'; +import type { AttachProjectResult } from '../../../../src/utils/attachProject.js'; + +const REAL_ORG = Boolean(process.env.TESTKIT_HUB_USERNAME ?? process.env.TESTKIT_ORG_USERNAME); + +const GITHUB_REPO = 'https://github.com/salesforcecli/plugin-devops-center'; + +describe('devops pipeline attach-project NUTs', () => { + let session: TestSession; + let orgFlag: string; + let pipelineId: string; + let projectId: string; + // Second project to test the idempotency / double-attach error path + let secondProjectId: string; + + before(async () => { + session = await TestSession.create({ devhubAuthStrategy: 'AUTO' }); + orgFlag = `--target-org ${session.hubOrg?.username ?? ''}`; + + if (REAL_ORG) { + const pipelineName = genUniqueString('NUT-attach-%s'); + const pipeline = execCmd<{ pipelineId: string }>( + `devops pipeline create --name "${pipelineName}" --repo ${GITHUB_REPO} --repo-type github --json ${orgFlag}`, + { ensureExitCode: 0 } + ); + pipelineId = pipeline.jsonOutput!.result.pipelineId!; + + const projName = genUniqueString('NUT-attach-proj-%s'); + const proj = execCmd<{ projectId: string }>(`devops project create --name "${projName}" --json ${orgFlag}`, { + ensureExitCode: 0, + }); + projectId = proj.jsonOutput!.result.projectId!; + + const projName2 = genUniqueString('NUT-attach-proj2-%s'); + const proj2 = execCmd<{ projectId: string }>(`devops project create --name "${projName2}" --json ${orgFlag}`, { + ensureExitCode: 0, + }); + secondProjectId = proj2.jsonOutput!.result.projectId!; + } + }); + + after(async () => { + await session?.clean(); + }); + + // ── flag-validation tests ───────────────────────────────────────────────── + + it('displays help text', () => { + const result = execCmd('devops pipeline attach-project --help', { ensureExitCode: 0 }); + expect(result.shellOutput.stdout).to.include('Attach a DevOps Center project to a pipeline'); + }); + + it('errors when --target-org is missing', () => { + const result = execCmd('devops pipeline attach-project', { ensureExitCode: 1 }); + expect(result.shellOutput.stderr).to.include('target-org'); + }); + + // ── real-org tests ──────────────────────────────────────────────────────── + + (REAL_ORG ? it : it.skip)('attaches a project to a pipeline and returns structured JSON', () => { + const result = execCmd( + `devops pipeline attach-project --pipeline-id ${pipelineId} --project-id ${projectId} --json ${orgFlag}`, + { ensureExitCode: 0 } + ); + const output = result.jsonOutput; + expect(output?.status).to.equal(0); + expect(output?.result.success).to.be.true; + expect(output?.result.projectId).to.equal(projectId); + expect(output?.result.pipelineId).to.equal(pipelineId); + }); + + (REAL_ORG ? it : it.skip)('errors when attaching the same project a second time', () => { + // The first attachment was done in the previous test; re-attaching should fail + const result = execCmd( + `devops pipeline attach-project --pipeline-id ${pipelineId} --project-id ${projectId} ${orgFlag}`, + { ensureExitCode: 1 } + ); + expect(result.shellOutput.stderr).to.include('already attached'); + }); + + (REAL_ORG ? it : it.skip)('attaches a second project to the same pipeline', () => { + const result = execCmd( + `devops pipeline attach-project --pipeline-id ${pipelineId} --project-id ${secondProjectId} --json ${orgFlag}`, + { ensureExitCode: 0 } + ); + expect(result.jsonOutput?.result.success).to.be.true; + expect(result.jsonOutput?.result.projectId).to.equal(secondProjectId); + }); +}); diff --git a/test/commands/devops/pipeline/create.nut.ts b/test/commands/devops/pipeline/create.nut.ts new file mode 100644 index 00000000..587c2553 --- /dev/null +++ b/test/commands/devops/pipeline/create.nut.ts @@ -0,0 +1,82 @@ +/* + * Copyright 2026, Salesforce, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { execCmd, TestSession, genUniqueString } from '@salesforce/cli-plugins-testkit'; +import { expect } from 'chai'; +import type { CreatePipelineResult } from '../../../../src/utils/createPipeline.js'; + +const REAL_ORG = Boolean(process.env.TESTKIT_HUB_USERNAME ?? process.env.TESTKIT_ORG_USERNAME); + +// Use a real GitHub repo URL that DevOps Center can validate without creating anything +const GITHUB_REPO = 'https://github.com/salesforcecli/plugin-devops-center'; + +describe('devops pipeline create NUTs', () => { + let session: TestSession; + let orgFlag: string; + + before(async () => { + session = await TestSession.create({ devhubAuthStrategy: 'AUTO' }); + orgFlag = `--target-org ${session.hubOrg?.username ?? ''}`; + }); + + after(async () => { + await session?.clean(); + }); + + // ── flag-validation tests ───────────────────────────────────────────────── + + it('displays help text', () => { + const result = execCmd('devops pipeline create --help', { ensureExitCode: 0 }); + expect(result.shellOutput.stdout).to.include('Create a DevOps Center pipeline'); + }); + + it('errors when --target-org is missing', () => { + const result = execCmd('devops pipeline create', { ensureExitCode: 1 }); + expect(result.shellOutput.stderr).to.include('target-org'); + }); + + it('rejects invalid --repo-type values', () => { + const result = execCmd(`devops pipeline create --name MyPipeline --repo ${GITHUB_REPO} --repo-type notavalidtype`, { + ensureExitCode: 2, + }); + expect(result.shellOutput.stderr).to.include('notavalidtype'); + }); + + // ── real-org tests ──────────────────────────────────────────────────────── + + (REAL_ORG ? it : it.skip)('creates a pipeline and returns structured JSON', () => { + const name = genUniqueString('NUT-pipeline-%s'); + const result = execCmd( + `devops pipeline create --name "${name}" --repo ${GITHUB_REPO} --repo-type github --json ${orgFlag}`, + { ensureExitCode: 0 } + ); + const output = result.jsonOutput; + expect(output?.status).to.equal(0); + expect(output?.result.success).to.be.true; + expect(output?.result.pipelineId).to.match(/^[a-zA-Z0-9]{15,18}$/); + expect(output?.result.name).to.equal(name); + expect(output?.result.repository?.repoType).to.equal('github'); + }); + + (REAL_ORG ? it : it.skip)('new pipeline starts in Inactive status', () => { + const name = genUniqueString('NUT-pipeline-inactive-%s'); + const result = execCmd( + `devops pipeline create --name "${name}" --repo ${GITHUB_REPO} --repo-type github --json ${orgFlag}`, + { ensureExitCode: 0 } + ); + expect(result.jsonOutput?.result.status).to.equal('Inactive'); + }); +}); diff --git a/test/commands/devops/pipeline/stage/add.nut.ts b/test/commands/devops/pipeline/stage/add.nut.ts new file mode 100644 index 00000000..958c83e8 --- /dev/null +++ b/test/commands/devops/pipeline/stage/add.nut.ts @@ -0,0 +1,93 @@ +/* + * Copyright 2026, Salesforce, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { execCmd, TestSession, genUniqueString } from '@salesforce/cli-plugins-testkit'; +import { expect } from 'chai'; +import type { AddPipelineStageResult } from '../../../../../src/utils/addPipelineStage.js'; +import type { CreatePipelineResult } from '../../../../../src/utils/createPipeline.js'; + +const REAL_ORG = Boolean(process.env.TESTKIT_HUB_USERNAME ?? process.env.TESTKIT_ORG_USERNAME); + +const GITHUB_REPO = 'https://github.com/salesforcecli/plugin-devops-center'; + +describe('devops pipeline stage add NUTs', () => { + let session: TestSession; + let orgFlag: string; + let pipelineId: string; + // One of the default stage IDs seeded by pipeline create, used as the `--next-stage-id` + let existingStageId: string; + + before(async () => { + session = await TestSession.create({ devhubAuthStrategy: 'AUTO' }); + orgFlag = `--target-org ${session.hubOrg?.username ?? ''}`; + + if (REAL_ORG) { + const name = genUniqueString('NUT-stage-add-%s'); + const pipeline = execCmd( + `devops pipeline create --name "${name}" --repo ${GITHUB_REPO} --repo-type github --json ${orgFlag}`, + { ensureExitCode: 0 } + ); + pipelineId = pipeline.jsonOutput!.result.pipelineId!; + + // Retrieve the first stage ID from the newly created pipeline via sf data query + const stagesResult = execCmd<{ records: Array<{ Id: string }> }>( + `data query --query "SELECT Id FROM DevopsPipelineStage WHERE DevopsPipelineId='${pipelineId}' ORDER BY CreatedDate ASC LIMIT 1" --json ${orgFlag}`, + { ensureExitCode: 0, cli: 'sf' } + ); + existingStageId = stagesResult.jsonOutput!.result.records[0].Id; + } + }); + + after(async () => { + await session?.clean(); + }); + + // ── flag-validation tests ───────────────────────────────────────────────── + + it('displays help text', () => { + const result = execCmd('devops pipeline stage add --help', { ensureExitCode: 0 }); + expect(result.shellOutput.stdout).to.include('Add a stage to a DevOps Center pipeline'); + }); + + it('errors when --target-org is missing', () => { + const result = execCmd('devops pipeline stage add', { ensureExitCode: 1 }); + expect(result.shellOutput.stderr).to.include('target-org'); + }); + + // ── real-org tests ──────────────────────────────────────────────────────── + + (REAL_ORG ? it : it.skip)('adds a stage before an existing stage and returns structured JSON', () => { + const stageName = genUniqueString('NUT-stage-%s'); + const result = execCmd( + `devops pipeline stage add --pipeline-id ${pipelineId} --name "${stageName}" --next-stage-id ${existingStageId} --json ${orgFlag}`, + { ensureExitCode: 0 } + ); + const output = result.jsonOutput; + expect(output?.status).to.equal(0); + expect(output?.result.success).to.be.true; + expect(output?.result.stageId).to.match(/^[a-zA-Z0-9]{15,18}$/); + expect(output?.result.name).to.equal(stageName); + expect(output?.result.nextStageId).to.equal(existingStageId); + }); + + (REAL_ORG ? it : it.skip)('errors when --next-stage-id does not belong to the pipeline', () => { + const result = execCmd( + `devops pipeline stage add --pipeline-id ${pipelineId} --name NewStage --next-stage-id 0XC000000000001AAA ${orgFlag}`, + { ensureExitCode: 1 } + ); + expect(result.shellOutput.stderr).to.include('0XC000000000001AAA'); + }); +}); diff --git a/test/commands/devops/project/create.nut.ts b/test/commands/devops/project/create.nut.ts new file mode 100644 index 00000000..5e5a060c --- /dev/null +++ b/test/commands/devops/project/create.nut.ts @@ -0,0 +1,73 @@ +/* + * Copyright 2026, Salesforce, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { execCmd, TestSession, genUniqueString } from '@salesforce/cli-plugins-testkit'; +import { expect } from 'chai'; +import type { CreateProjectResult } from '../../../../src/utils/createProject.js'; + +// These tests require a real org. Set TESTKIT_HUB_USERNAME (and TESTKIT_AUTH_URL or JWT vars) +// before running. CI sets these via secrets; locally use `sf org login web` and export the username. +const REAL_ORG = Boolean(process.env.TESTKIT_HUB_USERNAME ?? process.env.TESTKIT_ORG_USERNAME); + +describe('devops project create NUTs', () => { + let session: TestSession; + let orgFlag: string; + + before(async () => { + session = await TestSession.create({ devhubAuthStrategy: 'AUTO' }); + orgFlag = `--target-org ${session.hubOrg?.username ?? ''}`; + }); + + after(async () => { + await session?.clean(); + }); + + // ── flag-validation tests (no org required) ─────────────────────────────── + + it('displays help text', () => { + const result = execCmd('devops project create --help', { ensureExitCode: 0 }); + expect(result.shellOutput.stdout).to.include('Create a DevOps Center project'); + }); + + it('errors when --target-org is missing', () => { + const result = execCmd('devops project create', { ensureExitCode: 1 }); + expect(result.shellOutput.stderr).to.include('target-org'); + }); + + // ── real-org tests ──────────────────────────────────────────────────────── + + (REAL_ORG ? it : it.skip)('creates a project and returns structured JSON', () => { + const name = genUniqueString('NUT-project-%s'); + const result = execCmd(`devops project create --name "${name}" --json ${orgFlag}`, { + ensureExitCode: 0, + }); + const output = result.jsonOutput; + expect(output?.status).to.equal(0); + expect(output?.result.success).to.be.true; + expect(output?.result.projectId).to.match(/^[a-zA-Z0-9]{15,18}$/); + expect(output?.result.name).to.equal(name); + }); + + (REAL_ORG ? it : it.skip)('creates a project with a description', () => { + const name = genUniqueString('NUT-desc-%s'); + const desc = 'Created by NUT'; + const result = execCmd( + `devops project create --name "${name}" --description "${desc}" --json ${orgFlag}`, + { ensureExitCode: 0 } + ); + expect(result.jsonOutput?.result.description).to.equal(desc); + }); +}); diff --git a/test/commands/devops/project/list.nut.ts b/test/commands/devops/project/list.nut.ts new file mode 100644 index 00000000..c4c8e75d --- /dev/null +++ b/test/commands/devops/project/list.nut.ts @@ -0,0 +1,86 @@ +/* + * Copyright 2026, Salesforce, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { execCmd, TestSession, genUniqueString } from '@salesforce/cli-plugins-testkit'; +import { expect } from 'chai'; +import type { DevopsProjectListResult } from '../../../../src/commands/devops/project/list.js'; + +const REAL_ORG = Boolean(process.env.TESTKIT_HUB_USERNAME ?? process.env.TESTKIT_ORG_USERNAME); + +describe('devops project list NUTs', () => { + let session: TestSession; + let orgFlag: string; + let createdProjectId: string; + + before(async () => { + session = await TestSession.create({ devhubAuthStrategy: 'AUTO' }); + orgFlag = `--target-org ${session.hubOrg?.username ?? ''}`; + + if (REAL_ORG) { + // Seed a project so the list is guaranteed non-empty + const name = genUniqueString('NUT-list-seed-%s'); + const create = execCmd<{ projectId: string }>(`devops project create --name "${name}" --json ${orgFlag}`, { + ensureExitCode: 0, + }); + createdProjectId = create.jsonOutput!.result.projectId!; + } + }); + + after(async () => { + await session?.clean(); + }); + + // ── flag-validation tests ───────────────────────────────────────────────── + + it('displays help text', () => { + const result = execCmd('devops project list --help', { ensureExitCode: 0 }); + expect(result.shellOutput.stdout).to.include('List all DevOps Center projects'); + }); + + it('errors when --target-org is missing', () => { + const result = execCmd('devops project list', { ensureExitCode: 1 }); + expect(result.shellOutput.stderr).to.include('target-org'); + }); + + // ── real-org tests ──────────────────────────────────────────────────────── + + (REAL_ORG ? it : it.skip)('returns JSON with a projects array', () => { + const result = execCmd(`devops project list --json ${orgFlag}`, { + ensureExitCode: 0, + }); + const output = result.jsonOutput; + expect(output?.status).to.equal(0); + expect(output?.result.projects).to.be.an('array'); + }); + + (REAL_ORG ? it : it.skip)('lists the seeded project', () => { + const result = execCmd(`devops project list --json ${orgFlag}`, { + ensureExitCode: 0, + }); + const ids = result.jsonOutput!.result.projects.map((p) => p.Id); + expect(ids).to.include(createdProjectId); + }); + + (REAL_ORG ? it : it.skip)('each project record has Id and Name fields', () => { + const result = execCmd(`devops project list --json ${orgFlag}`, { + ensureExitCode: 0, + }); + for (const project of result.jsonOutput!.result.projects) { + expect(project.Id).to.match(/^[a-zA-Z0-9]{15,18}$/); + expect(project.Name).to.be.a('string').and.not.empty; + } + }); +}); diff --git a/test/commands/devops/pull-request/create.nut.ts b/test/commands/devops/pull-request/create.nut.ts new file mode 100644 index 00000000..2e78fc07 --- /dev/null +++ b/test/commands/devops/pull-request/create.nut.ts @@ -0,0 +1,81 @@ +/* + * Copyright 2026, Salesforce, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { execCmd, TestSession, genUniqueString } from '@salesforce/cli-plugins-testkit'; +import { expect } from 'chai'; + +const REAL_ORG = Boolean(process.env.TESTKIT_HUB_USERNAME ?? process.env.TESTKIT_ORG_USERNAME); + +describe('devops pull-request create NUTs', () => { + let session: TestSession; + let orgFlag: string; + // A work item that exists in the org but has no branch yet (freshly created) + let noBranchWorkItemName: string; + + before(async () => { + session = await TestSession.create({ devhubAuthStrategy: 'AUTO' }); + orgFlag = `--target-org ${session.hubOrg?.username ?? ''}`; + + if (REAL_ORG) { + // Create a project and a bare work item (no VCS branch assigned yet) + const projName = genUniqueString('NUT-pr-%s'); + const proj = execCmd<{ projectId: string }>(`devops project create --name "${projName}" --json ${orgFlag}`, { + ensureExitCode: 0, + }); + const projectId = proj.jsonOutput?.result.projectId as string; + + const subject = genUniqueString('NUT PR item %s'); + const wi = execCmd<{ workItemName: string }>( + `devops work-item create --project-id ${projectId} --subject "${subject}" --json ${orgFlag}`, + { ensureExitCode: 0 } + ); + noBranchWorkItemName = wi.jsonOutput!.result.workItemName!; + } + }); + + after(async () => { + await session?.clean(); + }); + + // ── flag-validation tests ───────────────────────────────────────────────── + + it('displays help text', () => { + const result = execCmd('devops pull-request create --help', { ensureExitCode: 0 }); + expect(result.shellOutput.stdout).to.include('Create a pull request'); + }); + + it('errors when --target-org is missing', () => { + const result = execCmd('devops pull-request create --work-item-name WI-001', { ensureExitCode: 1 }); + expect(result.shellOutput.stderr).to.include('target-org'); + }); + + it('errors when neither --work-item-name nor --work-item-id is supplied', () => { + // org resolution fires before exactlyOne validation → exit 1 + const result = execCmd('devops pull-request create', { ensureExitCode: 1 }); + expect(result.shellOutput.stderr).to.match(/work-item-name|work-item-id|target-org/); + }); + + // ── real-org tests ──────────────────────────────────────────────────────── + + // A work item without a DevOps Center branch assigned → command should error with NoBranch message + (REAL_ORG ? it : it.skip)('errors with NoBranch message for a work item with no branch', () => { + const result = execCmd(`devops pull-request create --work-item-name ${noBranchWorkItemName} ${orgFlag}`, { + ensureExitCode: 1, + }); + // The command errors before touching any VCS provider — no token required + expect(result.shellOutput.stderr).to.match(/no branch|NoBranch/i); + }); +}); diff --git a/test/commands/devops/stage/add-branch.nut.ts b/test/commands/devops/stage/add-branch.nut.ts new file mode 100644 index 00000000..30c39d9a --- /dev/null +++ b/test/commands/devops/stage/add-branch.nut.ts @@ -0,0 +1,99 @@ +/* + * Copyright 2026, Salesforce, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { execCmd, TestSession, genUniqueString } from '@salesforce/cli-plugins-testkit'; +import { expect } from 'chai'; +import type { AddStageBranchResult } from '../../../../src/utils/addStageBranch.js'; +import type { CreatePipelineResult } from '../../../../src/utils/createPipeline.js'; + +const REAL_ORG = Boolean(process.env.TESTKIT_HUB_USERNAME ?? process.env.TESTKIT_ORG_USERNAME); + +const GITHUB_REPO = 'https://github.com/salesforcecli/plugin-devops-center'; + +describe('devops stage add-branch NUTs', () => { + let session: TestSession; + let orgFlag: string; + // The last stage of the pipeline (no NextStageId) — branch setup must start right-to-left + let lastStageId: string; + let pipelineId: string; + + before(async () => { + session = await TestSession.create({ devhubAuthStrategy: 'AUTO' }); + orgFlag = `--target-org ${session.hubOrg?.username ?? ''}`; + + if (REAL_ORG) { + const name = genUniqueString('NUT-add-branch-%s'); + const pipeline = execCmd( + `devops pipeline create --name "${name}" --repo ${GITHUB_REPO} --repo-type github --json ${orgFlag}`, + { ensureExitCode: 0 } + ); + pipelineId = pipeline.jsonOutput!.result.pipelineId!; + + // Query for the last stage (no NextStageId) — that's where branch setup must start + const stagesResult = execCmd<{ records: Array<{ Id: string }> }>( + `data query --query "SELECT Id FROM DevopsPipelineStage WHERE DevopsPipelineId='${pipelineId}' AND NextStageId=null LIMIT 1" --json ${orgFlag}`, + { ensureExitCode: 0, cli: 'sf' } + ); + lastStageId = stagesResult.jsonOutput!.result.records[0].Id; + } + }); + + after(async () => { + await session?.clean(); + }); + + // ── flag-validation tests ───────────────────────────────────────────────── + + it('displays help text', () => { + const result = execCmd('devops stage add-branch --help', { ensureExitCode: 0 }); + expect(result.shellOutput.stdout).to.include('Add a source code repository branch to a pipeline stage'); + }); + + it('errors when --target-org is missing', () => { + const result = execCmd('devops stage add-branch', { ensureExitCode: 1 }); + expect(result.shellOutput.stderr).to.include('target-org'); + }); + + it('errors when --pipeline-id is an invalid Salesforce ID', () => { + const result = execCmd( + 'devops stage add-branch --pipeline-id not-an-id --stage-id 0XC000000000001AAA --branch-name main', + { ensureExitCode: 1 } + ); + expect(result.shellOutput.stderr).to.include('15 or 18 characters'); + }); + + // ── real-org tests ──────────────────────────────────────────────────────── + + (REAL_ORG ? it : it.skip)('adds a branch to the last stage and returns structured JSON', () => { + const result = execCmd( + `devops stage add-branch --pipeline-id ${pipelineId} --stage-id ${lastStageId} --branch-name main --json ${orgFlag}`, + { ensureExitCode: 0 } + ); + const output = result.jsonOutput; + expect(output?.status).to.equal(0); + expect(output?.result.success).to.be.true; + expect(output?.result.branchName).to.equal('main'); + expect(output?.result.repoBranchId).to.match(/^[a-zA-Z0-9]{15,18}$/); + }); + + (REAL_ORG ? it : it.skip)('errors when --stage-id does not belong to the pipeline', () => { + const result = execCmd( + `devops stage add-branch --pipeline-id ${pipelineId} --stage-id 0XC000000000001AAA --branch-name main ${orgFlag}`, + { ensureExitCode: 1 } + ); + expect(result.shellOutput.stderr).to.include('0XC000000000001AAA'); + }); +}); diff --git a/test/commands/devops/stage/add-environment.nut.ts b/test/commands/devops/stage/add-environment.nut.ts new file mode 100644 index 00000000..bd4e9a6b --- /dev/null +++ b/test/commands/devops/stage/add-environment.nut.ts @@ -0,0 +1,99 @@ +/* + * Copyright 2026, Salesforce, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { execCmd, TestSession, genUniqueString } from '@salesforce/cli-plugins-testkit'; +import { expect } from 'chai'; +import type { CreatePipelineResult } from '../../../../src/utils/createPipeline.js'; + +const REAL_ORG = Boolean(process.env.TESTKIT_HUB_USERNAME ?? process.env.TESTKIT_ORG_USERNAME); + +const GITHUB_REPO = 'https://github.com/salesforcecli/plugin-devops-center'; + +describe('devops stage add-environment NUTs', () => { + let session: TestSession; + let orgFlag: string; + let pipelineId: string; + let validStageId: string; + + before(async () => { + session = await TestSession.create({ devhubAuthStrategy: 'AUTO' }); + orgFlag = `--target-org ${session.hubOrg?.username ?? ''}`; + + if (REAL_ORG) { + const name = genUniqueString('NUT-add-env-%s'); + const pipeline = execCmd( + `devops pipeline create --name "${name}" --repo ${GITHUB_REPO} --repo-type github --json ${orgFlag}`, + { ensureExitCode: 0 } + ); + pipelineId = pipeline.jsonOutput!.result.pipelineId!; + + const stagesResult = execCmd<{ records: Array<{ Id: string }> }>( + `data query --query "SELECT Id FROM DevopsPipelineStage WHERE DevopsPipelineId='${pipelineId}' LIMIT 1" --json ${orgFlag}`, + { ensureExitCode: 0, cli: 'sf' } + ); + validStageId = stagesResult.jsonOutput!.result.records[0].Id; + } + }); + + after(async () => { + await session?.clean(); + }); + + // ── flag-validation tests ───────────────────────────────────────────────── + + it('displays help text', () => { + const result = execCmd('devops stage add-environment --help', { ensureExitCode: 0 }); + expect(result.shellOutput.stdout).to.include('Add a Salesforce environment to a pipeline stage'); + }); + + it('errors when --target-org is missing', () => { + const result = execCmd('devops stage add-environment', { ensureExitCode: 1 }); + expect(result.shellOutput.stderr).to.include('target-org'); + }); + + it('rejects invalid --org-type values', () => { + const result = execCmd( + 'devops stage add-environment --pipeline-id 0XB000000000001AAA --stage-id 0XC000000000001AAA --environment-name myEnv --org-type NotValid', + { ensureExitCode: 2 } + ); + expect(result.shellOutput.stderr).to.include('NotValid'); + }); + + // ── real-org tests ──────────────────────────────────────────────────────── + + // The full happy path requires interactive OAuth (browser open + org auth callback), + // which cannot run headlessly. We verify the command reaches the API layer by + // checking the error when a non-existent stage ID is supplied. + (REAL_ORG ? it : it.skip)('errors when --stage-id does not belong to the pipeline', () => { + const result = execCmd( + `devops stage add-environment --pipeline-id ${pipelineId} --stage-id 0XC000000000001AAA --environment-name myEnv --org-type Sandbox --no-browser ${orgFlag}`, + { ensureExitCode: 1 } + ); + expect(result.shellOutput.stderr).to.include('0XC000000000001AAA'); + }); + + (REAL_ORG ? it : it.skip)('errors with valid stage when no org auth is completed (timeout)', () => { + // With --no-browser the command waits for auth but no callback arrives → auth timeout error. + // We use a very short wait by setting the async flag; if the command has no --async, + // the timeout comes from the server side after the OAuth session expires. + // This confirms the command reaches DevOps Center and attempts the environment-creation flow. + const result = execCmd( + `devops stage add-environment --pipeline-id ${pipelineId} --stage-id ${validStageId} --environment-name NUT-env --org-type Sandbox --no-browser ${orgFlag}`, + { ensureExitCode: 1 } + ); + expect(result.shellOutput.stderr).to.match(/timed out|auth|AuthTimeout/i); + }); +}); diff --git a/test/commands/devops/work-item/create.nut.ts b/test/commands/devops/work-item/create.nut.ts new file mode 100644 index 00000000..b335b05b --- /dev/null +++ b/test/commands/devops/work-item/create.nut.ts @@ -0,0 +1,91 @@ +/* + * Copyright 2026, Salesforce, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { execCmd, TestSession, genUniqueString } from '@salesforce/cli-plugins-testkit'; +import { expect } from 'chai'; +import type { CreateWorkItemResult } from '../../../../src/utils/createWorkItem.js'; + +const REAL_ORG = Boolean(process.env.TESTKIT_HUB_USERNAME ?? process.env.TESTKIT_ORG_USERNAME); + +describe('devops work-item create NUTs', () => { + let session: TestSession; + let orgFlag: string; + // Project created in before() to host work items + let projectId: string; + + before(async () => { + session = await TestSession.create({ devhubAuthStrategy: 'AUTO' }); + orgFlag = `--target-org ${session.hubOrg?.username ?? ''}`; + + if (REAL_ORG) { + const name = genUniqueString('NUT-wi-create-%s'); + const create = execCmd<{ projectId: string }>(`devops project create --name "${name}" --json ${orgFlag}`, { + ensureExitCode: 0, + }); + projectId = create.jsonOutput!.result.projectId!; + } + }); + + after(async () => { + await session?.clean(); + }); + + // ── flag-validation tests ───────────────────────────────────────────────── + + it('displays help text', () => { + const result = execCmd('devops work-item create --help', { ensureExitCode: 0 }); + expect(result.shellOutput.stdout).to.include('Create a new work item'); + }); + + it('errors when --target-org is missing', () => { + const result = execCmd('devops work-item create', { ensureExitCode: 1 }); + expect(result.shellOutput.stderr).to.include('target-org'); + }); + + it('errors when --project-id prefix is wrong', () => { + // salesforceId flag with startsWith:'1Qg' rejects IDs that start with something else + const result = execCmd('devops work-item create --project-id 0XB000000000001AAA --subject Foo', { + ensureExitCode: 1, + }); + expect(result.shellOutput.stderr).to.include('1Qg'); + }); + + // ── real-org tests ──────────────────────────────────────────────────────── + + (REAL_ORG ? it : it.skip)('creates a work item and returns structured JSON', () => { + const subject = genUniqueString('NUT work item %s'); + const result = execCmd( + `devops work-item create --project-id ${projectId} --subject "${subject}" --json ${orgFlag}`, + { ensureExitCode: 0 } + ); + const output = result.jsonOutput; + expect(output?.status).to.equal(0); + expect(output?.result.success).to.be.true; + expect(output?.result.workItemId).to.match(/^[a-zA-Z0-9]{15,18}$/); + expect(output?.result.subject).to.equal(subject); + }); + + (REAL_ORG ? it : it.skip)('creates a work item with a description', () => { + const subject = genUniqueString('NUT wi desc %s'); + const description = 'NUT description text'; + const result = execCmd( + `devops work-item create --project-id ${projectId} --subject "${subject}" --description "${description}" --json ${orgFlag}`, + { ensureExitCode: 0 } + ); + // The workItemId proves the record was persisted; the API echoes subject back + expect(result.jsonOutput?.result.workItemId).to.match(/^[a-zA-Z0-9]{15,18}$/); + }); +}); diff --git a/test/commands/devops/work-item/list.nut.ts b/test/commands/devops/work-item/list.nut.ts new file mode 100644 index 00000000..0d54a59b --- /dev/null +++ b/test/commands/devops/work-item/list.nut.ts @@ -0,0 +1,96 @@ +/* + * Copyright 2026, Salesforce, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { execCmd, TestSession, genUniqueString } from '@salesforce/cli-plugins-testkit'; +import { expect } from 'chai'; +import type { DevopsWorkItemListResult } from '../../../../src/commands/devops/work-item/list.js'; + +const REAL_ORG = Boolean(process.env.TESTKIT_HUB_USERNAME ?? process.env.TESTKIT_ORG_USERNAME); + +describe('devops work-item list NUTs', () => { + let session: TestSession; + let orgFlag: string; + let projectId: string; + let createdWorkItemId: string; + + before(async () => { + session = await TestSession.create({ devhubAuthStrategy: 'AUTO' }); + orgFlag = `--target-org ${session.hubOrg?.username ?? ''}`; + + if (REAL_ORG) { + // Create a project and seed one work item so the list is non-empty + const projName = genUniqueString('NUT-wi-list-%s'); + const proj = execCmd<{ projectId: string }>(`devops project create --name "${projName}" --json ${orgFlag}`, { + ensureExitCode: 0, + }); + projectId = proj.jsonOutput!.result.projectId!; + + const subject = genUniqueString('seed item %s'); + const wi = execCmd<{ workItemId: string }>( + `devops work-item create --project-id ${projectId} --subject "${subject}" --json ${orgFlag}`, + { ensureExitCode: 0 } + ); + createdWorkItemId = wi.jsonOutput!.result.workItemId!; + } + }); + + after(async () => { + await session?.clean(); + }); + + // ── flag-validation tests ───────────────────────────────────────────────── + + it('displays help text', () => { + const result = execCmd('devops work-item list --help', { ensureExitCode: 0 }); + expect(result.shellOutput.stdout).to.include('List all work items'); + }); + + it('errors when --target-org is missing', () => { + const result = execCmd('devops work-item list', { ensureExitCode: 1 }); + expect(result.shellOutput.stderr).to.include('target-org'); + }); + + // ── real-org tests ──────────────────────────────────────────────────────── + + (REAL_ORG ? it : it.skip)('returns JSON with a workItems array', () => { + const result = execCmd( + `devops work-item list --project-id ${projectId} --json ${orgFlag}`, + { ensureExitCode: 0 } + ); + const output = result.jsonOutput; + expect(output?.status).to.equal(0); + expect(output?.result.workItems).to.be.an('array'); + }); + + (REAL_ORG ? it : it.skip)('lists the seeded work item', () => { + const result = execCmd( + `devops work-item list --project-id ${projectId} --json ${orgFlag}`, + { ensureExitCode: 0 } + ); + const ids = result.jsonOutput!.result.workItems.map((wi) => wi.id); + expect(ids).to.include(createdWorkItemId); + }); + + (REAL_ORG ? it : it.skip)('each work item has required fields', () => { + const result = execCmd( + `devops work-item list --project-id ${projectId} --json ${orgFlag}`, + { ensureExitCode: 0 } + ); + for (const wi of result.jsonOutput!.result.workItems) { + expect(wi).to.have.property('status').that.is.a('string'); + } + }); +}); diff --git a/test/commands/devops/work-item/status/update.nut.ts b/test/commands/devops/work-item/status/update.nut.ts new file mode 100644 index 00000000..6105e0f4 --- /dev/null +++ b/test/commands/devops/work-item/status/update.nut.ts @@ -0,0 +1,98 @@ +/* + * Copyright 2026, Salesforce, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { execCmd, TestSession, genUniqueString } from '@salesforce/cli-plugins-testkit'; +import { expect } from 'chai'; +import type { UpdateWorkItemStatusResult } from '../../../../../src/utils/updateWorkItemStatus.js'; + +const REAL_ORG = Boolean(process.env.TESTKIT_HUB_USERNAME ?? process.env.TESTKIT_ORG_USERNAME); + +describe('devops work-item status update NUTs', () => { + let session: TestSession; + let orgFlag: string; + let workItemName: string; + let workItemId: string; + + before(async () => { + session = await TestSession.create({ devhubAuthStrategy: 'AUTO' }); + orgFlag = `--target-org ${session.hubOrg?.username ?? ''}`; + + if (REAL_ORG) { + // Create a project and a work item to update + const projName = genUniqueString('NUT-wi-status-%s'); + const proj = execCmd<{ projectId: string }>(`devops project create --name "${projName}" --json ${orgFlag}`, { + ensureExitCode: 0, + }); + const projectId = proj.jsonOutput?.result.projectId as string; + + const subject = genUniqueString('NUT status item %s'); + const wi = execCmd<{ workItemId: string; workItemName: string }>( + `devops work-item create --project-id ${projectId} --subject "${subject}" --json ${orgFlag}`, + { ensureExitCode: 0 } + ); + workItemId = wi.jsonOutput!.result.workItemId!; + workItemName = wi.jsonOutput!.result.workItemName!; + } + }); + + after(async () => { + await session?.clean(); + }); + + // ── flag-validation tests ───────────────────────────────────────────────── + + it('displays help text', () => { + const result = execCmd('devops work-item status update --help', { ensureExitCode: 0 }); + expect(result.shellOutput.stdout).to.include('Update the status of a work item'); + }); + + it('errors with invalid --status value', () => { + const result = execCmd('devops work-item status update --work-item-name WI-001 --status InvalidStatus', { + ensureExitCode: 2, + }); + expect(result.shellOutput.stderr).to.include('InvalidStatus'); + }); + + it('errors when --target-org is missing (valid flags supplied)', () => { + const result = execCmd('devops work-item status update --work-item-name WI-001 --status "In Progress"', { + ensureExitCode: 1, + }); + expect(result.shellOutput.stderr).to.include('target-org'); + }); + + // ── real-org tests ──────────────────────────────────────────────────────── + + (REAL_ORG ? it : it.skip)('updates work item status by ID and returns structured JSON', () => { + const result = execCmd( + `devops work-item status update --work-item-id ${workItemId} --status "In Progress" --json ${orgFlag}`, + { ensureExitCode: 0 } + ); + const output = result.jsonOutput; + expect(output?.status).to.equal(0); + expect(output?.result.success).to.be.true; + expect(output?.result.workItemId).to.equal(workItemId); + expect(output?.result.status).to.equal('In Progress'); + }); + + (REAL_ORG ? it : it.skip)('updates work item status by name', () => { + const result = execCmd( + `devops work-item status update --work-item-name ${workItemName} --status "Ready to Promote" --json ${orgFlag}`, + { ensureExitCode: 0 } + ); + expect(result.jsonOutput?.result.success).to.be.true; + expect(result.jsonOutput?.result.status).to.equal('Ready to Promote'); + }); +}); diff --git a/test/commands/hello/world.nut.ts b/test/commands/hello/world.nut.ts deleted file mode 100644 index 7fdb26a5..00000000 --- a/test/commands/hello/world.nut.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright 2026, Salesforce, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -// TODO: remove file after we have another nut test