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
88 changes: 88 additions & 0 deletions test/commands/devops/nutHelpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/*
* 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 } from '@salesforce/cli-plugins-testkit';

/**
* Real-org NUTs run only when the environment supplies org auth. CI sets these
* vars to empty strings when no org is configured, so `.some(Boolean)` (not `??`)
* is required — empty strings must be treated as "not set".
*/
export const REAL_ORG = [
process.env.TESTKIT_HUB_USERNAME,
process.env.TESTKIT_ORG_USERNAME,
process.env.TESTKIT_AUTH_URL,
].some(Boolean);

/**
* GitHub repo the pipeline/stage/branch fixtures connect a DevOps Center
* pipeline to. The target org's DevOps Center must have a GitHub source
* authorized against this repo for those real-org tests to run (otherwise the
* server returns REPO_NOT_FOUND_OR_UNAUTHORIZED and the fixture guard skips
* them). Override per-environment with DC_NUT_REPO; the default is a non-SSO
* repo so CI isn't gated on SAML-protected org access.
*/
export const GITHUB_REPO = process.env.DC_NUT_REPO ?? 'https://github.com/ad-shreya/Solar';

/**
* Branch that `stage branch add` associates with a pipeline stage. It must already
* exist in GITHUB_REPO and must NOT be the repo's mainline (`main`): the server
* reserves the mainline and rejects associating it to a stage ("Failed to create
* Source Code Repository Branch"). A non-existent branch fails too ("Branch does
* not exist"), so this must name a real, non-default branch. Override with
* DC_NUT_STAGE_BRANCH when pointing at a different fixture repo.
*/
export const STAGE_BRANCH = process.env.DC_NUT_STAGE_BRANCH ?? 'staging';

/**
* A target org may be authenticated but not have the DevOps Center feature
* enabled (e.g. the shared CI dev hub). In that case every API-backed command
* fails (FUNCTIONALITY_NOT_ENABLED, or "sObject type 'DevopsPipeline' is not
* supported"). Probe once with a read-only command so real-org NUTs can skip
* cleanly instead of failing the whole suite.
*
* Detection is by exit code, not error text, so it is robust to whichever error
* a disabled org returns: `devops pipeline list` exits 0 only when the feature
* is enabled (even with zero pipelines). Returns false when there is no real
* org, so callers can gate both fixture setup and the real-org assertions on a
* single flag.
*/
export const isDevopsCenterEnabled = (orgFlag: string): boolean => {
if (!REAL_ORG) return false;
const result = execCmd(`devops pipeline list --json ${orgFlag}`, { ensureExitCode: undefined });
return result.jsonOutput?.status === 0;
};

export type SeededWorkItem = { workItemId: string; workItemName: string; subject: string };

/**
* `work-item create` returns only `{ success, subject }` — no id or name. Fixtures
* that need to update/review/promote a work item must read it back from
* `work-item list` (which returns `{ id, name, subject, ... }`). Create the item,
* then resolve its id and name by matching the unique subject.
*/
export const createWorkItem = (projectId: string, subject: string, orgFlag: string): SeededWorkItem => {
execCmd(`devops work-item create --project-id ${projectId} --subject "${subject}" --json ${orgFlag}`, {
ensureExitCode: 0,
});
const list = execCmd<{ workItems: Array<{ id: string; name: string; subject: string }> }>(
`devops work-item list --project-id ${projectId} --json ${orgFlag}`,
{ ensureExitCode: 0 }
);
const wi = (list.jsonOutput?.result.workItems ?? []).find((w) => w.subject === subject);
if (!wi) throw new Error(`Seeded work item '${subject}' not found in project ${projectId}`);
return { workItemId: wi.id, workItemName: wi.name, subject: wi.subject };
};
91 changes: 91 additions & 0 deletions test/commands/devops/pipeline/create.nut.ts
Original file line number Diff line number Diff line change
@@ -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 { GITHUB_REPO, isDevopsCenterEnabled } from '../nutHelpers.js';
import type { CreatePipelineResult } from '../../../../src/utils/createPipeline.js';

describe('devops pipeline create NUTs', () => {
let session: TestSession;
let dcEnabled = false;
let orgFlag: string;

before(async () => {
session = await TestSession.create({ devhubAuthStrategy: 'AUTO' });
orgFlag = `--target-org ${session.hubOrg?.username ?? ''}`;

dcEnabled = isDevopsCenterEnabled(orgFlag);
});

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 ────────────────────────────────────────────────────────

it('creates a pipeline and returns structured JSON', function () {
if (!dcEnabled) this.skip();

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');
});

it('new pipeline starts in Inactive status', function () {
if (!dcEnabled) this.skip();

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 }
);
const pipelineId = result.jsonOutput!.result.pipelineId!;
// The create response `status` is the operation result ('SUCCESS'); a new pipeline's
// inactive state is exposed via `pipeline get` → isActive: false.
const get = execCmd<{ isActive: boolean }>(`devops pipeline get --pipeline-id ${pipelineId} --json ${orgFlag}`, {
ensureExitCode: 0,
});
expect(get.jsonOutput?.result.isActive).to.equal(false);
});
});
96 changes: 96 additions & 0 deletions test/commands/devops/pipeline/get.nut.ts
Original file line number Diff line number Diff line change
@@ -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 { GITHUB_REPO, isDevopsCenterEnabled } from '../nutHelpers.js';
import type { PipelineGetResult } from '../../../../src/utils/getPipeline.js';

