Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 86 additions & 0 deletions test/commands/devops/pipeline/activate.nut.ts
Original file line number Diff line number Diff line change
@@ -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<ActivatePipelineResult>(
`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');
});
});
106 changes: 106 additions & 0 deletions test/commands/devops/pipeline/attach-project.nut.ts
Original file line number Diff line number Diff line change
@@ -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<AttachProjectResult>(
`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<AttachProjectResult>(
`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);
});
});
82 changes: 82 additions & 0 deletions test/commands/devops/pipeline/create.nut.ts
Original file line number Diff line number Diff line change
@@ -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<CreatePipelineResult>(
`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<CreatePipelineResult>(
`devops pipeline create --name "${name}" --repo ${GITHUB_REPO} --repo-type github --json ${orgFlag}`,
{ ensureExitCode: 0 }
);
expect(result.jsonOutput?.result.status).to.equal('Inactive');
});
});
93 changes: 93 additions & 0 deletions test/commands/devops/pipeline/stage/add.nut.ts
Original file line number Diff line number Diff line change
@@ -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<CreatePipelineResult>(
`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<AddPipelineStageResult>(
`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');
});
});
Loading
Loading