describe('devops pipeline get NUTs', () => {
let session: TestSession;
let dcEnabled = false;
let orgFlag: string;
let pipelineId: string;

before(async () => {
session = await TestSession.create({ devhubAuthStrategy: 'AUTO' });
orgFlag = `--target-org ${session.hubOrg?.username ?? ''}`;

dcEnabled = isDevopsCenterEnabled(orgFlag);

if (dcEnabled) {
try {
const name = genUniqueString('NUT-get-%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!;
} catch {
// Fixture setup needs VCS authentication / DevOps Center data that the
// target org may not have; skip the real-org tests instead of failing
// the whole suite (which would also drop the flag-validation tests).
dcEnabled = false;
}
}
});

after(async () => {
await session?.clean();
});

// ── flag-validation tests ─────────────────────────────────────────────────

it('displays help text', () => {
const result = execCmd('devops pipeline get --help', { ensureExitCode: 0 });
expect(result.shellOutput.stdout).to.include('Get details of a DevOps Center pipeline');
});

it('errors when --pipeline-id is an invalid Salesforce ID format', () => {
const result = execCmd('devops pipeline get --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 get --pipeline-id 0XB000000000001AAA', { ensureExitCode: 1 });
expect(result.shellOutput.stderr).to.include('target-org');
});

// ── real-org tests ────────────────────────────────────────────────────────

it('returns structured JSON for an existing pipeline', function () {
if (!dcEnabled) this.skip();

const result = execCmd<PipelineGetResult>(`devops pipeline get --pipeline-id ${pipelineId} --json ${orgFlag}`, {
ensureExitCode: 0,
});
const output = result.jsonOutput;
expect(output?.status).to.equal(0);
// create returns a 15-char id; get returns the 18-char form — compare on the 15-char prefix
expect(output?.result.id?.slice(0, 15)).to.equal(pipelineId);
expect(output?.result.name).to.be.a('string');
expect(output?.result.isActive).to.equal(false);
expect(output?.result.stages).to.be.an('array');
expect(output?.result.connectedProjects).to.be.an('array');
});

it('errors when the pipeline does not exist', function () {
if (!dcEnabled) this.skip();

const result = execCmd(`devops pipeline get --pipeline-id 0XB000000000001AAA ${orgFlag}`, { ensureExitCode: 1 });
expect(result.shellOutput.stderr).to.include('not found');
});
});
86 changes: 86 additions & 0 deletions test/commands/devops/pipeline/list.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 { GITHUB_REPO, isDevopsCenterEnabled } from '../nutHelpers.js';
import type { PipelineListResult } from '../../../../src/utils/listPipelines.js';

describe('devops pipeline list NUTs', () => {
let session: TestSession;
let dcEnabled = false;
let orgFlag: string;
let pipelineId: string;

before(async () => {
session = await TestSession.create({ devhubAuthStrategy: 'AUTO' });
orgFlag = `--target-org ${session.hubOrg?.username ?? ''}`;

dcEnabled = isDevopsCenterEnabled(orgFlag);

if (dcEnabled) {
try {
const name = genUniqueString('NUT-list-%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!;
} catch {
// Fixture setup needs VCS authentication / DevOps Center data that the
// target org may not have; skip the real-org tests instead of failing
// the whole suite (which would also drop the flag-validation tests).
dcEnabled = false;
}
}
});

after(async () => {
await session?.clean();
});

// ── flag-validation tests ─────────────────────────────────────────────────

it('displays help text', () => {
const result = execCmd('devops pipeline list --help', { ensureExitCode: 0 });
expect(result.shellOutput.stdout).to.include('List DevOps Center pipelines');
});

it('errors when --target-org is missing', () => {
const result = execCmd('devops pipeline list', { ensureExitCode: 1 });
expect(result.shellOutput.stderr).to.include('target-org');
});

// ── real-org tests ────────────────────────────────────────────────────────

it('returns JSON with a pipelines array', function () {
if (!dcEnabled) this.skip();

const result = execCmd<PipelineListResult>(`devops pipeline list --json ${orgFlag}`, { ensureExitCode: 0 });
const output = result.jsonOutput;
expect(output?.status).to.equal(0);
expect(output?.result.pipelines).to.be.an('array');
});

it('includes the seeded pipeline', function () {
if (!dcEnabled) this.skip();

const result = execCmd<PipelineListResult>(`devops pipeline list --json ${orgFlag}`, { ensureExitCode: 0 });
// list returns 18-char ids; the fixture holds the 15-char form — compare on the 15-char prefix
const ids = (result.jsonOutput?.result.pipelines ?? []).map((p) => p.Id?.slice(0, 15));
expect(ids).to.include(pipelineId);
});
});
Loading
Loading