From 93b22183c24bbf7f99d5729eebf0a1e49457d87f Mon Sep 17 00:00:00 2001 From: Eric Black Date: Wed, 7 Jan 2026 10:35:26 -0800 Subject: [PATCH 01/14] test: migrate ci:config:get to @oclif/test v4 Convert from chained test API to async/await with runCommand(). --- .../unit/commands/ci/config/get.unit.test.ts | 38 +++++++++++++++++++ .../commands/ci/config/get.unit.test.ts.skip | 36 ------------------ 2 files changed, 38 insertions(+), 36 deletions(-) create mode 100644 packages/cli/test/unit/commands/ci/config/get.unit.test.ts delete mode 100644 packages/cli/test/unit/commands/ci/config/get.unit.test.ts.skip diff --git a/packages/cli/test/unit/commands/ci/config/get.unit.test.ts b/packages/cli/test/unit/commands/ci/config/get.unit.test.ts new file mode 100644 index 0000000000..f509e65800 --- /dev/null +++ b/packages/cli/test/unit/commands/ci/config/get.unit.test.ts @@ -0,0 +1,38 @@ +import {runCommand} from '@oclif/test' +import {expect} from 'chai' +import nock from 'nock' + +const key = 'FOO' +const value = 'bar' +const pipeline = { + id: '123e4567-e89b-12d3-a456-426655440000', + name: 'test-pipeline', +} + +describe('heroku ci:config:get', function () { + afterEach(() => nock.cleanAll()) + + it('displays the config value', async () => { + nock('https://api.heroku.com') + .get(`/pipelines/${pipeline.id}`) + .reply(200, pipeline) + .get(`/pipelines/${pipeline.id}/stage/test/config-vars`) + .reply(200, {[key]: value}) + + const {stdout} = await runCommand(['ci:config:get', `--pipeline=${pipeline.id}`, key]) + + expect(stdout).to.equal(`${value}\n`) + }) + + it('displays config formatted for shell', async () => { + nock('https://api.heroku.com') + .get(`/pipelines/${pipeline.id}`) + .reply(200, pipeline) + .get(`/pipelines/${pipeline.id}/stage/test/config-vars`) + .reply(200, {[key]: value}) + + const {stdout} = await runCommand(['ci:config:get', `--pipeline=${pipeline.id}`, '--shell', key]) + + expect(stdout).to.equal(`${key}=${value}\n`) + }) +}) diff --git a/packages/cli/test/unit/commands/ci/config/get.unit.test.ts.skip b/packages/cli/test/unit/commands/ci/config/get.unit.test.ts.skip deleted file mode 100644 index e388fa10b2..0000000000 --- a/packages/cli/test/unit/commands/ci/config/get.unit.test.ts.skip +++ /dev/null @@ -1,36 +0,0 @@ -import {expect, test} from '@oclif/test' - -const key = 'FOO' -const value = 'bar' -const pipeline = { - id: '123e4567-e89b-12d3-a456-426655440000', - name: 'test-pipeline', -} - -describe('heroku ci:config:get', function () { - test - .stdout() - .nock('https://api.heroku.com', api => { - api.get(`/pipelines/${pipeline.id}`) - .reply(200, pipeline) - .get(`/pipelines/${pipeline.id}/stage/test/config-vars`) - .reply(200, {[key]: value}) - }) - .command(['ci:config:get', `--pipeline=${pipeline.id}`, key]) - .it('displays the config value', ({stdout}) => { - expect(stdout).to.equal(`${value}\n`) - }) - - test - .stdout() - .nock('https://api.heroku.com', api => { - api.get(`/pipelines/${pipeline.id}`) - .reply(200, pipeline) - .get(`/pipelines/${pipeline.id}/stage/test/config-vars`) - .reply(200, {[key]: value}) - }) - .command(['ci:config:get', `--pipeline=${pipeline.id}`, '--shell', key]) - .it('displays config formatted for shell', ({stdout}) => { - expect(stdout).to.equal(`${key}=${value}\n`) - }) -}) From 8cc39bd20f22ff44a62ca937a01c9b9045f9b871 Mon Sep 17 00:00:00 2001 From: Eric Black Date: Wed, 7 Jan 2026 10:37:18 -0800 Subject: [PATCH 02/14] test: migrate ci:config to @oclif/test v4 Convert from chained test API to async/await with runCommand(). --- .../commands/ci/config/index.unit.test.ts | 71 ++++++++++++++++++ .../ci/config/index.unit.test.ts.skip | 75 ------------------- 2 files changed, 71 insertions(+), 75 deletions(-) create mode 100644 packages/cli/test/unit/commands/ci/config/index.unit.test.ts delete mode 100644 packages/cli/test/unit/commands/ci/config/index.unit.test.ts.skip diff --git a/packages/cli/test/unit/commands/ci/config/index.unit.test.ts b/packages/cli/test/unit/commands/ci/config/index.unit.test.ts new file mode 100644 index 0000000000..c4831bc6b0 --- /dev/null +++ b/packages/cli/test/unit/commands/ci/config/index.unit.test.ts @@ -0,0 +1,71 @@ +import {runCommand} from '@oclif/test' +import {expect} from 'chai' +import nock from 'nock' + +describe('ci:config', function () { + const pipeline = {id: '14402644-c207-43aa-9bc1-974a34914010', name: 'my-pipeline'} + const config = { + KEY1: 'VALUE1', + OTHER: 'test', + RAILS_ENV: 'test', + } + + afterEach(() => nock.cleanAll()) + + it('errors when not specifying a pipeline or an app', async () => { + const {error} = await runCommand(['ci:config']) + expect(error?.message).to.contain('Exactly one of the following must be provided: --app, --pipeline') + }) + + it('displays config when a pipeline is specified', async () => { + nock('https://api.heroku.com') + .get(`/pipelines?eq[name]=${pipeline.name}`) + .reply(200, [ + { + id: pipeline.id, + name: pipeline.name, + }, + ]) + .get(`/pipelines/${pipeline.id}/stage/test/config-vars`) + .reply(200, config) + + const {stdout} = await runCommand(['ci:config', `--pipeline=${pipeline.name}`]) + + expect(stdout).to.include('=== my-pipeline test config vars') + expect(stdout).to.include('KEY1: VALUE1\nOTHER: test\nRAILS_ENV: test\n') + }) + + it('displays config formatted as JSON', async () => { + nock('https://api.heroku.com') + .get(`/pipelines?eq[name]=${pipeline.name}`) + .reply(200, [ + { + id: pipeline.id, + name: pipeline.name, + }, + ]) + .get(`/pipelines/${pipeline.id}/stage/test/config-vars`) + .reply(200, config) + + const {stdout} = await runCommand(['ci:config', `--pipeline=${pipeline.name}`, '--json']) + + expect(stdout).to.equal('{\n "KEY1": "VALUE1",\n "OTHER": "test",\n "RAILS_ENV": "test"\n}\n') + }) + + it('displays config formatted for shell', async () => { + nock('https://api.heroku.com') + .get(`/pipelines?eq[name]=${pipeline.name}`) + .reply(200, [ + { + id: pipeline.id, + name: pipeline.name, + }, + ]) + .get(`/pipelines/${pipeline.id}/stage/test/config-vars`) + .reply(200, config) + + const {stdout} = await runCommand(['ci:config', `--pipeline=${pipeline.name}`, '--shell']) + + expect(stdout).to.equal('KEY1=VALUE1\nOTHER=test\nRAILS_ENV=test\n') + }) +}) diff --git a/packages/cli/test/unit/commands/ci/config/index.unit.test.ts.skip b/packages/cli/test/unit/commands/ci/config/index.unit.test.ts.skip deleted file mode 100644 index e5b912798f..0000000000 --- a/packages/cli/test/unit/commands/ci/config/index.unit.test.ts.skip +++ /dev/null @@ -1,75 +0,0 @@ -import {expect, test} from '@oclif/test' - -describe('ci:config', function () { - const pipeline = {id: '14402644-c207-43aa-9bc1-974a34914010', name: 'my-pipeline'} - const config = { - KEY1: 'VALUE1', - OTHER: 'test', - RAILS_ENV: 'test', - } - - test - .command(['ci:config']) - .catch(error => { - expect(error.message).to.contain('Exactly one of the following must be provided: --app, --pipeline') - }) - .it('errors when not specifying a pipeline or an app') - - test - .stdout() - .nock('https://api.heroku.com', api => { - api.get(`/pipelines?eq[name]=${pipeline.name}`) - .reply(200, [ - { - id: pipeline.id, - name: pipeline.name, - }, - ]) - - api.get(`/pipelines/${pipeline.id}/stage/test/config-vars`) - .reply(200, config) - }) - .command(['ci:config', `--pipeline=${pipeline.name}`]) - .it('displays config when a pipeline is specified', ({stdout}) => { - expect(stdout).to.include('=== my-pipeline test config vars') - expect(stdout).to.include('KEY1: VALUE1\nOTHER: test\nRAILS_ENV: test\n') - }) - - test - .stdout() - .nock('https://api.heroku.com', api => { - api.get(`/pipelines?eq[name]=${pipeline.name}`) - .reply(200, [ - { - id: pipeline.id, - name: pipeline.name, - }, - ]) - - api.get(`/pipelines/${pipeline.id}/stage/test/config-vars`) - .reply(200, config) - }) - .command(['ci:config', `--pipeline=${pipeline.name}`, '--json']) - .it('displays config formatted as JSON', ({stdout}) => { - expect(stdout).to.equal('{\n "KEY1": "VALUE1",\n "OTHER": "test",\n "RAILS_ENV": "test"\n}\n') - }) - - test - .stdout() - .nock('https://api.heroku.com', api => { - api.get(`/pipelines?eq[name]=${pipeline.name}`) - .reply(200, [ - { - id: pipeline.id, - name: pipeline.name, - }, - ]) - - api.get(`/pipelines/${pipeline.id}/stage/test/config-vars`) - .reply(200, config) - }) - .command(['ci:config', `--pipeline=${pipeline.name}`, '--shell']) - .it('displays config formatted for shell', ({stdout}) => { - expect(stdout).to.equal('KEY1=VALUE1\nOTHER=test\nRAILS_ENV=test\n') - }) -}) From 1c64aac7e52f85ab580587660f8e11f08d16c0a4 Mon Sep 17 00:00:00 2001 From: Eric Black Date: Wed, 7 Jan 2026 10:39:08 -0800 Subject: [PATCH 03/14] test: migrate ci:config:set to @oclif/test v4 Convert from chained test API to async/await with runCommand(). --- .../unit/commands/ci/config/set.unit.test.ts | 37 +++++++++++++++++ .../commands/ci/config/set.unit.test.ts.skip | 40 ------------------- 2 files changed, 37 insertions(+), 40 deletions(-) create mode 100644 packages/cli/test/unit/commands/ci/config/set.unit.test.ts delete mode 100644 packages/cli/test/unit/commands/ci/config/set.unit.test.ts.skip diff --git a/packages/cli/test/unit/commands/ci/config/set.unit.test.ts b/packages/cli/test/unit/commands/ci/config/set.unit.test.ts new file mode 100644 index 0000000000..6f63f3e7de --- /dev/null +++ b/packages/cli/test/unit/commands/ci/config/set.unit.test.ts @@ -0,0 +1,37 @@ +import {runCommand} from '@oclif/test' +import {expect} from 'chai' +import nock from 'nock' + +const key = 'FOO' +const value = 'bar' +const pipeline = { + id: '123e4567-e89b-12d3-a456-426655440000', + name: 'test-pipeline', +} + +describe('heroku ci:config:set', function () { + afterEach(() => nock.cleanAll()) + + it('sets new config', async () => { + nock('https://api.heroku.com') + .get(`/pipelines/${pipeline.id}`) + .reply(200, pipeline) + .patch(`/pipelines/${pipeline.id}/stage/test/config-vars`) + .reply(200, {[key]: value}) + + const {stdout} = await runCommand(['ci:config:set', `--pipeline=${pipeline.id}`, '--', `${key}=${value}`]) + + expect(stdout).to.include(key) + expect(stdout).to.include(value) + }) + + it('errors with example of valid args', async () => { + const {error} = await runCommand(['ci:config:set', `--pipeline=${pipeline.id}`]) + expect(error?.message).to.equal('Usage: heroku ci:config:set KEY1 [KEY2 ...]\nMust specify KEY to set.') + }) + + it('errors with explanation of required flags', async () => { + const {error} = await runCommand(['ci:config:set', '--', `${key}=${value}`]) + expect(error?.message).to.include('Exactly one of the following must be provided: --app, --pipeline') + }) +}) diff --git a/packages/cli/test/unit/commands/ci/config/set.unit.test.ts.skip b/packages/cli/test/unit/commands/ci/config/set.unit.test.ts.skip deleted file mode 100644 index c863827189..0000000000 --- a/packages/cli/test/unit/commands/ci/config/set.unit.test.ts.skip +++ /dev/null @@ -1,40 +0,0 @@ -import {expect, test} from '@oclif/test' - -const key = 'FOO' -const value = 'bar' -const pipeline = { - id: '123e4567-e89b-12d3-a456-426655440000', - name: 'test-pipeline', -} - -describe('heroku ci:config:set', function () { - test - .stdout() - .nock('https://api.heroku.com', api => { - api.get(`/pipelines/${pipeline.id}`) - .reply(200, pipeline) - .patch(`/pipelines/${pipeline.id}/stage/test/config-vars`) - .reply(200, {[key]: value}) - }) - .command(['ci:config:set', `--pipeline=${pipeline.id}`, '--', `${key}=${value}`]) - .it('sets new config', ({stdout}) => { - expect(stdout).to.include(key) - expect(stdout).to.include(value) - }) - - test - .stderr() - .command(['ci:config:set', `--pipeline=${pipeline.id}`]) - .catch(error => { - expect(error.message).to.equal('Usage: heroku ci:config:set KEY1 [KEY2 ...]\nMust specify KEY to set.') - }) - .it('errors with example of valid args') - - test - .stderr() - .command(['ci:config:set', '--', `${key}=${value}`]) - .catch(error => { - expect(error.message).to.include('Exactly one of the following must be provided: --app, --pipeline') - }) - .it('errors with explanation of required flags') -}) From c3e8f46ad898126e43d6c98e8f064540d95ec15e Mon Sep 17 00:00:00 2001 From: Eric Black Date: Wed, 7 Jan 2026 10:40:34 -0800 Subject: [PATCH 04/14] test: migrate ci:config:unset to @oclif/test v4 Convert from chained test API to async/await with runCommand(). --- .../commands/ci/config/unset.unit.test.ts | 30 +++++++++++++++++++ .../ci/config/unset.unit.test.ts.skip | 30 ------------------- 2 files changed, 30 insertions(+), 30 deletions(-) create mode 100644 packages/cli/test/unit/commands/ci/config/unset.unit.test.ts delete mode 100644 packages/cli/test/unit/commands/ci/config/unset.unit.test.ts.skip diff --git a/packages/cli/test/unit/commands/ci/config/unset.unit.test.ts b/packages/cli/test/unit/commands/ci/config/unset.unit.test.ts new file mode 100644 index 0000000000..aaa17d6e54 --- /dev/null +++ b/packages/cli/test/unit/commands/ci/config/unset.unit.test.ts @@ -0,0 +1,30 @@ +import {runCommand} from '@oclif/test' +import {expect} from 'chai' +import nock from 'nock' + +const key = 'FOO' +const pipeline = { + id: '123e4567-e89b-12d3-a456-426655440000', + name: 'test-pipeline', +} + +describe('heroku ci:config:unset', function () { + afterEach(() => nock.cleanAll()) + + it('displays the config value key being unset', async () => { + nock('https://api.heroku.com') + .get(`/pipelines/${pipeline.id}`) + .reply(200, pipeline) + .patch(`/pipelines/${pipeline.id}/stage/test/config-vars`) + .reply(200, {[key]: null}) + + const {stderr} = await runCommand(['ci:config:unset', `--pipeline=${pipeline.id}`, key]) + + expect(stderr).to.contain('Unsetting FOO... done') + }) + + it('errors with example of valid args', async () => { + const {error} = await runCommand(['ci:config:unset', `--pipeline=${pipeline.id}`]) + expect(error?.message).to.equal('Usage: heroku ci:config:unset KEY1 [KEY2 ...]\nMust specify KEY to unset.') + }) +}) diff --git a/packages/cli/test/unit/commands/ci/config/unset.unit.test.ts.skip b/packages/cli/test/unit/commands/ci/config/unset.unit.test.ts.skip deleted file mode 100644 index ddbfc2ebdb..0000000000 --- a/packages/cli/test/unit/commands/ci/config/unset.unit.test.ts.skip +++ /dev/null @@ -1,30 +0,0 @@ -import {expect, test} from '@oclif/test' - -const key = 'FOO' -const pipeline = { - id: '123e4567-e89b-12d3-a456-426655440000', - name: 'test-pipeline', -} - -describe('heroku ci:config:unset', function () { - test - .stderr() - .nock('https://api.heroku.com', api => { - api.get(`/pipelines/${pipeline.id}`) - .reply(200, pipeline) - .patch(`/pipelines/${pipeline.id}/stage/test/config-vars`) - .reply(200, {[key]: null}) - }) - .command(['ci:config:unset', `--pipeline=${pipeline.id}`, key]) - .it('displays the config value key being unset', ({stderr}) => { - expect(stderr).to.contain('Unsetting FOO... done') - }) - - test - .stderr() - .command(['ci:config:unset', `--pipeline=${pipeline.id}`]) - .catch(error => { - expect(error.message).to.equal('Usage: heroku ci:config:unset KEY1 [KEY2 ...]\nMust specify KEY to unset.') - }) - .it('errors with example of valid args') -}) From c019743c589ea8cd0d24fa25037d5b124cea906d Mon Sep 17 00:00:00 2001 From: Eric Black Date: Wed, 7 Jan 2026 10:45:23 -0800 Subject: [PATCH 05/14] test: migrate ci to @oclif/test v4 Convert from chained test API to async/await with runCommand(). Use custom runCommand helper for test with sinon stub. --- .../test/unit/commands/ci/index.unit.test.ts | 170 +++++++++++++++++ .../unit/commands/ci/index.unit.test.ts.skip | 175 ------------------ 2 files changed, 170 insertions(+), 175 deletions(-) create mode 100644 packages/cli/test/unit/commands/ci/index.unit.test.ts delete mode 100644 packages/cli/test/unit/commands/ci/index.unit.test.ts.skip diff --git a/packages/cli/test/unit/commands/ci/index.unit.test.ts b/packages/cli/test/unit/commands/ci/index.unit.test.ts new file mode 100644 index 0000000000..4314292442 --- /dev/null +++ b/packages/cli/test/unit/commands/ci/index.unit.test.ts @@ -0,0 +1,170 @@ +import {runCommand} from '@oclif/test' +import {expect} from 'chai' +import nock from 'nock' +import sinon from 'sinon' +import {PipelineService} from '../../../../src/lib/ci/pipelines.js' +import removeAllWhitespace from '../../../helpers/utils/remove-whitespaces.js' +import customRunCommand from '../../../helpers/runCommand.js' +import Cmd from '../../../../src/commands/ci/index.js' + +describe('ci', function () { + afterEach(() => nock.cleanAll()) + + it('errors when not specifying a pipeline or an app', async () => { + const {error} = await runCommand(['ci']) + expect(error?.message).to.contain('Required flag: --pipeline PIPELINE or --app APP') + }) + + describe('when specifying a pipeline', function () { + const pipeline = {id: '14402644-c207-43aa-9bc1-974a34914010', name: 'my-pipeline'} + + let testRuns: any = [] + const statusIcon = ['✓', '!', '✗', '-', '!', '?', '-'] + const statuses = ['succeeded', 'errored', 'failed', 'creating', 'cancelled', 'foo', ''] + const commit_branch = 'main' + const commit_sha = ['d2e177a', '14a0a11', '40d9717', 'f2e574e'] + let promptStub: sinon.SinonStub + + const chosenOption = { + pipeline: { + id: '14402644-c207-43aa-9bc1-974a34914010', + name: '14402644-c207-43aa-9bc1-974a34914010', + created_at: '05/10/2023', + }, + } + + beforeEach(function () { + testRuns = [] + for (let i = 0; i < 20; i++) { + testRuns.push({ + commit_branch, + commit_sha: commit_sha[i % 4], + number: i, + pipeline: {id: pipeline.id}, + status: statuses[i % 7], + }) + } + }) + + it('shows the latest 15 test runs', async () => { + nock('https://api.heroku.com') + .get(`/pipelines?eq[name]=${pipeline.name}`) + .reply(200, [ + { + id: pipeline.id, + name: pipeline.name, + }, + ]) + .get(`/pipelines/${pipeline.id}/test-runs`) + .reply(200, testRuns) + + const {stdout} = await runCommand(['ci', `--pipeline=${pipeline.name}`]) + + expect(stdout).to.contain(`=== Showing latest test runs for the ${pipeline.name} pipeline`) + + const actual = removeAllWhitespace(stdout) + let expected: string + for (let i = 7; i < 10; i++) { + expected = removeAllWhitespace(`${statusIcon[i % 7]} ${testRuns[i].number} main ${testRuns[i].commit_sha} ${testRuns[i].status} `) + expect(actual).to.contain(expected) + } + + for (let i = 10; i < 20; i++) { + expected = removeAllWhitespace(`${statusIcon[i % 7]} ${testRuns[i].number} main ${testRuns[i].commit_sha} ${testRuns[i].status} `) + expect(actual).to.contain(expected) + } + + expect(actual).not.to.contain(removeAllWhitespace(`${testRuns[4].number} ${testRuns[4].commit_sha}`)) + }) + + it('returns pipeline id', async () => { + nock('https://api.heroku.com') + .get(`/pipelines/${pipeline.id}`) + .reply(200, + { + id: pipeline.id, + name: pipeline.id, + }, + ) + .get(`/pipelines/${pipeline.id}/test-runs`) + .reply(200, testRuns) + + const {stdout} = await runCommand(['ci', `--pipeline=${pipeline.id}`]) + + expect(stdout).to.contain(`=== Showing latest test runs for the ${pipeline.id} pipeline`) + }) + + it('errors if no pipeline is found', async () => { + nock('https://api.heroku.com') + .get(`/pipelines?eq[name]=${pipeline.name}`) + .reply(200, []) + + const {error} = await runCommand(['ci', `--pipeline=${pipeline.name}`]) + + expect(error?.message).to.equal('Pipeline not found') + }) + + describe('specifying a pipeline with prompt', function () { + beforeEach(function () { + promptStub = sinon.stub(PipelineService.prototype, 'promptForPipeline') + promptStub.onFirstCall().resolves(chosenOption) + }) + + afterEach(function () { + promptStub.restore() + }) + + it('selects a pipeline from the prompt', async () => { + nock('https://api.heroku.com') + .get(`/pipelines?eq[name]=${pipeline.name}`) + .reply(200, [ + { + id: pipeline.id, + name: pipeline.id, + created_at: '05/10/2023', + }, + { + id: pipeline.id, + name: pipeline.id, + created_at: '05/11/2023', + }, + { + id: pipeline.id, + name: pipeline.id, + created_at: '05/12/2023', + }, + ]) + .get(`/pipelines/${pipeline.id}/test-runs`) + .reply(200, testRuns) + + await customRunCommand(Cmd, [`--pipeline=${pipeline.name}`]) + + expect(promptStub.calledOnce).to.equal(true) + }) + }) + + it('shows the latest 15 test runs in json', async () => { + nock('https://api.heroku.com') + .get(`/pipelines?eq[name]=${pipeline.name}`) + .reply(200, [ + { + id: pipeline.id, + name: pipeline.name, + }, + ]) + .get(`/pipelines/${pipeline.id}/test-runs`) + .reply(200, testRuns) + + const {stdout} = await runCommand(['ci', '--json', `--pipeline=${pipeline.name}`]) + + expect(stdout).not.to.contain(`=== Showing latest test runs for the ${pipeline.name} pipeline`) + const jsonOut = JSON.parse(stdout) + for (let i = 0; i < 4; i++) { + expect(jsonOut[i].commit_branch).to.equal('main') + expect(jsonOut[i].commit_sha).to.equal(commit_sha[3 - i]) + expect(jsonOut[i].status).to.equal(statuses[5 - i]) + expect(jsonOut[i].pipeline.id).to.equal(pipeline.id) + } + }) + }) +}) diff --git a/packages/cli/test/unit/commands/ci/index.unit.test.ts.skip b/packages/cli/test/unit/commands/ci/index.unit.test.ts.skip deleted file mode 100644 index 990a5d8d6c..0000000000 --- a/packages/cli/test/unit/commands/ci/index.unit.test.ts.skip +++ /dev/null @@ -1,175 +0,0 @@ -import {expect, test} from '@oclif/test' -import sinon from 'sinon' -import {PipelineService} from '../../../../src/lib/ci/pipelines.js' -import removeAllWhitespace from '../../../helpers/utils/remove-whitespaces.js' - -describe('ci', function () { - test - .command(['ci']) - .catch(error => { - expect(error.message).to.contain('Required flag: --pipeline PIPELINE or --app APP') - }) - .it('errors when not specifying a pipeline or an app') - - describe('when specifying a pipeline', function () { - const pipeline = {id: '14402644-c207-43aa-9bc1-974a34914010', name: 'my-pipeline'} - - let testRuns: any = [] - const statusIcon = ['✓', '!', '✗', '-', '!', '?', '-'] - const statuses = ['succeeded', 'errored', 'failed', 'creating', 'cancelled', 'foo', ''] - const commit_branch = 'main' - const commit_sha = ['d2e177a', '14a0a11', '40d9717', 'f2e574e'] - let promptStub: sinon.SinonStub - - const chosenOption = { - pipeline: { - id: '14402644-c207-43aa-9bc1-974a34914010', - name: '14402644-c207-43aa-9bc1-974a34914010', - created_at: '05/10/2023', - }, - } - - beforeEach(function () { - testRuns = [] - for (let i = 0; i < 20; i++) { - testRuns.push({ - commit_branch, - commit_sha: commit_sha[i % 4], - number: i, - pipeline: {id: pipeline.id}, - status: statuses[i % 7], - }) - } - }) - - test - .stdout() - .nock('https://api.heroku.com', api => { - api.get(`/pipelines?eq[name]=${pipeline.name}`) - .reply(200, [ - { - id: pipeline.id, - name: pipeline.name, - }, - ]) - - api.get(`/pipelines/${pipeline.id}/test-runs`) - .reply(200, testRuns) - }) - .command(['ci', `--pipeline=${pipeline.name}`]) - .it('shows the latest 15 test runs', ({stdout}) => { - expect(stdout).to.contain(`=== Showing latest test runs for the ${pipeline.name} pipeline`) - - const actual = removeAllWhitespace(stdout) - let expected: string - for (let i = 7; i < 10; i++) { - expected = removeAllWhitespace(`${statusIcon[i % 7]} ${testRuns[i].number} main ${testRuns[i].commit_sha} ${testRuns[i].status} `) - expect(actual).to.contain(expected) - } - - for (let i = 10; i < 20; i++) { - expected = removeAllWhitespace(`${statusIcon[i % 7]} ${testRuns[i].number} main ${testRuns[i].commit_sha} ${testRuns[i].status} `) - expect(actual).to.contain(expected) - } - - expect(actual).not.to.contain(removeAllWhitespace(`${testRuns[4].number} ${testRuns[4].commit_sha}`)) - }) - - test - .stdout() - .nock('https://api.heroku.com', api => { - api.get(`/pipelines/${pipeline.id}`) - .reply(200, - { - id: pipeline.id, - name: pipeline.id, - }, - ) - - api.get(`/pipelines/${pipeline.id}/test-runs`) - .reply(200, testRuns) - }) - .command(['ci', `--pipeline=${pipeline.id}`]) - .it('returns pipeline id', ({stdout}) => { - expect(stdout).to.contain(`=== Showing latest test runs for the ${pipeline.id} pipeline`) - }) - - test - .stdout() - .nock('https://api.heroku.com', api => { - api.get(`/pipelines?eq[name]=${pipeline.name}`) - .reply(200, []) - }) - .command(['ci', `--pipeline=${pipeline.name}`]) - .catch(error => { - expect(error.message).to.equal('Pipeline not found') - }) - .it('errors if no pipeline is found') - - describe('specifying a pipeline with prompt', function () { - before(function () { - promptStub = sinon.stub(PipelineService.prototype, 'promptForPipeline') - promptStub.onFirstCall().resolves(chosenOption) - }) - - test - .stdout() - .nock('https://api.heroku.com', api => { - api.get(`/pipelines?eq[name]=${pipeline.name}`) - .reply(200, [ - { - id: pipeline.id, - name: pipeline.id, - created_at: '05/10/2023', - }, - { - id: pipeline.id, - name: pipeline.id, - created_at: '05/11/2023', - }, - { - id: pipeline.id, - name: pipeline.id, - created_at: '05/12/2023', - }, - ]) - - api.get(`/pipelines/${pipeline.id}/test-runs`) - .reply(200, testRuns) - }) - .command(['ci', `--pipeline=${pipeline.name}`]) - .it('selects a pipeline from the prompt', () => { - expect(promptStub.calledOnce).to.equal(true) - }) - - after(function () { - promptStub.restore() - }) - }) - - test - .stdout() - .nock('https://api.heroku.com', api => { - api.get(`/pipelines?eq[name]=${pipeline.name}`) - .reply(200, [ - { - id: pipeline.id, - name: pipeline.name, - }, - ]) - api.get(`/pipelines/${pipeline.id}/test-runs`) - .reply(200, testRuns) - }) - .command(['ci', '--json', `--pipeline=${pipeline.name}`]) - .it('shows the latest 15 test runs in json', ({stdout}) => { - expect(stdout).not.to.contain(`=== Showing latest test runs for the ${pipeline.name} pipeline`) - const jsonOut = JSON.parse(stdout) - for (let i = 0; i < 4; i++) { - expect(jsonOut[i].commit_branch).to.equal('main') - expect(jsonOut[i].commit_sha).to.equal(commit_sha[3 - i]) - expect(jsonOut[i].status).to.equal(statuses[5 - i]) - expect(jsonOut[i].pipeline.id).to.equal(pipeline.id) - } - }) - }) -}) From 8f9ac279b9ace3b2fbe7d3ea6594d71d348a82d9 Mon Sep 17 00:00:00 2001 From: Eric Black Date: Wed, 7 Jan 2026 10:47:00 -0800 Subject: [PATCH 06/14] test: migrate ci:info to @oclif/test v4 Convert from chained test API to async/await with runCommand(). Handle exit code testing with error.oclif.exit property. --- .../test/unit/commands/ci/info.unit.test.ts | 286 +++++++++++++++++ .../unit/commands/ci/info.unit.test.ts.skip | 301 ------------------ 2 files changed, 286 insertions(+), 301 deletions(-) create mode 100644 packages/cli/test/unit/commands/ci/info.unit.test.ts delete mode 100644 packages/cli/test/unit/commands/ci/info.unit.test.ts.skip diff --git a/packages/cli/test/unit/commands/ci/info.unit.test.ts b/packages/cli/test/unit/commands/ci/info.unit.test.ts new file mode 100644 index 0000000000..668ed3f744 --- /dev/null +++ b/packages/cli/test/unit/commands/ci/info.unit.test.ts @@ -0,0 +1,286 @@ +import {runCommand} from '@oclif/test' +import {expect} from 'chai' +import nock from 'nock' + +describe('ci:info', function () { + const testRunNumber = 10 + const testRun = {id: 'f53d34b4-c3a9-4608-a186-17257cf71d62', number: 10} + + afterEach(() => nock.cleanAll()) + + it('errors when not specifying a test run', async () => { + const {error} = await runCommand(['ci:info']) + expect(error?.message).to.equal('Missing 1 required arg:\ntest-run auto-incremented test run number\nSee more help with --help') + }) + + it('errors when not specifying a pipeline or an app', async () => { + const {error} = await runCommand(['ci:info', `${testRun.number}`]) + expect(error?.message).to.contain('Required flag: --pipeline PIPELINE or --app APP') + }) + + describe('when specifying a pipeline', function () { + const pipeline = {id: '14402644-c207-43aa-9bc1-974a34914010', name: 'pipeline'} + + it('it shows the setup, test, and final result output', async () => { + nock('https://api.heroku.com') + .get(`/pipelines?eq[name]=${pipeline.name}`) + .reply(200, [ + {id: pipeline.id}, + ]) + .get(`/pipelines/${pipeline.id}/test-runs/${testRunNumber}`) + .reply(200, + { + commit_branch: 'main', + commit_message: 'Merge pull request #5848 from heroku/cli', + commit_sha: 'b9e982a60904730510a1c9e2dd2df64aef6f0d84', + id: testRun.id, + number: testRun.number, + pipeline: {id: pipeline.id}, + status: 'succeeded', + }, + ) + .get(`/test-runs/${testRun.id}/test-nodes`) + .reply(200, [ + { + commit_branch: 'main', + commit_message: 'Merge pull request #5848 from heroku/cli', + commit_sha: 'b9e982a60904730510a1c9e2dd2df64aef6f0d84', + id: testRun.id, + number: testRun.number, + pipeline: {id: pipeline.id}, + exit_code: 0, + status: 'succeeded', + setup_stream_url: `https://test-setup-output.heroku.com/streams/${testRun.id.slice(0, 3)}/test-runs/${testRun.id}`, + output_stream_url: `https://test-output.heroku.com/streams/${testRun.id.slice(0, 3)}/test-runs/${testRun.id}`, + }, + ]) + + nock('https://test-setup-output.heroku.com/streams') + .get(`/${testRun.id.slice(0, 3)}/test-runs/${testRun.id}`) + .reply(200, 'Test setup output') + + nock('https://test-output.heroku.com/streams') + .get(`/${testRun.id.slice(0, 3)}/test-runs/${testRun.id}`) + .reply(200, 'Test output') + + const {stdout} = await runCommand(['ci:info', `${testRun.number}`, `--pipeline=${pipeline.name}`]) + + expect(stdout).to.equal('Test setup outputTest output\n✓ #10 main:b9e982a succeeded\n') + }) + + describe('and the exit was not successful', function () { + const testRunExitCode = 34 + + it('it shows the setup, test, and final result output', async () => { + nock('https://api.heroku.com') + .get(`/pipelines?eq[name]=${pipeline.name}`) + .reply(200, [ + {id: pipeline.id}, + ]) + .get(`/pipelines/${pipeline.id}/test-runs/${testRunNumber}`) + .reply(200, + { + commit_branch: 'main', + commit_message: 'Merge pull request #5848 from heroku/cli', + commit_sha: 'b9e982a60904730510a1c9e2dd2df64aef6f0d84', + id: testRun.id, + number: testRun.number, + pipeline: {id: pipeline.id}, + status: 'failed', + }, + ) + .get(`/test-runs/${testRun.id}/test-nodes`) + .reply(200, [ + { + commit_branch: 'main', + commit_message: 'Merge pull request #5848 from heroku/cli', + commit_sha: 'b9e982a60904730510a1c9e2dd2df64aef6f0d84', + id: testRun.id, + number: testRun.number, + pipeline: {id: pipeline.id}, + exit_code: testRunExitCode, + status: 'succeeded', + setup_stream_url: `https://test-setup-output.heroku.com/streams/${testRun.id.slice(0, 3)}/test-runs/${testRun.id}`, + output_stream_url: `https://test-output.heroku.com/streams/${testRun.id.slice(0, 3)}/test-runs/${testRun.id}`, + }, + ]) + + nock('https://test-setup-output.heroku.com/streams') + .get(`/${testRun.id.slice(0, 3)}/test-runs/${testRun.id}`) + .reply(200, 'Test setup output') + + nock('https://test-output.heroku.com/streams') + .get(`/${testRun.id.slice(0, 3)}/test-runs/${testRun.id}`) + .reply(200, 'Test output') + + const {stdout, error} = await runCommand(['ci:info', `${testRun.number}`, `--pipeline=${pipeline.name}`]) + + expect(stdout).to.equal('Test setup outputTest output\n✗ #10 main:b9e982a failed\n') + expect(error?.oclif?.exit).to.equal(testRunExitCode) + }) + }) + + describe('when the pipeline has parallel test runs enabled', function () { + it('shows a result for each node', async () => { + nock('https://api.heroku.com') + .get(`/pipelines?eq[name]=${pipeline.name}`) + .reply(200, [ + {id: pipeline.id}, + ]) + .get(`/pipelines/${pipeline.id}/test-runs/${testRunNumber}`) + .reply(200, + { + commit_branch: 'main', + commit_message: 'Merge pull request #5848 from heroku/cli', + commit_sha: 'b9e982a60904730510a1c9e2dd2df64aef6f0d84', + id: testRun.id, + number: testRun.number, + pipeline: {id: pipeline.id}, + status: 'succeeded', + }, + ) + .get(`/test-runs/${testRun.id}/test-nodes`) + .reply(200, [ + { + commit_branch: 'main', + commit_message: 'Merge pull request #5848 from heroku/cli', + commit_sha: 'b9e982a60904730510a1c9e2dd2df64aef6f0d84', + id: testRun.id, + number: testRun.number, + pipeline: {id: pipeline.id}, + exit_code: 0, + index: 0, + status: 'succeeded', + }, + { + commit_branch: 'main', + commit_message: 'Merge pull request #5848 from heroku/cli', + commit_sha: 'b9e982a60904730510a1c9e2dd2df64aef6f0d84', + id: testRun.id, + number: testRun.number, + pipeline: {id: pipeline.id}, + exit_code: 0, + index: 1, + status: 'succeeded', + }, + ]) + + const {stdout} = await runCommand(['ci:info', `${testRun.number}`, `--pipeline=${pipeline.name}`]) + + expect(stdout).to.equal('✓ #10 main:b9e982a succeeded\n\n✓ #0 succeeded\n✓ #1 succeeded\n') + }) + + describe('and the user passes in a test node index', function () { + it('displays the setup and test output for the specified node', async () => { + nock('https://api.heroku.com') + .get(`/pipelines?eq[name]=${pipeline.name}`) + .reply(200, [ + {id: pipeline.id}, + ]) + .get(`/pipelines/${pipeline.id}/test-runs/${testRunNumber}`) + .reply(200, + { + commit_branch: 'main', + commit_message: 'Merge pull request #5848 from heroku/cli', + commit_sha: 'b9e982a60904730510a1c9e2dd2df64aef6f0d84', + id: testRun.id, + number: testRun.number, + pipeline: {id: pipeline.id}, + status: 'succeeded', + }, + ) + .get(`/test-runs/${testRun.id}/test-nodes`) + .reply(200, [ + { + commit_branch: 'main', + commit_message: 'Merge pull request #5848 from heroku/cli', + commit_sha: 'b9e982a60904730510a1c9e2dd2df64aef6f0d84', + id: testRun.id, + number: testRun.number, + pipeline: {id: pipeline.id}, + exit_code: 0, + index: 0, + status: 'succeeded', + }, + { + commit_branch: 'main', + commit_message: 'Merge pull request #5848 from heroku/cli', + commit_sha: 'b9e982a60904730510a1c9e2dd2df64aef6f0d84', + id: testRun.id, + number: testRun.number, + pipeline: {id: pipeline.id}, + exit_code: 0, + index: 1, + setup_stream_url: `https://test-setup-output.heroku.com/streams/${testRun.id.slice(0, 3)}/test-runs/${testRun.id}`, + output_stream_url: `https://test-output.heroku.com/streams/${testRun.id.slice(0, 3)}/test-runs/${testRun.id}`, + status: 'succeeded', + }, + ]) + + nock('https://test-setup-output.heroku.com/streams') + .get(`/${testRun.id.slice(0, 3)}/test-runs/${testRun.id}`) + .reply(200, 'Test setup output') + + nock('https://test-output.heroku.com/streams') + .get(`/${testRun.id.slice(0, 3)}/test-runs/${testRun.id}`) + .reply(200, 'Test output') + + const {stdout} = await runCommand(['ci:info', `${testRun.number}`, `--pipeline=${pipeline.name}`, '--node=1']) + + expect(stdout).to.equal('Test setup outputTest output\n✓ #10 main:b9e982a succeeded\n') + }) + + describe('and the pipeline does not have parallel tests enabled', function () { + it('displays the setup and test output for the first node and a warning', async () => { + nock('https://api.heroku.com') + .get(`/pipelines?eq[name]=${pipeline.name}`) + .reply(200, [ + {id: pipeline.id}, + ]) + .get(`/pipelines/${pipeline.id}/test-runs/${testRunNumber}`) + .reply(200, + { + commit_branch: 'main', + commit_message: 'Merge pull request #5848 from heroku/cli', + commit_sha: 'b9e982a60904730510a1c9e2dd2df64aef6f0d84', + id: testRun.id, + number: testRun.number, + pipeline: {id: pipeline.id}, + status: 'succeeded', + }, + ) + .get(`/test-runs/${testRun.id}/test-nodes`) + .reply(200, [ + { + commit_branch: 'main', + commit_message: 'Merge pull request #5848 from heroku/cli', + commit_sha: 'b9e982a60904730510a1c9e2dd2df64aef6f0d84', + id: testRun.id, + number: testRun.number, + pipeline: {id: pipeline.id}, + exit_code: 0, + index: 1, + setup_stream_url: `https://test-setup-output.heroku.com/streams/${testRun.id.slice(0, 3)}/test-runs/${testRun.id}`, + output_stream_url: `https://test-output.heroku.com/streams/${testRun.id.slice(0, 3)}/test-runs/${testRun.id}`, + status: 'succeeded', + }, + ]) + + nock('https://test-setup-output.heroku.com/streams') + .get(`/${testRun.id.slice(0, 3)}/test-runs/${testRun.id}`) + .reply(200, 'Test setup output') + + nock('https://test-output.heroku.com/streams') + .get(`/${testRun.id.slice(0, 3)}/test-runs/${testRun.id}`) + .reply(200, 'Test output') + + const {stdout, stderr} = await runCommand(['ci:info', `${testRun.number}`, `--pipeline=${pipeline.name}`, '--node=1']) + + expect(stdout).to.equal('Test setup outputTest output\n✓ #10 main:b9e982a succeeded\n\n') + expect(stderr).to.contain('Warning: This pipeline doesn\'t have parallel test runs') + }) + }) + }) + }) + }) +}) diff --git a/packages/cli/test/unit/commands/ci/info.unit.test.ts.skip b/packages/cli/test/unit/commands/ci/info.unit.test.ts.skip deleted file mode 100644 index 00b8195e31..0000000000 --- a/packages/cli/test/unit/commands/ci/info.unit.test.ts.skip +++ /dev/null @@ -1,301 +0,0 @@ -import {expect, test} from '@oclif/test' - -describe('ci:info', function () { - const testRunNumber = 10 - const testRun = {id: 'f53d34b4-c3a9-4608-a186-17257cf71d62', number: 10} - - test - .command(['ci:info']) - .catch(error => { - expect(error.message).to.equal('Missing 1 required arg:\ntest-run auto-incremented test run number\nSee more help with --help') - }) - .it('errors when not specifying a test run') - - test - .command(['ci:info', `${testRun.number}`]) - .catch(error => { - expect(error.message).to.contain('Required flag: --pipeline PIPELINE or --app APP') - }) - .it('errors when not specifying a pipeline or an app') - - describe('when specifying a pipeline', function () { - const pipeline = {id: '14402644-c207-43aa-9bc1-974a34914010', name: 'pipeline'} - - test - .stdout() - .nock('https://api.heroku.com', api => { - api.get(`/pipelines?eq[name]=${pipeline.name}`) - .reply(200, [ - {id: pipeline.id}, - ]) - - api.get(`/pipelines/${pipeline.id}/test-runs/${testRunNumber}`) - .reply(200, - { - commit_branch: 'main', - commit_message: 'Merge pull request #5848 from heroku/cli', - commit_sha: 'b9e982a60904730510a1c9e2dd2df64aef6f0d84', - id: testRun.id, - number: testRun.number, - pipeline: {id: pipeline.id}, - status: 'succeeded', - }, - ) - - api.get(`/test-runs/${testRun.id}/test-nodes`) - .reply(200, [ - { - commit_branch: 'main', - commit_message: 'Merge pull request #5848 from heroku/cli', - commit_sha: 'b9e982a60904730510a1c9e2dd2df64aef6f0d84', - id: testRun.id, - number: testRun.number, - pipeline: {id: pipeline.id}, - exit_code: 0, - status: 'succeeded', - setup_stream_url: `https://test-setup-output.heroku.com/streams/${testRun.id.slice(0, 3)}/test-runs/${testRun.id}`, - output_stream_url: `https://test-output.heroku.com/streams/${testRun.id.slice(0, 3)}/test-runs/${testRun.id}`, - }, - ]) - }) - .nock('https://test-setup-output.heroku.com/streams', testOutputAPI => { - testOutputAPI.get(`/${testRun.id.slice(0, 3)}/test-runs/${testRun.id}`) - .reply(200, 'Test setup output') - }) - .nock('https://test-output.heroku.com/streams', testOutputAPI => { - testOutputAPI.get(`/${testRun.id.slice(0, 3)}/test-runs/${testRun.id}`) - .reply(200, 'Test output') - }) - .command(['ci:info', `${testRun.number}`, `--pipeline=${pipeline.name}`]) - .it('it shows the setup, test, and final result output', ({stdout}) => { - expect(stdout).to.equal('Test setup outputTest output\n✓ #10 main:b9e982a succeeded\n') - }) - - describe('and the exit was not successful', function () { - const testRunExitCode = 34 - test - .stdout() - .nock('https://api.heroku.com', api => { - api.get(`/pipelines?eq[name]=${pipeline.name}`) - .reply(200, [ - {id: pipeline.id}, - ]) - - api.get(`/pipelines/${pipeline.id}/test-runs/${testRunNumber}`) - .reply(200, - { - commit_branch: 'main', - commit_message: 'Merge pull request #5848 from heroku/cli', - commit_sha: 'b9e982a60904730510a1c9e2dd2df64aef6f0d84', - id: testRun.id, - number: testRun.number, - pipeline: {id: pipeline.id}, - status: 'failed', - }, - ) - - api.get(`/test-runs/${testRun.id}/test-nodes`) - .reply(200, [ - { - commit_branch: 'main', - commit_message: 'Merge pull request #5848 from heroku/cli', - commit_sha: 'b9e982a60904730510a1c9e2dd2df64aef6f0d84', - id: testRun.id, - number: testRun.number, - pipeline: {id: pipeline.id}, - exit_code: testRunExitCode, - status: 'succeeded', - setup_stream_url: `https://test-setup-output.heroku.com/streams/${testRun.id.slice(0, 3)}/test-runs/${testRun.id}`, - output_stream_url: `https://test-output.heroku.com/streams/${testRun.id.slice(0, 3)}/test-runs/${testRun.id}`, - }, - ]) - }) - .nock('https://test-setup-output.heroku.com/streams', testOutputAPI => { - testOutputAPI.get(`/${testRun.id.slice(0, 3)}/test-runs/${testRun.id}`) - .reply(200, 'Test setup output') - }) - .nock('https://test-output.heroku.com/streams', testOutputAPI => { - testOutputAPI.get(`/${testRun.id.slice(0, 3)}/test-runs/${testRun.id}`) - .reply(200, 'Test output') - }) - .command(['ci:info', `${testRun.number}`, `--pipeline=${pipeline.name}`]) - .exit(testRunExitCode) - .it('it shows the setup, test, and final result output', ({stdout}) => { - expect(stdout).to.equal('Test setup outputTest output\n✗ #10 main:b9e982a failed\n') - }) - }) - - describe('when the pipeline has parallel test runs enabled', function () { - test - .stdout() - .nock('https://api.heroku.com', api => { - api.get(`/pipelines?eq[name]=${pipeline.name}`) - .reply(200, [ - {id: pipeline.id}, - ]) - - api.get(`/pipelines/${pipeline.id}/test-runs/${testRunNumber}`) - .reply(200, - { - commit_branch: 'main', - commit_message: 'Merge pull request #5848 from heroku/cli', - commit_sha: 'b9e982a60904730510a1c9e2dd2df64aef6f0d84', - id: testRun.id, - number: testRun.number, - pipeline: {id: pipeline.id}, - status: 'succeeded', - }, - ) - - api.get(`/test-runs/${testRun.id}/test-nodes`) - .reply(200, [ - { - commit_branch: 'main', - commit_message: 'Merge pull request #5848 from heroku/cli', - commit_sha: 'b9e982a60904730510a1c9e2dd2df64aef6f0d84', - id: testRun.id, - number: testRun.number, - pipeline: {id: pipeline.id}, - exit_code: 0, - index: 0, - status: 'succeeded', - }, - { - commit_branch: 'main', - commit_message: 'Merge pull request #5848 from heroku/cli', - commit_sha: 'b9e982a60904730510a1c9e2dd2df64aef6f0d84', - id: testRun.id, - number: testRun.number, - pipeline: {id: pipeline.id}, - exit_code: 0, - index: 1, - status: 'succeeded', - }, - ]) - }) - .command(['ci:info', `${testRun.number}`, `--pipeline=${pipeline.name}`]) - .it('shows a result for each node', ({stdout}) => { - expect(stdout).to.equal('✓ #10 main:b9e982a succeeded\n\n✓ #0 succeeded\n✓ #1 succeeded\n') - }) - - describe('and the user passes in a test node index', function () { - test - .stdout() - .nock('https://api.heroku.com', api => { - api.get(`/pipelines?eq[name]=${pipeline.name}`) - .reply(200, [ - {id: pipeline.id}, - ]) - - api.get(`/pipelines/${pipeline.id}/test-runs/${testRunNumber}`) - .reply(200, - { - commit_branch: 'main', - commit_message: 'Merge pull request #5848 from heroku/cli', - commit_sha: 'b9e982a60904730510a1c9e2dd2df64aef6f0d84', - id: testRun.id, - number: testRun.number, - pipeline: {id: pipeline.id}, - status: 'succeeded', - }, - ) - - api.get(`/test-runs/${testRun.id}/test-nodes`) - .reply(200, [ - { - commit_branch: 'main', - commit_message: 'Merge pull request #5848 from heroku/cli', - commit_sha: 'b9e982a60904730510a1c9e2dd2df64aef6f0d84', - id: testRun.id, - number: testRun.number, - pipeline: {id: pipeline.id}, - exit_code: 0, - index: 0, - status: 'succeeded', - }, - { - commit_branch: 'main', - commit_message: 'Merge pull request #5848 from heroku/cli', - commit_sha: 'b9e982a60904730510a1c9e2dd2df64aef6f0d84', - id: testRun.id, - number: testRun.number, - pipeline: {id: pipeline.id}, - exit_code: 0, - index: 1, - setup_stream_url: `https://test-setup-output.heroku.com/streams/${testRun.id.slice(0, 3)}/test-runs/${testRun.id}`, - output_stream_url: `https://test-output.heroku.com/streams/${testRun.id.slice(0, 3)}/test-runs/${testRun.id}`, - status: 'succeeded', - }, - ]) - }) - .nock('https://test-setup-output.heroku.com/streams', testOutputAPI => { - testOutputAPI.get(`/${testRun.id.slice(0, 3)}/test-runs/${testRun.id}`) - .reply(200, 'Test setup output') - }) - .nock('https://test-output.heroku.com/streams', testOutputAPI => { - testOutputAPI.get(`/${testRun.id.slice(0, 3)}/test-runs/${testRun.id}`) - .reply(200, 'Test output') - }) - .command(['ci:info', `${testRun.number}`, `--pipeline=${pipeline.name}`, '--node=1']) - .it('displays the setup and test output for the specified node', ({stdout}) => { - expect(stdout).to.equal('Test setup outputTest output\n✓ #10 main:b9e982a succeeded\n') - }) - - describe('and the pipeline does not have parallel tests enabled', function () { - test - .stdout() - .stderr() - .nock('https://api.heroku.com', api => { - api.get(`/pipelines?eq[name]=${pipeline.name}`) - .reply(200, [ - {id: pipeline.id}, - ]) - - api.get(`/pipelines/${pipeline.id}/test-runs/${testRunNumber}`) - .reply(200, - { - commit_branch: 'main', - commit_message: 'Merge pull request #5848 from heroku/cli', - commit_sha: 'b9e982a60904730510a1c9e2dd2df64aef6f0d84', - id: testRun.id, - number: testRun.number, - pipeline: {id: pipeline.id}, - status: 'succeeded', - }, - ) - - api.get(`/test-runs/${testRun.id}/test-nodes`) - .reply(200, [ - { - commit_branch: 'main', - commit_message: 'Merge pull request #5848 from heroku/cli', - commit_sha: 'b9e982a60904730510a1c9e2dd2df64aef6f0d84', - id: testRun.id, - number: testRun.number, - pipeline: {id: pipeline.id}, - exit_code: 0, - index: 1, - setup_stream_url: `https://test-setup-output.heroku.com/streams/${testRun.id.slice(0, 3)}/test-runs/${testRun.id}`, - output_stream_url: `https://test-output.heroku.com/streams/${testRun.id.slice(0, 3)}/test-runs/${testRun.id}`, - status: 'succeeded', - }, - ]) - }) - .nock('https://test-setup-output.heroku.com/streams', testOutputAPI => { - testOutputAPI.get(`/${testRun.id.slice(0, 3)}/test-runs/${testRun.id}`) - .reply(200, 'Test setup output') - }) - .nock('https://test-output.heroku.com/streams', testOutputAPI => { - testOutputAPI.get(`/${testRun.id.slice(0, 3)}/test-runs/${testRun.id}`) - .reply(200, 'Test output') - }) - .command(['ci:info', `${testRun.number}`, `--pipeline=${pipeline.name}`, '--node=1']) - .it('displays the setup and test output for the first node and a warning', ({stdout, stderr}) => { - expect(stdout).to.equal('Test setup outputTest output\n✓ #10 main:b9e982a succeeded\n\n') - expect(stderr).to.contain('Warning: This pipeline doesn\'t have parallel test runs') - }) - }) - }) - }) - }) -}) From ac4c8e5b24332a67b8e5e2b65905853fcee74d33 Mon Sep 17 00:00:00 2001 From: Eric Black Date: Wed, 7 Jan 2026 10:48:49 -0800 Subject: [PATCH 07/14] test: migrate ci:last to @oclif/test v4 Convert from chained test API to async/await with runCommand(). --- .../test/unit/commands/ci/last.unit.test.ts | 164 +++++++++++++++++ .../unit/commands/ci/last.unit.test.ts.skip | 174 ------------------ 2 files changed, 164 insertions(+), 174 deletions(-) create mode 100644 packages/cli/test/unit/commands/ci/last.unit.test.ts delete mode 100644 packages/cli/test/unit/commands/ci/last.unit.test.ts.skip diff --git a/packages/cli/test/unit/commands/ci/last.unit.test.ts b/packages/cli/test/unit/commands/ci/last.unit.test.ts new file mode 100644 index 0000000000..04ab9b28c8 --- /dev/null +++ b/packages/cli/test/unit/commands/ci/last.unit.test.ts @@ -0,0 +1,164 @@ +import {runCommand} from '@oclif/test' +import {expect} from 'chai' +import nock from 'nock' + +describe('ci:last', function () { + const testRunNumber = 10 + const testRunId = 'f53d34b4-c3a9-4608-a186-17257cf71d62' + + afterEach(() => nock.cleanAll()) + + it('errors when not specifying a pipeline or an app', async () => { + const {error} = await runCommand(['ci:last']) + expect(error?.message).to.contain('Required flag: --pipeline PIPELINE or --app APP') + }) + + describe('when specifying an application', function () { + const application = {id: '14402644-c207-43aa-9bc1-974a34914010', name: 'pipeline'} + const pipeline = {id: '45450264-b207-467a-Abc1-999c34883645', name: 'aquafresh'} + + it('warns the user that there are no CI runs', async () => { + nock('https://api.heroku.com') + .get(`/apps/${application.name}/pipeline-couplings`) + .reply(200, { + id: '01234567-89ab-cdef-0123-456789abcdef', + app: { + id: `${application.id}`, + }, + pipeline: { + id: `${pipeline.id}`, + }, + stage: 'production', + }) + .get(`/pipelines/${pipeline.id}/test-runs`) + .reply(200, []) + + const {stderr} = await runCommand(['ci:last', '--app', `${application.name}`]) + + expect(stderr).to.contain('No Heroku CI runs found for the specified app and/or pipeline.') + }) + + it('errors when no pipelines exist', async () => { + nock('https://api.heroku.com') + .get(`/apps/${application.name}/pipeline-couplings`) + .reply(200, {}) + + const {error} = await runCommand(['ci:last', '--app', `${application.name}`]) + + expect(error?.message).to.contain(`No pipeline found with application ${application.name}`) + }) + }) + + describe('when specifying a pipeline', function () { + const pipeline = {id: '14402644-c207-43aa-9bc1-974a34914010', name: 'pipeline'} + + it('and a pipeline without parallel test runs it shows node output', async () => { + nock('https://api.heroku.com') + .get(`/pipelines?eq[name]=${pipeline.name}`) + .reply(200, [ + {id: pipeline.id}, + ]) + .get(`/pipelines/${pipeline.id}/test-runs/${testRunNumber}`) + .reply(200, + { + commit_branch: 'main', + commit_message: 'Merge pull request #5848 from heroku/cli', + commit_sha: 'b9e982a60904730510a1c9e2dd2df64aef6f0d84', + id: testRunId, + number: testRunNumber, + pipeline: {id: pipeline.id}, + status: 'succeeded', + }, + ) + .get(`/pipelines/${pipeline.id}/test-runs`) + .reply(200, [ + { + commit_branch: 'main', + commit_message: 'Merge pull request #5849 from heroku/cli', + commit_sha: 'b9e982a60904730510a1c9e2dd2df64aef6f0d84', + id: testRunId, + number: testRunNumber, + pipeline: {id: pipeline.id}, + status: 'succeeded', + }, + { + commit_branch: 'main', + commit_message: 'Merge pull request #5848 from heroku/cli', + commit_sha: 'b9e982a60904730510a1c9e2dd2df64aef6f0d84', + id: 'testRun.id', + number: 9, + pipeline: {id: pipeline.id}, + status: 'succeeded', + }, + ]) + .get(`/test-runs/${testRunId}/test-nodes`) + .reply(200, [ + { + commit_branch: 'main', + commit_message: 'Merge pull request #5848 from heroku/cli', + commit_sha: 'b9e982a60904730510a1c9e2dd2df64aef6f0d84', + id: testRunId, + number: testRunNumber, + pipeline: {id: pipeline.id}, + status: 'succeeded', + setup_stream_url: `https://test-setup-output.heroku.com/streams/${testRunId.slice(0, 3)}/test-runs/${testRunId}`, + output_stream_url: `https://test-output.heroku.com/streams/${testRunId.slice(0, 3)}/test-runs/${testRunId}`, + }, + ]) + + nock('https://test-setup-output.heroku.com/streams') + .get(`/${testRunId.slice(0, 3)}/test-runs/${testRunId}`) + .reply(200, 'Test setup output') + + nock('https://test-output.heroku.com/streams') + .get(`/${testRunId.slice(0, 3)}/test-runs/${testRunId}`) + .reply(200, 'Test output') + + const {stdout} = await runCommand(['ci:last', `--pipeline=${pipeline.name}`]) + + expect(stdout).to.equal('Test setup outputTest output\n✓ #10 main:b9e982a succeeded\n') + }) + }) + + describe('when test nodes is an empty array', function () { + const pipeline = {id: '14402644-c207-43aa-9bc1-974a34914010', name: 'pipeline'} + + it('shows an error about not test nodes found', async () => { + nock('https://api.heroku.com') + .get(`/pipelines?eq[name]=${pipeline.name}`) + .reply(200, [ + {id: pipeline.id}, + ]) + .get(`/pipelines/${pipeline.id}/test-runs/${testRunNumber}`) + .reply(200, + { + commit_branch: 'main', + commit_message: 'Merge pull request #5848 from heroku/cli', + commit_sha: 'b9e982a60904730510a1c9e2dd2df64aef6f0d84', + id: testRunId, + number: testRunNumber, + pipeline: {id: pipeline.id}, + status: 'cancelled', + }, + ) + .get(`/pipelines/${pipeline.id}/test-runs`) + .reply(200, [ + { + commit_branch: 'main', + commit_message: 'Merge pull request #5849 from heroku/cli', + commit_sha: 'b9e982a60904730510a1c9e2dd2df64aef6f0d84', + id: testRunId, + number: testRunNumber, + pipeline: {id: pipeline.id}, + status: 'cancelled', + }, + ]) + .get(`/test-runs/${testRunId}/test-nodes`) + .reply(200, []) + + const {error} = await runCommand(['ci:last', `--pipeline=${pipeline.name}`]) + + expect(error?.message).to.contain(`Test run ${testRunNumber} was cancelled. No Heroku CI runs found for this pipeline.`) + }) + }) +}) diff --git a/packages/cli/test/unit/commands/ci/last.unit.test.ts.skip b/packages/cli/test/unit/commands/ci/last.unit.test.ts.skip deleted file mode 100644 index ed3bbef69d..0000000000 --- a/packages/cli/test/unit/commands/ci/last.unit.test.ts.skip +++ /dev/null @@ -1,174 +0,0 @@ -import {expect, test} from '@oclif/test' - -describe('ci:last', function () { - const testRunNumber = 10 - const testRunId = 'f53d34b4-c3a9-4608-a186-17257cf71d62' - - test - .command(['ci:last']) - .catch(error => { - expect(error.message).to.contain('Required flag: --pipeline PIPELINE or --app APP') - }) - .it('errors when not specifying a pipeline or an app') - - describe('when specifying an application', function () { - const application = {id: '14402644-c207-43aa-9bc1-974a34914010', name: 'pipeline'} - const pipeline = {id: '45450264-b207-467a-Abc1-999c34883645', name: 'aquafresh'} - - test - .stderr() - .nock('https://api.heroku.com', api => { - api.get(`/apps/${application.name}/pipeline-couplings`) - .reply(200, { - id: '01234567-89ab-cdef-0123-456789abcdef', - app: { - id: `${application.id}`, - }, - pipeline: { - id: `${pipeline.id}`, - }, - stage: 'production', - }) - api.get(`/pipelines/${pipeline.id}/test-runs`) - .reply(200, []) - }) - .command(['ci:last', '--app', `${application.name}`]) - .it('warns the user that there are no CI runs', ctx => { - expect(ctx.stderr).to.contain('No Heroku CI runs found for the specified app and/or pipeline.') - }) - - test - .stderr() - .nock('https://api.heroku.com', api => { - api.get(`/apps/${application.name}/pipeline-couplings`) - .reply(200, {}) - }) - .command(['ci:last', '--app', `${application.name}`]) - .catch(error => { - expect(error.message).to.contain(`No pipeline found with application ${application.name}`) - }) - .it('errors when no pipelines exist') - }) - - describe('when specifying a pipeline', function () { - const pipeline = {id: '14402644-c207-43aa-9bc1-974a34914010', name: 'pipeline'} - - test - .stdout() - .nock('https://api.heroku.com', api => { - api.get(`/pipelines?eq[name]=${pipeline.name}`) - .reply(200, [ - {id: pipeline.id}, - ]) - - api.get(`/pipelines/${pipeline.id}/test-runs/${testRunNumber}`) - .reply(200, - { - commit_branch: 'main', - commit_message: 'Merge pull request #5848 from heroku/cli', - commit_sha: 'b9e982a60904730510a1c9e2dd2df64aef6f0d84', - id: testRunId, - number: testRunNumber, - pipeline: {id: pipeline.id}, - status: 'succeeded', - }, - ) - - api.get(`/pipelines/${pipeline.id}/test-runs`) - .reply(200, [ - { - commit_branch: 'main', - commit_message: 'Merge pull request #5849 from heroku/cli', - commit_sha: 'b9e982a60904730510a1c9e2dd2df64aef6f0d84', - id: testRunId, - number: testRunNumber, - pipeline: {id: pipeline.id}, - status: 'succeeded', - }, - { - commit_branch: 'main', - commit_message: 'Merge pull request #5848 from heroku/cli', - commit_sha: 'b9e982a60904730510a1c9e2dd2df64aef6f0d84', - id: 'testRun.id', - number: 9, - pipeline: {id: pipeline.id}, - status: 'succeeded', - }, - ]) - - api.get(`/test-runs/${testRunId}/test-nodes`) - .reply(200, [ - { - commit_branch: 'main', - commit_message: 'Merge pull request #5848 from heroku/cli', - commit_sha: 'b9e982a60904730510a1c9e2dd2df64aef6f0d84', - id: testRunId, - number: testRunNumber, - pipeline: {id: pipeline.id}, - status: 'succeeded', - setup_stream_url: `https://test-setup-output.heroku.com/streams/${testRunId.slice(0, 3)}/test-runs/${testRunId}`, - output_stream_url: `https://test-output.heroku.com/streams/${testRunId.slice(0, 3)}/test-runs/${testRunId}`, - }, - ]) - }) - .nock('https://test-setup-output.heroku.com/streams', testOutputAPI => { - testOutputAPI.get(`/${testRunId.slice(0, 3)}/test-runs/${testRunId}`) - .reply(200, 'Test setup output') - }) - .nock('https://test-output.heroku.com/streams', testOutputAPI => { - testOutputAPI.get(`/${testRunId.slice(0, 3)}/test-runs/${testRunId}`) - .reply(200, 'Test output') - }) - .command(['ci:last', `--pipeline=${pipeline.name}`]) - .it('and a pipeline without parallel test runs it shows node output', ({stdout}) => { - expect(stdout).to.equal('Test setup outputTest output\n✓ #10 main:b9e982a succeeded\n') - }) - }) - - describe('when test nodes is an empty array', function () { - const pipeline = {id: '14402644-c207-43aa-9bc1-974a34914010', name: 'pipeline'} - - test - .stdout() - .nock('https://api.heroku.com', api => { - api.get(`/pipelines?eq[name]=${pipeline.name}`) - .reply(200, [ - {id: pipeline.id}, - ]) - - api.get(`/pipelines/${pipeline.id}/test-runs/${testRunNumber}`) - .reply(200, - { - commit_branch: 'main', - commit_message: 'Merge pull request #5848 from heroku/cli', - commit_sha: 'b9e982a60904730510a1c9e2dd2df64aef6f0d84', - id: testRunId, - number: testRunNumber, - pipeline: {id: pipeline.id}, - status: 'cancelled', - }, - ) - - api.get(`/pipelines/${pipeline.id}/test-runs`) - .reply(200, [ - { - commit_branch: 'main', - commit_message: 'Merge pull request #5849 from heroku/cli', - commit_sha: 'b9e982a60904730510a1c9e2dd2df64aef6f0d84', - id: testRunId, - number: testRunNumber, - pipeline: {id: pipeline.id}, - status: 'cancelled', - }, - ]) - - api.get(`/test-runs/${testRunId}/test-nodes`) - .reply(200, []) - }) - .command(['ci:last', `--pipeline=${pipeline.name}`]) - .catch(error => { - expect(error.message).to.contain(`Test run ${testRunNumber} was cancelled. No Heroku CI runs found for this pipeline.`) - }) - .it('shows an error about not test nodes found') - }) -}) From d650ad752c9eefc23d7c599351937d34087e9b4d Mon Sep 17 00:00:00 2001 From: Eric Black Date: Wed, 7 Jan 2026 10:51:20 -0800 Subject: [PATCH 08/14] test: migrate ci:migrate-manifest to @oclif/test v4 Convert from chained test API to async/await with runCommand(). Replace .do() with direct async setup before runCommand(). --- ....ts.skip => migrate-manifest.unit.test.ts} | 42 +++++++++---------- 1 file changed, 20 insertions(+), 22 deletions(-) rename packages/cli/test/unit/commands/ci/{migrate-manifest.unit.test.ts.skip => migrate-manifest.unit.test.ts} (72%) diff --git a/packages/cli/test/unit/commands/ci/migrate-manifest.unit.test.ts.skip b/packages/cli/test/unit/commands/ci/migrate-manifest.unit.test.ts similarity index 72% rename from packages/cli/test/unit/commands/ci/migrate-manifest.unit.test.ts.skip rename to packages/cli/test/unit/commands/ci/migrate-manifest.unit.test.ts index 7a5840c11b..1713345e2e 100644 --- a/packages/cli/test/unit/commands/ci/migrate-manifest.unit.test.ts.skip +++ b/packages/cli/test/unit/commands/ci/migrate-manifest.unit.test.ts @@ -1,4 +1,5 @@ -import {expect, test} from '@oclif/test' +import {runCommand} from '@oclif/test' +import {expect} from 'chai' import {promises as fs} from 'node:fs' const {readFile, writeFile, unlink} = fs const unlinkFile = unlink @@ -140,28 +141,25 @@ describe('ci:migrate-manifest', function () { }, } - test - .stdout() - .command(['ci:migrate-manifest']) - .it('creates an app.json file if none exists', async ({stdout}) => { - const fileContents = await readFile(`${process.cwd()}/app.json`, 'utf8') - appJsonFileContents = JSON.parse(fileContents) + it('creates an app.json file if none exists', async () => { + const {stdout} = await runCommand(['ci:migrate-manifest']) - expect(stdout).to.equal('We couldn\'t find an app-ci.json file in the current directory, but we\'re creating a new app.json manifest for you.\nPlease check the contents of your app.json before committing to your repo.\nYou\'re all set! 🎉\n') - expect(appJsonFileContents).to.deep.equal(mockNewAppJsonFileContents) - }) + const fileContents = await readFile(`${process.cwd()}/app.json`, 'utf8') + appJsonFileContents = JSON.parse(fileContents) - test - .stdout() - .do(async () => { - await writeFile('app-ci.json', `${JSON.stringify(mockOldAppCiJsonFileContents, null, ' ')}\n`) - }) - .command(['ci:migrate-manifest']) - .it('creates converted app.json file when app-ci.json file is present', async ({stdout}) => { - const fileContents = await readFile(`${process.cwd()}/app.json`, 'utf8') - appJsonFileContents = JSON.parse(fileContents) + expect(stdout).to.equal('We couldn\'t find an app-ci.json file in the current directory, but we\'re creating a new app.json manifest for you.\nPlease check the contents of your app.json before committing to your repo.\nYou\'re all set! 🎉\n') + expect(appJsonFileContents).to.deep.equal(mockNewAppJsonFileContents) + }) + + it('creates converted app.json file when app-ci.json file is present', async () => { + await writeFile('app-ci.json', `${JSON.stringify(mockOldAppCiJsonFileContents, null, ' ')}\n`) + + const {stdout} = await runCommand(['ci:migrate-manifest']) - expect(stdout).to.equal('Please check the contents of your app.json before committing to your repo.\nYou\'re all set! 🎉\n') - expect(appJsonFileContents).to.deep.equal(mockConvertedAppJSONFileContents) - }) + const fileContents = await readFile(`${process.cwd()}/app.json`, 'utf8') + appJsonFileContents = JSON.parse(fileContents) + + expect(stdout).to.equal('Please check the contents of your app.json before committing to your repo.\nYou\'re all set! 🎉\n') + expect(appJsonFileContents).to.deep.equal(mockConvertedAppJSONFileContents) + }) }) From 490950dfeffff8fa08cb8e4b2cc32e428f8911a3 Mon Sep 17 00:00:00 2001 From: Eric Black Date: Wed, 7 Jan 2026 11:15:02 -0800 Subject: [PATCH 09/14] feat: add GitService and FileService wrappers for testing Add GitService class in git.ts and FileService class in source.ts to wrap ES module functions, enabling proper stubbing in tests. test: migrate ci:rerun to @oclif/test v4 Convert from chained test API to async/await with customRunCommand(). Use GitService and FileService stubs instead of trying to stub ES modules directly. --- packages/cli/src/lib/ci/git.ts | 32 ++- packages/cli/src/lib/ci/source.ts | 35 ++- .../test/unit/commands/ci/rerun.unit.test.ts | 206 ++++++++++++++ .../unit/commands/ci/rerun.unit.test.ts.skip | 261 ------------------ 4 files changed, 253 insertions(+), 281 deletions(-) create mode 100644 packages/cli/test/unit/commands/ci/rerun.unit.test.ts delete mode 100644 packages/cli/test/unit/commands/ci/rerun.unit.test.ts.skip diff --git a/packages/cli/src/lib/ci/git.ts b/packages/cli/src/lib/ci/git.ts index cb32c5ac77..0990cce54f 100644 --- a/packages/cli/src/lib/ci/git.ts +++ b/packages/cli/src/lib/ci/git.ts @@ -1,8 +1,7 @@ -import fs from 'fs-extra' import {vars} from '@heroku-cli/command' -import {spawn} from 'node:child_process' - +import fs from 'fs-extra' import gh from 'github-url-to-object' +import {spawn} from 'node:child_process' import tmp from 'tmp' const NOT_A_GIT_REPOSITORY = 'not a git repository' @@ -79,11 +78,11 @@ async function readCommit(commit: string) { const ref = await getRef(commit) const message = await getCommitTitle(ref!) - return Promise.resolve({ + return { branch, - ref, message, - }) + ref, + } } function sshGitUrl(app: string) { @@ -144,14 +143,25 @@ async function createRemote(remote: string, url: string) { return null } +// GitService class for easier testing/stubbing +export class GitService { + async createArchive(ref: string) { + return createArchive(ref) + } + + async githubRepository() { + return githubRepository() + } +} + export { createArchive, - githubRepository, - readCommit, - sshGitUrl, - gitUrl, createRemote, + gitUrl, + githubRepository, + inGitRepo, listRemotes, + readCommit, rmRemote, - inGitRepo, + sshGitUrl, } diff --git a/packages/cli/src/lib/ci/source.ts b/packages/cli/src/lib/ci/source.ts index 392c559c89..6978a1fa40 100644 --- a/packages/cli/src/lib/ci/source.ts +++ b/packages/cli/src/lib/ci/source.ts @@ -1,20 +1,34 @@ import {Command} from '@heroku-cli/command' -import {promises as fs} from 'fs' -import {createReadStream} from 'fs' -import * as git from './git.js' -import {got} from 'got' import debug from 'debug' +import {createReadStream, promises as fs} from 'fs' +import {got} from 'got' + +import {GitService} from './git.js' const ciDebug = debug('ci') +const gitService = new GitService() + +// FileService class for easier testing/stubbing +export class FileService { + createReadStream(filePath: string) { + return createReadStream(filePath) + } + + async stat(filePath: string) { + return fs.stat(filePath) + } +} + +const fileService = new FileService() async function uploadArchive(url: string, filePath: string) { const request = got.stream.put(url, { headers: { - 'content-length': (await fs.stat(filePath)).size.toString(), + 'content-length': (await fileService.stat(filePath)).size.toString(), }, }) - createReadStream(filePath).pipe(request) + fileService.createReadStream(filePath).pipe(request) return new Promise((resolve: any, reject: any) => { request.on('error', reject) @@ -23,15 +37,15 @@ async function uploadArchive(url: string, filePath: string) { } async function prepareSource(ref: any, command: Command) { - const filePath = await git.createArchive(ref) + const filePath = await gitService.createArchive(ref) const {body: source} = await command.heroku.post('/sources') await uploadArchive(source.source_blob.put_url, filePath) - return Promise.resolve(source) + return source } export async function createSourceBlob(ref: any, command: Command) { try { - const githubRepository = await git.githubRepository() + const githubRepository = await gitService.githubRepository() const {user, repo} = githubRepository const {body: archiveLink} = await command.heroku.get(`https://kolkrabbi.heroku.com/github/repos/${user}/${repo}/tarball/${ref}`) @@ -46,3 +60,6 @@ export async function createSourceBlob(ref: any, command: Command) { const sourceBlob = await prepareSource(ref, command) return sourceBlob.source_blob.get_url } + +// Export service instances for testing +export {fileService, gitService} diff --git a/packages/cli/test/unit/commands/ci/rerun.unit.test.ts b/packages/cli/test/unit/commands/ci/rerun.unit.test.ts new file mode 100644 index 0000000000..9a70fb083b --- /dev/null +++ b/packages/cli/test/unit/commands/ci/rerun.unit.test.ts @@ -0,0 +1,206 @@ +import {expect} from 'chai' +import nock from 'nock' +import sinon from 'sinon' +import {PassThrough} from 'node:stream' +import {gitService, fileService} from '../../../../src/lib/ci/source.js' +import {got} from 'got' +import customRunCommand from '../../../helpers/runCommand.js' +import Cmd from '../../../../src/commands/ci/rerun.js' +import {stdout} from 'stdout-stderr' + +describe('ci:rerun', function () { + afterEach(() => nock.cleanAll()) + + it('errors when not specifying a pipeline or an app', async () => { + try { + await customRunCommand(Cmd, []) + } catch (error: any) { + expect(error.message).to.contain('Required flag: --pipeline PIPELINE or --app APP') + } + }) + + describe('when specifying a pipeline', function () { + const pipeline = {id: '14402644-c207-43aa-9bc1-974a34914010', name: 'pipeline'} + const ghRepository = { + user: 'heroku-fake', repo: 'my-repo', ref: '668a5ce22eefc7b67c84c1cfe3a766f1958e0add', branch: 'my-test-branch', + } + const oldTestRun = { + commit_branch: ghRepository.branch, + commit_message: 'earlier commit', + commit_sha: '2F3CAFFD6AEEC967A7D71EB7ABEC0993D036430691E668A8710248DF4541111E', + id: 'd76b690b-a4ce-4a7b-83ca-c30792d4f3be', + number: 10, + pipeline: {id: pipeline.id}, + status: 'failed', + } + const newTestRun = { + commit_branch: ghRepository.branch, + commit_message: 'latest commit', + commit_sha: ghRepository.ref, + id: 'b6512323-3a11-43ac-b4e4-9668b6a6b30c', + number: 11, + pipeline: {id: pipeline.id}, + status: 'succeeded', + } + let sandbox: ReturnType + + beforeEach(function () { + sandbox = sinon.createSandbox() + + // Stub gitService methods + sandbox.stub(gitService, 'githubRepository').resolves({user: ghRepository.user, repo: ghRepository.repo} as any) + sandbox.stub(gitService, 'createArchive').resolves('new-archive.tgz') + + // Stub fileService methods + sandbox.stub(fileService, 'stat').resolves({size: 500} as any) + sandbox.stub(fileService, 'createReadStream').returns((() => { + const stream = new PassThrough() + stream.end('fake archive data') + return stream + })() as any) + + // Stub got.stream + sandbox.stub(got, 'stream').value({ + put() { + const stream = new PassThrough() + setImmediate(() => { + stream.emit('response') + }) + return stream + }, + }) + }) + + afterEach(function () { + sandbox.restore() + }) + + describe('when not specifying a run #', function () { + it('it runs the test and displays the test output for the first node', async () => { + nock('https://api.heroku.com') + .get(`/pipelines?eq[name]=${pipeline.name}`) + .reply(200, [ + {id: pipeline.id}, + ]) + .get(`/pipelines/${pipeline.id}/test-runs`) + .reply(200, [oldTestRun]) + .post('/test-runs') + .reply(200, newTestRun) + .get(`/pipelines/${pipeline.id}/test-runs/${newTestRun.number}`) + .reply(200, newTestRun) + .get(`/test-runs/${newTestRun.id}/test-nodes`) + .times(2) + .reply(200, [ + { + commit_branch: newTestRun.commit_branch, + commit_message: newTestRun.commit_message, + commit_sha: newTestRun.commit_sha, + id: newTestRun.id, + number: newTestRun.number, + pipeline: {id: pipeline.id}, + exit_code: 0, + status: newTestRun.status, + setup_stream_url: `https://test-setup-output.heroku.com/streams/${newTestRun.id.slice(0, 3)}/test-runs/${newTestRun.id}`, + output_stream_url: `https://test-output.heroku.com/streams/${newTestRun.id.slice(0, 3)}/test-runs/${newTestRun.id}`, + }, + ]) + .post('/sources') + .reply(200, {source_blob: {put_url: 'https://aws-puturl', get_url: 'https://aws-geturl'}}) + + nock('https://test-setup-output.heroku.com/streams') + .get(`/${newTestRun.id.slice(0, 3)}/test-runs/${newTestRun.id}`) + .reply(200, 'New Test setup output') + + nock('https://test-output.heroku.com/streams') + .get(`/${newTestRun.id.slice(0, 3)}/test-runs/${newTestRun.id}`) + .reply(200, 'New Test output') + + nock('https://kolkrabbi.heroku.com') + .get(`/pipelines/${pipeline.id}/repository`) + .reply(200, { + ci: true, + organization: {id: 'e037ed63-5781-48ee-b2b7-8c55c571b63e'}, + owner: { + id: '463147bf-d572-41cf-bbf4-11ebc1c0bc3b', + heroku: { + user_id: '463147bf-d572-41cf-bbf4-11ebc1c0bc3b'}, + github: {user_id: 306015}, + }, + repository: { + id: 138865824, + name: 'raulb/atleti', + type: 'github', + }, + }) + + await customRunCommand(Cmd, [`--pipeline=${pipeline.name}`]) + + expect(stdout.output).to.equal('Rerunning test run #10...\nNew Test setup outputNew Test output\n✓ #11 my-test-branch:668a5ce succeeded\n') + }) + }) + + describe('when specifying a run #', function () { + it('it runs the test and displays the test output for the first node', async () => { + nock('https://api.heroku.com') + .get(`/pipelines?eq[name]=${pipeline.name}`) + .reply(200, [ + {id: pipeline.id}, + ]) + .get(`/pipelines/${pipeline.id}/test-runs/${oldTestRun.number}`) + .reply(200, oldTestRun) + .post('/test-runs') + .reply(200, newTestRun) + .get(`/pipelines/${pipeline.id}/test-runs/${newTestRun.number}`) + .reply(200, newTestRun) + .get(`/test-runs/${newTestRun.id}/test-nodes`) + .times(2) + .reply(200, [ + { + commit_branch: newTestRun.commit_branch, + commit_message: newTestRun.commit_message, + commit_sha: newTestRun.commit_sha, + id: newTestRun.id, + number: newTestRun.number, + pipeline: {id: pipeline.id}, + exit_code: 0, + status: newTestRun.status, + setup_stream_url: `https://test-setup-output.heroku.com/streams/${newTestRun.id.slice(0, 3)}/test-runs/${newTestRun.id}`, + output_stream_url: `https://test-output.heroku.com/streams/${newTestRun.id.slice(0, 3)}/test-runs/${newTestRun.id}`, + }, + ]) + .post('/sources') + .reply(200, {source_blob: {put_url: 'https://aws-puturl', get_url: 'https://aws-geturl'}}) + + nock('https://test-setup-output.heroku.com/streams') + .get(`/${newTestRun.id.slice(0, 3)}/test-runs/${newTestRun.id}`) + .reply(200, 'New Test setup output') + + nock('https://test-output.heroku.com/streams') + .get(`/${newTestRun.id.slice(0, 3)}/test-runs/${newTestRun.id}`) + .reply(200, 'New Test output') + + nock('https://kolkrabbi.heroku.com') + .get(`/pipelines/${pipeline.id}/repository`) + .reply(200, { + ci: true, + organization: {id: 'e037ed63-5781-48ee-b2b7-8c55c571b63e'}, + owner: { + id: '463147bf-d572-41cf-bbf4-11ebc1c0bc3b', + heroku: { + user_id: '463147bf-d572-41cf-bbf4-11ebc1c0bc3b'}, + github: {user_id: 306015}, + }, + repository: { + id: 138865824, + name: 'raulb/atleti', + type: 'github', + }, + }) + + await customRunCommand(Cmd, [`${oldTestRun.number}`, `--pipeline=${pipeline.name}`]) + + expect(stdout.output).to.equal('Rerunning test run #10...\nNew Test setup outputNew Test output\n✓ #11 my-test-branch:668a5ce succeeded\n') + }) + }) + }) +}) diff --git a/packages/cli/test/unit/commands/ci/rerun.unit.test.ts.skip b/packages/cli/test/unit/commands/ci/rerun.unit.test.ts.skip deleted file mode 100644 index cbbe9e4202..0000000000 --- a/packages/cli/test/unit/commands/ci/rerun.unit.test.ts.skip +++ /dev/null @@ -1,261 +0,0 @@ -import {test, expect} from '@oclif/test' -import {promises as fs} from 'fs' -import {PassThrough} from 'node:stream' - -import * as git from '../../../../src/lib/ci/git.js' -import got from 'got' - -describe('ci:rerun', function () { - test - .command(['ci:rerun']) - .catch(error => { - expect(error.message).to.contain('Required flag: --pipeline PIPELINE or --app APP') - }) - .it('errors when not specifying a pipeline or an app') - - describe('when specifying a pipeline', function () { - const pipeline = {id: '14402644-c207-43aa-9bc1-974a34914010', name: 'pipeline'} - const ghRepository = { - user: 'heroku-fake', repo: 'my-repo', ref: '668a5ce22eefc7b67c84c1cfe3a766f1958e0add', branch: 'my-test-branch', - } - const oldTestRun = { - commit_branch: ghRepository.branch, - commit_message: 'earlier commit', - commit_sha: '2F3CAFFD6AEEC967A7D71EB7ABEC0993D036430691E668A8710248DF4541111E', - id: 'd76b690b-a4ce-4a7b-83ca-c30792d4f3be', - number: 10, - pipeline: {id: pipeline.id}, - status: 'failed', - } - const newTestRun = { - commit_branch: ghRepository.branch, - commit_message: 'latest commit', - commit_sha: ghRepository.ref, - id: 'b6512323-3a11-43ac-b4e4-9668b6a6b30c', - number: 11, - pipeline: {id: pipeline.id}, - status: 'succeeded', - } - const gitFake = { - readCommit: () => ({branch: ghRepository.branch, ref: ghRepository.ref}), - remoteFromGitConfig: () => Promise.resolve('heroku'), - getBranch: () => Promise.resolve(ghRepository.branch), - getRef: () => Promise.resolve(ghRepository.ref), - getCommitTitle: () => Promise.resolve(`pushed to ${ghRepository.branch}`), - githubRepository: () => Promise.resolve({user: ghRepository.user, repo: ghRepository.repo}), - createArchive: () => Promise.resolve('new-archive.tgz'), - spawn: () => Promise.resolve(), - urlExists: () => Promise.resolve(), - exec(args: any) { - switch (args.join(' ')) { - case 'remote': { - return Promise.resolve('heroku') - } - - default: { - return Promise.resolve() - } - } - }, - } - - const fsFake = { - stat: () => Promise.resolve({size: 500}), - createReadStream: () => ({ - pipe(dest: any) { - // Simulate a readable stream that properly pipes to destination - if (dest && typeof dest.once === 'function') { - dest.once('response', () => {}) - } - - return dest - }, - once() {}, - on() {}, - }), - } - - const gotFake = { - stream: {put() { - const stream = new PassThrough() - // Simulate HTTP response by emitting 'response' event - setImmediate(() => { - stream.emit('response') - }) - return stream - }}, - } - - describe('when not specifying a run #', function () { - test - .stdout() - .nock('https://api.heroku.com', api => { - api.get(`/pipelines?eq[name]=${pipeline.name}`) - .reply(200, [ - {id: pipeline.id}, - ]) - - api.get(`/pipelines/${pipeline.id}/test-runs`) - .reply(200, [oldTestRun]) - - api.post('/test-runs') - .reply(200, newTestRun) - - api.get(`/pipelines/${pipeline.id}/test-runs/${newTestRun.number}`) - .reply(200, newTestRun) - - api.get(`/test-runs/${newTestRun.id}/test-nodes`) - .times(2) - .reply(200, [ - { - commit_branch: newTestRun.commit_branch, - commit_message: newTestRun.commit_message, - commit_sha: newTestRun.commit_sha, - id: newTestRun.id, - number: newTestRun.number, - pipeline: {id: pipeline.id}, - exit_code: 0, - status: newTestRun.status, - setup_stream_url: `https://test-setup-output.heroku.com/streams/${newTestRun.id.slice(0, 3)}/test-runs/${newTestRun.id}`, - output_stream_url: `https://test-output.heroku.com/streams/${newTestRun.id.slice(0, 3)}/test-runs/${newTestRun.id}`, - }, - ]) - - api.post('/sources') - .reply(200, {source_blob: {put_url: 'https://aws-puturl', get_url: 'https://aws-geturl'}}) - }) - .nock('https://test-setup-output.heroku.com/streams', testOutputAPI => { - testOutputAPI.get(`/${newTestRun.id.slice(0, 3)}/test-runs/${newTestRun.id}`) - .reply(200, 'New Test setup output') - }) - .nock('https://test-output.heroku.com/streams', testOutputAPI => { - testOutputAPI.get(`/${newTestRun.id.slice(0, 3)}/test-runs/${newTestRun.id}`) - .reply(200, 'New Test output') - }) - .nock('https://kolkrabbi.heroku.com', kolkrabbiAPI => { - kolkrabbiAPI.get(`/pipelines/${pipeline.id}/repository`) - .reply(200, { - ci: true, - organization: {id: 'e037ed63-5781-48ee-b2b7-8c55c571b63e'}, - owner: { - id: '463147bf-d572-41cf-bbf4-11ebc1c0bc3b', - heroku: { - user_id: '463147bf-d572-41cf-bbf4-11ebc1c0bc3b'}, - github: {user_id: 306015}, - }, - repository: { - id: 138865824, - name: 'raulb/atleti', - type: 'github', - }, - }) - }) - .stub(git, 'githubRepository', gitFake.githubRepository) - .stub(git, 'createArchive', gitFake.createArchive) - .stub(fs, 'stat', fsFake.stat) - .stub(fs, 'createReadStream', () => { - const stream = new PassThrough() - stream.end('fake archive data') - return stream - }) - .stub( - got, - 'stream', - // disable below is due to incomplete type definition of `stub` - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore-next-line - gotFake.stream, - ) - .command(['ci:rerun', `--pipeline=${pipeline.name}`]) - .it('it runs the test and displays the test output for the first node', ({stdout}) => { - expect(stdout).to.equal('Rerunning test run #10...\nNew Test setup outputNew Test output\n✓ #11 my-test-branch:668a5ce succeeded\n') - }) - }) - - describe('when specifying a run #', function () { - test - .stdout() - .nock('https://api.heroku.com', api => { - api.get(`/pipelines?eq[name]=${pipeline.name}`) - .reply(200, [ - {id: pipeline.id}, - ]) - - api.get(`/pipelines/${pipeline.id}/test-runs/${oldTestRun.number}`) - .reply(200, oldTestRun) - - api.post('/test-runs') - .reply(200, newTestRun) - - api.get(`/pipelines/${pipeline.id}/test-runs/${newTestRun.number}`) - .reply(200, newTestRun) - - api.get(`/test-runs/${newTestRun.id}/test-nodes`) - .times(2) - .reply(200, [ - { - commit_branch: newTestRun.commit_branch, - commit_message: newTestRun.commit_message, - commit_sha: newTestRun.commit_sha, - id: newTestRun.id, - number: newTestRun.number, - pipeline: {id: pipeline.id}, - exit_code: 0, - status: newTestRun.status, - setup_stream_url: `https://test-setup-output.heroku.com/streams/${newTestRun.id.slice(0, 3)}/test-runs/${newTestRun.id}`, - output_stream_url: `https://test-output.heroku.com/streams/${newTestRun.id.slice(0, 3)}/test-runs/${newTestRun.id}`, - }, - ]) - - api.post('/sources') - .reply(200, {source_blob: {put_url: 'https://aws-puturl', get_url: 'https://aws-geturl'}}) - }) - .nock('https://test-setup-output.heroku.com/streams', testOutputAPI => { - testOutputAPI.get(`/${newTestRun.id.slice(0, 3)}/test-runs/${newTestRun.id}`) - .reply(200, 'New Test setup output') - }) - .nock('https://test-output.heroku.com/streams', testOutputAPI => { - testOutputAPI.get(`/${newTestRun.id.slice(0, 3)}/test-runs/${newTestRun.id}`) - .reply(200, 'New Test output') - }) - .nock('https://kolkrabbi.heroku.com', kolkrabbiAPI => { - kolkrabbiAPI.get(`/pipelines/${pipeline.id}/repository`) - .reply(200, { - ci: true, - organization: {id: 'e037ed63-5781-48ee-b2b7-8c55c571b63e'}, - owner: { - id: '463147bf-d572-41cf-bbf4-11ebc1c0bc3b', - heroku: { - user_id: '463147bf-d572-41cf-bbf4-11ebc1c0bc3b'}, - github: {user_id: 306015}, - }, - repository: { - id: 138865824, - name: 'raulb/atleti', - type: 'github', - }, - }) - }) - .stub(git, 'githubRepository', gitFake.githubRepository) - .stub(git, 'createArchive', gitFake.createArchive) - .stub(fs, 'stat', fsFake.stat) - .stub(fs, 'createReadStream', () => { - const stream = new PassThrough() - stream.end('fake archive data') - return stream - }) - .stub( - got, - 'stream', - // disable below is due to incomplete type definition of `stub` - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore-next-line - gotFake.stream, - ) - .command(['ci:rerun', `${oldTestRun.number}`, `--pipeline=${pipeline.name}`]) - .it('it runs the test and displays the test output for the first node', ({stdout}) => { - expect(stdout).to.equal('Rerunning test run #10...\nNew Test setup outputNew Test output\n✓ #11 my-test-branch:668a5ce succeeded\n') - }) - }) - }) -}) From 82a05419d0d09989b0ee11e12568b5c8a6f36484 Mon Sep 17 00:00:00 2001 From: Eric Black Date: Wed, 7 Jan 2026 11:22:02 -0800 Subject: [PATCH 10/14] feat: add readCommit to GitService and update ci:run command Add readCommit method to GitService class for consistency. Update ci:run command to use GitService instance instead of direct imports. test: migrate ci:run to @oclif/test v4 Convert from chained test API to async/await with customRunCommand(). Use GitService and FileService stubs for ES module dependencies. --- packages/cli/src/commands/ci/run.ts | 6 +- packages/cli/src/lib/ci/git.ts | 4 + .../test/unit/commands/ci/run.unit.test.ts | 198 ++++++++++++++ .../unit/commands/ci/run.unit.test.ts.skip | 248 ------------------ 4 files changed, 206 insertions(+), 250 deletions(-) create mode 100644 packages/cli/test/unit/commands/ci/run.unit.test.ts delete mode 100644 packages/cli/test/unit/commands/ci/run.unit.test.ts.skip diff --git a/packages/cli/src/commands/ci/run.ts b/packages/cli/src/commands/ci/run.ts index abf4827a98..8c9679aaab 100644 --- a/packages/cli/src/commands/ci/run.ts +++ b/packages/cli/src/commands/ci/run.ts @@ -3,11 +3,13 @@ import * as Heroku from '@heroku-cli/schema' import {ux} from '@oclif/core' import * as Kolkrabbi from '../../lib/ci/interfaces/kolkrabbi.js' -import * as git from '../../lib/ci/git.js' +import {GitService} from '../../lib/ci/git.js' import {getPipeline} from '../../lib/ci/pipelines.js' import {createSourceBlob} from '../../lib/ci/source.js' import {displayAndExit} from '../../lib/ci/test-run.js' +const gitService = new GitService() + export default class CiRun extends Command { static description = 'run tests against current directory' @@ -25,7 +27,7 @@ export default class CiRun extends Command { async run() { const {flags} = await this.parse(CiRun) const pipeline = await getPipeline(flags, this.heroku) - const commit = await git.readCommit('HEAD') + const commit = await gitService.readCommit('HEAD') ux.action.start('Preparing source') const sourceBlobUrl = await createSourceBlob(commit.ref, this) diff --git a/packages/cli/src/lib/ci/git.ts b/packages/cli/src/lib/ci/git.ts index 0990cce54f..b0b6532542 100644 --- a/packages/cli/src/lib/ci/git.ts +++ b/packages/cli/src/lib/ci/git.ts @@ -152,6 +152,10 @@ export class GitService { async githubRepository() { return githubRepository() } + + async readCommit(commit: string) { + return readCommit(commit) + } } export { diff --git a/packages/cli/test/unit/commands/ci/run.unit.test.ts b/packages/cli/test/unit/commands/ci/run.unit.test.ts new file mode 100644 index 0000000000..d46f6c0e9f --- /dev/null +++ b/packages/cli/test/unit/commands/ci/run.unit.test.ts @@ -0,0 +1,198 @@ +import {expect} from 'chai' +import {got} from 'got' +import nock from 'nock' +import {PassThrough} from 'node:stream' +import sinon from 'sinon' +import {stdout} from 'stdout-stderr' + +import Cmd from '../../../../src/commands/ci/run.js' +import {fileService, gitService} from '../../../../src/lib/ci/source.js' +import customRunCommand from '../../../helpers/runCommand.js' + +describe('ci:run', function () { + afterEach(function () { + return nock.cleanAll() + }) + + it('errors when not specifying a pipeline or an app', async function () { + try { + await customRunCommand(Cmd, []) + } catch (error: any) { + expect(error.message).to.contain('Required flag: --pipeline PIPELINE or --app APP') + } + }) + + describe('when specifying a pipeline', function () { + const pipeline = {id: '14402644-c207-43aa-9bc1-974a34914010', name: 'pipeline'} + const ghRepository = { + branch: 'my-test-branch', + ref: '668a5ce22eefc7b67c84c1cfe3a766f1958e0add', + repo: 'my-repo', + user: 'heroku-fake', + } + const newTestRun = { + commit_branch: ghRepository.branch, + commit_message: 'latest commit', + commit_sha: ghRepository.ref, + id: 'b6512323-3a11-43ac-b4e4-9668b6a6b30c', + number: 11, + pipeline: {id: pipeline.id}, + status: 'succeeded', + } + + let sandbox: ReturnType + + beforeEach(function () { + sandbox = sinon.createSandbox() + + // Stub gitService methods + sandbox.stub(gitService, 'readCommit').resolves({branch: ghRepository.branch, ref: ghRepository.ref, message: `pushed to ${ghRepository.branch}`}) + sandbox.stub(gitService, 'githubRepository').resolves({user: ghRepository.user, repo: ghRepository.repo} as any) + sandbox.stub(gitService, 'createArchive').resolves('new-archive.tgz') + + // Stub fileService methods + sandbox.stub(fileService, 'stat').resolves({size: 500} as any) + sandbox.stub(fileService, 'createReadStream').returns((() => { + const stream = new PassThrough() + stream.end('fake archive data') + return stream + })() as any) + + // Stub got.stream + sandbox.stub(got, 'stream').value({ + put() { + const stream = new PassThrough() + setImmediate(() => { + stream.emit('response') + }) + return stream + }, + }) + }) + + afterEach(function () { + sandbox.restore() + }) + + it('it runs the test and displays the test output for the first node', async function () { + nock('https://api.heroku.com') + .get(`/pipelines?eq[name]=${pipeline.name}`) + .reply(200, [ + {id: pipeline.id}, + ]) + .post('/test-runs') + .reply(200, newTestRun) + .get(`/pipelines/${pipeline.id}/test-runs/${newTestRun.number}`) + .reply(200, newTestRun) + .get(`/test-runs/${newTestRun.id}/test-nodes`) + .times(2) + .reply(200, [ + { + commit_branch: newTestRun.commit_branch, + commit_message: newTestRun.commit_message, + commit_sha: newTestRun.commit_sha, + exit_code: 0, + id: newTestRun.id, + number: newTestRun.number, + output_stream_url: `https://test-output.heroku.com/streams/${newTestRun.id.slice(0, 3)}/test-runs/${newTestRun.id}`, + pipeline: {id: pipeline.id}, + setup_stream_url: `https://test-setup-output.heroku.com/streams/${newTestRun.id.slice(0, 3)}/test-runs/${newTestRun.id}`, + status: newTestRun.status, + }, + ]) + .post('/sources') + .reply(200, {source_blob: {get_url: 'https://aws-geturl', put_url: 'https://aws-puturl'}}) + + nock('https://test-setup-output.heroku.com/streams') + .get(`/${newTestRun.id.slice(0, 3)}/test-runs/${newTestRun.id}`) + .reply(200, 'New Test setup output') + + nock('https://test-output.heroku.com/streams') + .get(`/${newTestRun.id.slice(0, 3)}/test-runs/${newTestRun.id}`) + .reply(200, 'New Test output') + + nock('https://kolkrabbi.heroku.com') + .get(`/pipelines/${pipeline.id}/repository`) + .reply(200, { + ci: true, + organization: {id: 'e037ed63-5781-48ee-b2b7-8c55c571b63e'}, + owner: { + github: {user_id: 306015}, + heroku: { + user_id: '463147bf-d572-41cf-bbf4-11ebc1c0bc3b'}, + id: '463147bf-d572-41cf-bbf4-11ebc1c0bc3b', + }, + repository: { + id: 138865824, + name: 'raulb/atleti', + type: 'github', + }, + }) + + await customRunCommand(Cmd, [`--pipeline=${pipeline.name}`]) + + expect(stdout.output).to.equal('New Test setup outputNew Test output\n✓ #11 my-test-branch:668a5ce succeeded\n') + }) + + describe('when the commit is not in the remote repository', function () { + it('it runs the test and displays the test output for the first node', async function () { + nock('https://api.heroku.com') + .get(`/pipelines?eq[name]=${pipeline.name}`) + .reply(200, [ + {id: pipeline.id}, + ]) + .post('/test-runs') + .reply(200, newTestRun) + .get(`/pipelines/${pipeline.id}/test-runs/${newTestRun.number}`) + .reply(200, newTestRun) + .get(`/test-runs/${newTestRun.id}/test-nodes`) + .times(2) + .reply(200, [ + { + commit_branch: newTestRun.commit_branch, + commit_message: newTestRun.commit_message, + commit_sha: newTestRun.commit_sha, + exit_code: 0, + id: newTestRun.id, + number: newTestRun.number, + output_stream_url: `https://test-output.heroku.com/streams/${newTestRun.id.slice(0, 3)}/test-runs/${newTestRun.id}`, + pipeline: {id: pipeline.id}, + setup_stream_url: `https://test-setup-output.heroku.com/streams/${newTestRun.id.slice(0, 3)}/test-runs/${newTestRun.id}`, + status: newTestRun.status, + }, + ]) + .post('/sources') + .reply(200, {source_blob: {put_url: 'https://aws-puturl', get_url: 'https://aws-geturl'}}) + + nock('https://test-setup-output.heroku.com/streams') + .get(`/${newTestRun.id.slice(0, 3)}/test-runs/${newTestRun.id}`) + .reply(200, 'New Test setup output') + + nock('https://test-output.heroku.com/streams') + .get(`/${newTestRun.id.slice(0, 3)}/test-runs/${newTestRun.id}`) + .reply(200, 'New Test output') + + nock('https://kolkrabbi.heroku.com') + .get(`/pipelines/${pipeline.id}/repository`) + .reply(200, { + ci: true, + organization: {id: 'e037ed63-5781-48ee-b2b7-8c55c571b63e'}, + owner: { + github: {user_id: 306015}, + heroku: {user_id: '463147bf-d572-41cf-bbf4-11ebc1c0bc3b'}, + id: '463147bf-d572-41cf-bbf4-11ebc1c0bc3b', + }, + repository: { + id: 138865824, + name: 'raulb/atleti', + type: 'github', + }, + }) + + await customRunCommand(Cmd, [`--pipeline=${pipeline.name}`]) + + expect(stdout.output).to.equal('New Test setup outputNew Test output\n✓ #11 my-test-branch:668a5ce succeeded\n') + }) + }) + }) +}) diff --git a/packages/cli/test/unit/commands/ci/run.unit.test.ts.skip b/packages/cli/test/unit/commands/ci/run.unit.test.ts.skip deleted file mode 100644 index 16021f624a..0000000000 --- a/packages/cli/test/unit/commands/ci/run.unit.test.ts.skip +++ /dev/null @@ -1,248 +0,0 @@ -import {expect, test} from '@oclif/test' -import {promises as fs} from 'fs' -import {PassThrough} from 'node:stream' - -import * as git from '../../../../src/lib/ci/git.js' -import got from 'got' - -describe('ci:run', function () { - test - .command(['ci:run']) - .catch(error => { - expect(error.message).to.contain('Required flag: --pipeline PIPELINE or --app APP') - }) - .it('errors when not specifying a pipeline or an app') - - describe('when specifying a pipeline', function () { - const pipeline = {id: '14402644-c207-43aa-9bc1-974a34914010', name: 'pipeline'} - const ghRepository = { - user: 'heroku-fake', repo: 'my-repo', ref: '668a5ce22eefc7b67c84c1cfe3a766f1958e0add', branch: 'my-test-branch', - } - const newTestRun = { - commit_branch: ghRepository.branch, - commit_message: 'latest commit', - commit_sha: ghRepository.ref, - id: 'b6512323-3a11-43ac-b4e4-9668b6a6b30c', - number: 11, - pipeline: {id: pipeline.id}, - status: 'succeeded', - } - - const gitFake = { - readCommit: () => ({branch: ghRepository.branch, ref: ghRepository.ref}), - remoteFromGitConfig: () => Promise.resolve('heroku'), - getBranch: () => Promise.resolve(ghRepository.branch), - getRef: () => Promise.resolve(ghRepository.ref), - getCommitTitle: () => Promise.resolve(`pushed to ${ghRepository.branch}`), - githubRepository: () => Promise.resolve({user: ghRepository.user, repo: ghRepository.repo}), - createArchive: () => Promise.resolve('new-archive.tgz'), - spawn: () => Promise.resolve(), - urlExists: () => Promise.resolve(), - exec(args: any) { - switch (args.join(' ')) { - case 'remote': { - return Promise.resolve('heroku') - } - - default: { - return Promise.resolve() - } - } - }, - } - - const fsFake = { - stat: () => Promise.resolve({size: 500}), - createReadStream: () => ({ - pipe(dest: any) { - // Simulate a readable stream that properly pipes to destination - if (dest && typeof dest.once === 'function') { - dest.once('response', () => {}) - } - - return dest - }, - once() {}, - on() {}, - }), - } - - const gotFake = { - stream: {put() { - const stream = new PassThrough() - // Simulate HTTP response by emitting 'response' event - setImmediate(() => { - stream.emit('response') - }) - return stream - }}, - } - - test - .stdout() - .nock('https://api.heroku.com', api => { - api.get(`/pipelines?eq[name]=${pipeline.name}`) - .reply(200, [ - {id: pipeline.id}, - ]) - - api.post('/test-runs') - .reply(200, newTestRun) - - api.get(`/pipelines/${pipeline.id}/test-runs/${newTestRun.number}`) - .reply(200, newTestRun) - - api.get(`/test-runs/${newTestRun.id}/test-nodes`) - .times(2) - .reply(200, [ - { - commit_branch: newTestRun.commit_branch, - commit_message: newTestRun.commit_message, - commit_sha: newTestRun.commit_sha, - id: newTestRun.id, - number: newTestRun.number, - pipeline: {id: pipeline.id}, - exit_code: 0, - status: newTestRun.status, - setup_stream_url: `https://test-setup-output.heroku.com/streams/${newTestRun.id.slice(0, 3)}/test-runs/${newTestRun.id}`, - output_stream_url: `https://test-output.heroku.com/streams/${newTestRun.id.slice(0, 3)}/test-runs/${newTestRun.id}`, - }, - ]) - - api.post('/sources') - .reply(200, {source_blob: {put_url: 'https://aws-puturl', get_url: 'https://aws-geturl'}}) - }) - .nock('https://test-setup-output.heroku.com/streams', testOutputAPI => { - testOutputAPI.get(`/${newTestRun.id.slice(0, 3)}/test-runs/${newTestRun.id}`) - .reply(200, 'New Test setup output') - }) - .nock('https://test-output.heroku.com/streams', testOutputAPI => { - testOutputAPI.get(`/${newTestRun.id.slice(0, 3)}/test-runs/${newTestRun.id}`) - .reply(200, 'New Test output') - }) - .nock('https://kolkrabbi.heroku.com', kolkrabbiAPI => { - kolkrabbiAPI.get(`/pipelines/${pipeline.id}/repository`) - .reply(200, { - ci: true, - organization: {id: 'e037ed63-5781-48ee-b2b7-8c55c571b63e'}, - owner: { - id: '463147bf-d572-41cf-bbf4-11ebc1c0bc3b', - heroku: { - user_id: '463147bf-d572-41cf-bbf4-11ebc1c0bc3b'}, - github: {user_id: 306015}, - }, - repository: { - id: 138865824, - name: 'raulb/atleti', - type: 'github', - }, - }) - }) - .stub(git, 'readCommit', gitFake.readCommit) - .stub(git, 'githubRepository', gitFake.githubRepository) - .stub(git, 'createArchive', gitFake.createArchive) - .stub(fs, 'stat', fsFake.stat) - .stub(fs, 'createReadStream', () => { - const stream = new PassThrough() - // Optionally, write some data and end the stream to simulate file contents - stream.end('fake archive data') - return stream - }) - .stub( - got, - 'stream', - // disable below is due to incomplete type definition of `stub` - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore-next-line - gotFake.stream, - ) - .command(['ci:run', `--pipeline=${pipeline.name}`]) - .it('it runs the test and displays the test output for the first node', ({stdout}) => { - expect(stdout).to.equal('New Test setup outputNew Test output\n✓ #11 my-test-branch:668a5ce succeeded\n') - }) - - describe('when the commit is not in the remote repository', function () { - test - .stdout() - .nock('https://api.heroku.com', api => { - api.get(`/pipelines?eq[name]=${pipeline.name}`) - .reply(200, [ - {id: pipeline.id}, - ]) - - api.post('/test-runs') - .reply(200, newTestRun) - - api.get(`/pipelines/${pipeline.id}/test-runs/${newTestRun.number}`) - .reply(200, newTestRun) - - api.get(`/test-runs/${newTestRun.id}/test-nodes`) - .times(2) - .reply(200, [ - { - commit_branch: newTestRun.commit_branch, - commit_message: newTestRun.commit_message, - commit_sha: newTestRun.commit_sha, - id: newTestRun.id, - number: newTestRun.number, - pipeline: {id: pipeline.id}, - exit_code: 0, - status: newTestRun.status, - setup_stream_url: `https://test-setup-output.heroku.com/streams/${newTestRun.id.slice(0, 3)}/test-runs/${newTestRun.id}`, - output_stream_url: `https://test-output.heroku.com/streams/${newTestRun.id.slice(0, 3)}/test-runs/${newTestRun.id}`, - }, - ]) - - api.post('/sources') - .reply(200, {source_blob: {put_url: 'https://aws-puturl', get_url: 'https://aws-geturl'}}) - }) - .nock('https://test-setup-output.heroku.com/streams', testOutputAPI => { - testOutputAPI.get(`/${newTestRun.id.slice(0, 3)}/test-runs/${newTestRun.id}`) - .reply(200, 'New Test setup output') - }) - .nock('https://test-output.heroku.com/streams', testOutputAPI => { - testOutputAPI.get(`/${newTestRun.id.slice(0, 3)}/test-runs/${newTestRun.id}`) - .reply(200, 'New Test output') - }) - .nock('https://kolkrabbi.heroku.com', kolkrabbiAPI => { - kolkrabbiAPI.get(`/pipelines/${pipeline.id}/repository`) - .reply(200, { - ci: true, - organization: {id: 'e037ed63-5781-48ee-b2b7-8c55c571b63e'}, - owner: { - id: '463147bf-d572-41cf-bbf4-11ebc1c0bc3b', - heroku: { - user_id: '463147bf-d572-41cf-bbf4-11ebc1c0bc3b'}, - github: {user_id: 306015}, - }, - repository: { - id: 138865824, - name: 'raulb/atleti', - type: 'github', - }, - }) - }) - .stub(git, 'readCommit', gitFake.readCommit) - .stub(git, 'githubRepository', gitFake.githubRepository) - .stub(git, 'createArchive', gitFake.createArchive) - .stub(fs, 'stat', fsFake.stat) - .stub(fs, 'createReadStream', () => { - const stream = new PassThrough() - stream.end('fake archive data') - return stream - }) - .stub( - got, - 'stream', - // disable below is due to incomplete type definition of `stub` - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore-next-line - gotFake.stream, - ) - .command(['ci:run', `--pipeline=${pipeline.name}`]) - .it('it runs the test and displays the test output for the first node', ({stdout}) => { - expect(stdout).to.equal('New Test setup outputNew Test output\n✓ #11 my-test-branch:668a5ce succeeded\n') - }) - }) - }) -}) From fa2b0204a8c22c5c09aecd2aaff3e3cd50657502 Mon Sep 17 00:00:00 2001 From: Eric Black Date: Wed, 7 Jan 2026 11:31:22 -0800 Subject: [PATCH 11/14] test: migrate certs/generate to @oclif/test v4 - Convert from chained API to async/await with runCommand() - Replace .nock().command().it() with async setup + await runCommand() - Update expect() to use chai instead of @oclif/test - Fix test isolation by adding nock.cleanAll() before test-specific mocks - All 9 tests passing --- ...rate.unit.test.ts.skip => generate.unit.test.ts} | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) rename packages/cli/test/unit/commands/certs/{generate.unit.test.ts.skip => generate.unit.test.ts} (99%) diff --git a/packages/cli/test/unit/commands/certs/generate.unit.test.ts.skip b/packages/cli/test/unit/commands/certs/generate.unit.test.ts similarity index 99% rename from packages/cli/test/unit/commands/certs/generate.unit.test.ts.skip rename to packages/cli/test/unit/commands/certs/generate.unit.test.ts index 0931a3dfe7..00f17addfd 100644 --- a/packages/cli/test/unit/commands/certs/generate.unit.test.ts.skip +++ b/packages/cli/test/unit/commands/certs/generate.unit.test.ts @@ -1,12 +1,12 @@ -import Cmd from '../../../../src/commands/certs/generate.js' -import {stdout, stderr} from 'stdout-stderr' -import runCommand from '../../../helpers/runCommand.js' +import {expect} from 'chai' import nock from 'nock' -import {endpoint} from '../../../helpers/stubs/sni-endpoints.js' import * as sinon from 'sinon' - -import {expect} from '@oclif/test' import {SinonStub} from 'sinon' +import {stdout, stderr} from 'stdout-stderr' + +import Cmd from '../../../../src/commands/certs/generate.js' +import runCommand from '../../../helpers/runCommand.js' +import {endpoint} from '../../../helpers/stubs/sni-endpoints.js' describe('heroku certs:generate', function () { let promptForOwnerInfoStub: SinonStub @@ -27,6 +27,7 @@ describe('heroku certs:generate', function () { afterEach(function () { promptForOwnerInfoStub.restore() spawnOpenSSLStub.restore() + nock.cleanAll() }) it('# with certificate prompts emitted if no parts of subject provided', async function () { From ad39c269d82fdff3ab838475e0833f6b5cbeed74 Mon Sep 17 00:00:00 2001 From: Eric Black Date: Wed, 7 Jan 2026 11:34:05 -0800 Subject: [PATCH 12/14] test: migrate certs/add to @oclif/test v4 - Update imports to use chai instead of @oclif/test - Already using runCommand() helper and async/await pattern - All 10 tests passing --- ...add.unit.test.ts.skip => add.unit.test.ts} | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) rename packages/cli/test/unit/commands/certs/{add.unit.test.ts.skip => add.unit.test.ts} (99%) diff --git a/packages/cli/test/unit/commands/certs/add.unit.test.ts.skip b/packages/cli/test/unit/commands/certs/add.unit.test.ts similarity index 99% rename from packages/cli/test/unit/commands/certs/add.unit.test.ts.skip rename to packages/cli/test/unit/commands/certs/add.unit.test.ts index 7fb1647e57..1826065b10 100644 --- a/packages/cli/test/unit/commands/certs/add.unit.test.ts.skip +++ b/packages/cli/test/unit/commands/certs/add.unit.test.ts @@ -1,21 +1,20 @@ -import {stdout, stderr} from 'stdout-stderr' -import runCommand from '../../../helpers/runCommand.js' +import {expect} from 'chai' import nock from 'nock' - -import sinon from 'sinon' -import {SinonStub} from 'sinon' +import sinon, {SinonStub} from 'sinon' +import {stderr, stdout} from 'stdout-stderr' import tsheredoc from 'tsheredoc' + +import runCommand from '../../../helpers/runCommand.js' const heredoc = tsheredoc.default +import Cmd from '../../../../src/commands/certs/add.js' +import {CertAndKeyManager} from '../../../../src/lib/certs/get_cert_and_key.js' import { + certificateDetails, endpoint, + endpointHeroku, endpointStables, endpointWildcard, - certificateDetails, - endpointHeroku, } from '../../../helpers/stubs/sni-endpoints.js' -import Cmd from '../../../../src/commands/certs/add.js' -import {CertAndKeyManager} from '../../../../src/lib/certs/get_cert_and_key.js' -import {expect} from '@oclif/test' describe('heroku certs:add', function () { let stubbedSelectDomainsReturnValue: {domains: string[]} = {domains: []} From 2f304e1e0d6f814c27594685f5371e8c2c326af1 Mon Sep 17 00:00:00 2001 From: Eric Black Date: Wed, 7 Jan 2026 11:47:26 -0800 Subject: [PATCH 13/14] fix: export shared gitService instance to fix test stubbing The issue was that src/commands/ci/run.ts was creating its own gitService instance, while tests were stubbing the gitService instance from source.ts. This caused the stubs to not affect the actual command execution. Solution: - Export a shared gitService instance from src/lib/ci/git.ts - Update both run.ts and source.ts to import the shared instance - Update tests to import gitService from git.ts directly This ensures tests stub the same instance used by the commands. --- packages/cli/src/commands/ci/run.ts | 4 +--- packages/cli/src/lib/ci/git.ts | 3 +++ packages/cli/src/lib/ci/source.ts | 3 +-- packages/cli/test/unit/commands/ci/rerun.unit.test.ts | 3 ++- packages/cli/test/unit/commands/ci/run.unit.test.ts | 3 ++- 5 files changed, 9 insertions(+), 7 deletions(-) diff --git a/packages/cli/src/commands/ci/run.ts b/packages/cli/src/commands/ci/run.ts index 8c9679aaab..f4bdc22743 100644 --- a/packages/cli/src/commands/ci/run.ts +++ b/packages/cli/src/commands/ci/run.ts @@ -3,13 +3,11 @@ import * as Heroku from '@heroku-cli/schema' import {ux} from '@oclif/core' import * as Kolkrabbi from '../../lib/ci/interfaces/kolkrabbi.js' -import {GitService} from '../../lib/ci/git.js' +import {gitService} from '../../lib/ci/git.js' import {getPipeline} from '../../lib/ci/pipelines.js' import {createSourceBlob} from '../../lib/ci/source.js' import {displayAndExit} from '../../lib/ci/test-run.js' -const gitService = new GitService() - export default class CiRun extends Command { static description = 'run tests against current directory' diff --git a/packages/cli/src/lib/ci/git.ts b/packages/cli/src/lib/ci/git.ts index b0b6532542..050946630d 100644 --- a/packages/cli/src/lib/ci/git.ts +++ b/packages/cli/src/lib/ci/git.ts @@ -158,6 +158,9 @@ export class GitService { } } +// Export a shared instance for use across commands +export const gitService = new GitService() + export { createArchive, createRemote, diff --git a/packages/cli/src/lib/ci/source.ts b/packages/cli/src/lib/ci/source.ts index 6978a1fa40..84023ef0a1 100644 --- a/packages/cli/src/lib/ci/source.ts +++ b/packages/cli/src/lib/ci/source.ts @@ -3,10 +3,9 @@ import debug from 'debug' import {createReadStream, promises as fs} from 'fs' import {got} from 'got' -import {GitService} from './git.js' +import {gitService} from './git.js' const ciDebug = debug('ci') -const gitService = new GitService() // FileService class for easier testing/stubbing export class FileService { diff --git a/packages/cli/test/unit/commands/ci/rerun.unit.test.ts b/packages/cli/test/unit/commands/ci/rerun.unit.test.ts index 9a70fb083b..0b87bde965 100644 --- a/packages/cli/test/unit/commands/ci/rerun.unit.test.ts +++ b/packages/cli/test/unit/commands/ci/rerun.unit.test.ts @@ -2,7 +2,8 @@ import {expect} from 'chai' import nock from 'nock' import sinon from 'sinon' import {PassThrough} from 'node:stream' -import {gitService, fileService} from '../../../../src/lib/ci/source.js' +import {gitService} from '../../../../src/lib/ci/git.js' +import {fileService} from '../../../../src/lib/ci/source.js' import {got} from 'got' import customRunCommand from '../../../helpers/runCommand.js' import Cmd from '../../../../src/commands/ci/rerun.js' diff --git a/packages/cli/test/unit/commands/ci/run.unit.test.ts b/packages/cli/test/unit/commands/ci/run.unit.test.ts index d46f6c0e9f..9be7d57b63 100644 --- a/packages/cli/test/unit/commands/ci/run.unit.test.ts +++ b/packages/cli/test/unit/commands/ci/run.unit.test.ts @@ -6,7 +6,8 @@ import sinon from 'sinon' import {stdout} from 'stdout-stderr' import Cmd from '../../../../src/commands/ci/run.js' -import {fileService, gitService} from '../../../../src/lib/ci/source.js' +import {gitService} from '../../../../src/lib/ci/git.js' +import {fileService} from '../../../../src/lib/ci/source.js' import customRunCommand from '../../../helpers/runCommand.js' describe('ci:run', function () { From ba3322032b0aaee7c03740a021906850c52b37cb Mon Sep 17 00:00:00 2001 From: Eric Black Date: Mon, 12 Jan 2026 12:07:42 -0800 Subject: [PATCH 14/14] update with test conventions --- .../test/unit/commands/certs/add.unit.test.ts | 164 +++++++++--------- .../unit/commands/certs/generate.unit.test.ts | 17 +- .../unit/commands/ci/config/get.unit.test.ts | 19 +- .../commands/ci/config/index.unit.test.ts | 24 ++- .../unit/commands/ci/config/set.unit.test.ts | 32 ++-- .../commands/ci/config/unset.unit.test.ts | 28 +-- .../test/unit/commands/ci/index.unit.test.ts | 46 +++-- .../test/unit/commands/ci/info.unit.test.ts | 74 ++++---- .../test/unit/commands/ci/last.unit.test.ts | 34 ++-- .../commands/ci/migrate-manifest.unit.test.ts | 140 +++++++-------- .../test/unit/commands/ci/rerun.unit.test.ts | 60 ++++--- .../test/unit/commands/ci/run.unit.test.ts | 20 ++- 12 files changed, 372 insertions(+), 286 deletions(-) diff --git a/packages/cli/test/unit/commands/certs/add.unit.test.ts b/packages/cli/test/unit/commands/certs/add.unit.test.ts index 1826065b10..7d341bf8a0 100644 --- a/packages/cli/test/unit/commands/certs/add.unit.test.ts +++ b/packages/cli/test/unit/commands/certs/add.unit.test.ts @@ -20,15 +20,17 @@ describe('heroku certs:add', function () { let stubbedSelectDomainsReturnValue: {domains: string[]} = {domains: []} let stubbedSelectDomains: SinonStub let stubbedGetCertAndKey: SinonStub + let api: nock.Scope function mockDomains() { - nock('https://api.heroku.com') + api .get('/apps/example/domains') .reply(200, []) stubbedSelectDomainsReturnValue = {domains: []} } beforeEach(async function () { + api = nock('https://api.heroku.com') stubbedSelectDomains = sinon.stub(Cmd.prototype, 'selectDomains') // eslint-disable-next-line arrow-body-style stubbedSelectDomains.callsFake(async (domainOptions: string[]) => { @@ -41,19 +43,17 @@ describe('heroku certs:add', function () { crt: Buffer.from('pem content'), key: Buffer.from('key content'), })) - nock.cleanAll() }) afterEach(function () { sinon.restore() + api.done() + nock.cleanAll() }) it('# works with a cert and key', async function () { - nock('https://api.heroku.com') - .get('/apps/example') - .reply(200, {space: null}) mockDomains() - const mockSni = nock('https://api.heroku.com') + api .post('/apps/example/sni-endpoints', { certificate_chain: 'pem content', private_key: 'key content', }) @@ -64,17 +64,13 @@ describe('heroku certs:add', function () { 'pem_file', 'key_file', ]) - mockSni.done() expect(stderr.output).to.contain('Adding SSL certificate to example... done\n') expect(stdout.output).to.equal(`Certificate details:\n${heredoc(certificateDetails)}`) }) it('# creates an SNI endpoint', async function () { - nock('https://api.heroku.com') - .get('/apps/example') - .reply(200, {space: null}) mockDomains() - const mock = nock('https://api.heroku.com') + api .post('/apps/example/sni-endpoints', { certificate_chain: 'pem content', private_key: 'key content', }) @@ -85,20 +81,15 @@ describe('heroku certs:add', function () { 'pem_file', 'key_file', ]) - mock.done() expect(stderr.output).to.contain('Adding SSL certificate to example... done\n') expect(stdout.output).to.eq(`Certificate details:\n${heredoc(certificateDetails)}`) }) it('# shows the configure prompt', async function () { - nock('https://api.heroku.com') - .get('/apps/example') - .reply(200, {space: null}) - nock('https://api.heroku.com') + api .get('/apps/example/domains') - .reply(200, [{id: 123, hostname: 'example.org'}]) - mockDomains() - const mockSni = nock('https://api.heroku.com') + .reply(200, [{hostname: 'example.org', id: 123}]) + api .post('/apps/example/sni-endpoints', { certificate_chain: 'pem content', private_key: 'key content', }) @@ -109,39 +100,32 @@ describe('heroku certs:add', function () { 'pem_file', 'key_file', ]) - mockSni.done() expect(stderr.output).to.contain('Adding SSL certificate to example... done\n') expect(stdout.output).to.eq(`Certificate details:\n${heredoc(certificateDetails)}=== Almost done! Which of these domains on this application would you like this certificate associated with?\n\n`) }) describe('stable cnames', function () { - beforeEach(async function () { - nock('https://api.heroku.com') - .get('/apps/example') - .reply(200, {space: null}) - }) - it('# prompts creates an SNI endpoint with stable cnames', async function () { - const mock = nock('https://api.heroku.com') + api .post('/apps/example/sni-endpoints', { certificate_chain: 'pem content', private_key: 'key content', }) .reply(200, endpointStables) - const domainsMock = nock('https://api.heroku.com') + api .get('/apps/example/domains') .reply(200, [ - {kind: 'custom', hostname: 'biz.example.com', cname: 'biz.example.com.herokudns.com'}, { - kind: 'custom', - hostname: 'baz.example.org', + {cname: 'biz.example.com.herokudns.com', hostname: 'biz.example.com', kind: 'custom'}, { cname: 'baz.example.org.herokudns.com', - }, {kind: 'custom', hostname: 'example.org', cname: 'example.org.herokudns.com'}, { + hostname: 'baz.example.org', kind: 'custom', - hostname: 'example.co.uk', + }, {cname: 'example.org.herokudns.com', hostname: 'example.org', kind: 'custom'}, { cname: 'example.co.uk.herokudns.com', - }, {kind: 'heroku', hostname: 'haiku.herokuapp.com', cname: 'haiku.herokuapp.com'}, + hostname: 'example.co.uk', + kind: 'custom', + }, {cname: 'haiku.herokuapp.com', hostname: 'haiku.herokuapp.com', kind: 'heroku'}, ]) - const domainsCreate = nock('https://api.heroku.com') + api .patch('/apps/example/domains/biz.example.com') .reply(200) @@ -156,28 +140,25 @@ describe('heroku certs:add', function () { expect(stubbedSelectDomains.firstCall.args[0]).to.eql([ 'biz.example.com', ]) - mock.done() - domainsMock.done() - domainsCreate.done() expect(stderr.output).to.contain('Adding SSL certificate to example... done\n') expect(stdout.output.trim()).to.equal('Certificate details:\nCommon Name(s): foo.example.org\n bar.example.org\n biz.example.com\nExpires At: 2013-08-01 21:34 UTC\nIssuer: /C=US/ST=California/L=San Francisco/O=Heroku by Salesforce/CN=secure.example.org\nStarts At: 2012-08-01 21:34 UTC\nSubject: /C=US/ST=California/L=San Francisco/O=Heroku by Salesforce/CN=secure.example.org\nSSL certificate is self signed.\n=== Almost done! Which of these domains on this application would you like this certificate associated with?') }) it('# does not error out if the cert CN is for the heroku domain', async function () { - const mock = nock('https://api.heroku.com') + api .post('/apps/example/sni-endpoints', { certificate_chain: 'pem content', private_key: 'key content', }) .reply(200, endpointHeroku) - const domainsMock = nock('https://api.heroku.com') + api .get('/apps/example/domains') .reply(200, [ - {kind: 'heroku', hostname: 'tokyo-1050.herokuapp.com', cname: null}, + {cname: null, hostname: 'tokyo-1050.herokuapp.com', kind: 'heroku'}, ]) - const domainsMockPatch = nock('https://api.heroku.com') + api .patch('/apps/example/domains/tokyo-1050.herokuapp.com') .reply(200, [ - {kind: 'heroku', hostname: 'tokyo-1050.herokuapp.com', cname: null}, + {cname: null, hostname: 'tokyo-1050.herokuapp.com', kind: 'heroku'}, ]) stubbedSelectDomainsReturnValue = {domains: ['tokyo-1050.herokuapp.com']} @@ -190,9 +171,6 @@ describe('heroku certs:add', function () { expect(stubbedSelectDomains.firstCall.args[0]).to.eql([ 'tokyo-1050.herokuapp.com', ]) - mock.done() - domainsMock.done() - domainsMockPatch.done() expect(stderr.output).to.contain('Adding SSL certificate to example... done\n') expect(stdout.output.trim()).to.equal('Certificate details:\nCommon Name(s): tokyo-1050.herokuapp.com\nExpires At: 2013-08-01 21:34 UTC\nIssuer: /C=US/ST=California/L=San Francisco/O=Heroku by Salesforce/CN=heroku.com\nStarts At: 2012-08-01 21:34 UTC\nSubject: /C=US/ST=California/L=San Francisco/O=Heroku by Salesforce/CN=tokyo-1050.herokuapp.com\nSSL certificate is not trusted.\n=== Almost done! Which of these domains on this application would you like this certificate associated with?') }) @@ -206,10 +184,10 @@ describe('heroku certs:add', function () { const domainsMock = nock('https://api.heroku.com') .get('/apps/example/domains') .reply(200, [ - {kind: 'custom', hostname: '*.example.org', cname: 'wildcard.example.org.herokudns.com'}, { - kind: 'custom', - hostname: '*.example.com', + {cname: 'wildcard.example.org.herokudns.com', hostname: '*.example.org', kind: 'custom'}, { cname: 'wildcard.example.com.herokudns.com', + hostname: '*.example.com', + kind: 'custom', }, ]) stubbedSelectDomainsReturnValue = {domains: ['tokyo-1050.herokuapp.com']} @@ -259,10 +237,10 @@ describe('heroku certs:add', function () { const domainsMock = nock('https://api.heroku.com') .get('/apps/example/domains') .reply(200, [ - {kind: 'custom', hostname: 'foo.example.org', cname: 'foo.example.org.herokudns.com'}, { - kind: 'custom', - hostname: 'bar.example.com', + {cname: 'foo.example.org.herokudns.com', hostname: 'foo.example.org', kind: 'custom'}, { cname: 'bar.example.com.herokudns.com', + hostname: 'bar.example.com', + kind: 'custom', }, ]) const domainsMockPatch = nock('https://api.heroku.com') @@ -307,55 +285,75 @@ describe('heroku certs:add', function () { const domainsMock = nock('https://api.heroku.com') .get('/apps/example/domains') .reply(200, [ - {kind: 'heroku', hostname: 'tokyo-1050.herokuapp.com', cname: null, status: 'none'}, { - kind: 'custom', - hostname: 'foo.example.org', + { cname: null, + hostname: 'tokyo-1050.herokuapp.com', + kind: 'heroku', status: 'none', - }, {kind: 'custom', hostname: 'bar.example.org', cname: null, status: 'none'}, { + }, { + cname: null, + hostname: 'foo.example e.org', kind: 'custom', - hostname: 'biz.example.com', + status: 'none', + }, { cname: null, + hostname: 'bar.example.org', + kind: 'custom', + status: 'none', + }, { + cname: null, + hostname: 'biz.example.com', + kind: 'custom', status: 'none', }, ]) const domainsRetry = nock('https://api.heroku.com') .get('/apps/example/domains') .reply(200, [ - {kind: 'heroku', hostname: 'tokyo-1050.herokuapp.com', cname: null, status: 'none'}, { - kind: 'custom', - hostname: 'foo.example.org', + { cname: null, + hostname: 'tokyo-1050.herokuapp.com', + kind: 'heroku', status: 'none', }, { + cname: null, + hostname: 'foo.example.org', kind: 'custom', - hostname: 'bar.example.org', + status: 'none', + }, { cname: 'bar.example.org.herokudns.com', + hostname: 'bar.example.org', + kind: 'custom', status: 'succeeded', }, { - kind: 'custom', - hostname: 'biz.example.com', cname: 'biz.example.com.herokudns.com', + hostname: 'biz.example.com', + kind: 'custom', status: 'succeeded', }, ]) const domainsSuccess = nock('https://api.heroku.com') .get('/apps/example/domains') .reply(200, [ - {kind: 'heroku', hostname: 'tokyo-1050.herokuapp.com', cname: null, status: 'none'}, { - kind: 'custom', - hostname: 'foo.example.org', + { + cname: null, + hostname: 'tokyo-1050.herokuapp.com', + kind: 'heroku', + status: 'none', + }, { cname: 'foo.example.org.herokudns.com', + hostname: 'foo.example.org', + kind: 'custom', status: 'succeeded', }, { - kind: 'custom', - hostname: 'bar.example.org', cname: 'bar.example.org.herokudns.com', + hostname: 'bar.example.org', + kind: 'custom', status: 'succeeded', }, { - kind: 'custom', - hostname: 'biz.example.com', cname: 'biz.example.com.herokudns.com', + hostname: 'biz.example.com', + kind: 'custom', status: 'succeeded', }, ]) @@ -400,24 +398,34 @@ describe('heroku certs:add', function () { }) it('# tries 30 times and then gives up', async function () { - const mock = nock('https://api.heroku.com') + api .post('/apps/example/sni-endpoints', { certificate_chain: 'pem content', private_key: 'key content', }) .reply(200, endpointStables) - const domainsMock = nock('https://api.heroku.com') + api .get('/apps/example/domains') .times(30) .reply(200, [ - {kind: 'heroku', hostname: 'tokyo-1050.herokuapp.com', cname: null, status: 'none'}, { - kind: 'custom', - hostname: 'foo.example.org', + { cname: null, + hostname: 'tokyo-1050.herokuapp.com', + kind: 'heroku', status: 'none', - }, {kind: 'custom', hostname: 'bar.example.org', cname: null, status: 'none'}, { + }, { + cname: null, + hostname: 'foo.example.org', kind: 'custom', - hostname: 'biz.example.com', + status: 'none', + }, { cname: null, + hostname: 'bar.example.org', + kind: 'custom', + status: 'none', + }, { + cname: null, + hostname: 'biz.example.com', + kind: 'custom', status: 'none', }, ]) @@ -433,8 +441,6 @@ describe('heroku certs:add', function () { expect(message).to.contain('Timed out while waiting for stable domains to be created') } - mock.done() - domainsMock.done() expect(stderr.output).to.contain('Adding SSL certificate to example... done') expect(stderr.output).to.contain('Waiting for stable domains to be created... !') expect(stdout.output).to.equal('Certificate details:\nCommon Name(s): foo.example.org\n bar.example.org\n biz.example.com\nExpires At: 2013-08-01 21:34 UTC\nIssuer: /C=US/ST=California/L=San Francisco/O=Heroku by Salesforce/CN=secure.example.org\nStarts At: 2012-08-01 21:34 UTC\nSubject: /C=US/ST=California/L=San Francisco/O=Heroku by Salesforce/CN=secure.example.org\nSSL certificate is self signed.\n') diff --git a/packages/cli/test/unit/commands/certs/generate.unit.test.ts b/packages/cli/test/unit/commands/certs/generate.unit.test.ts index 00f17addfd..8c16336af4 100644 --- a/packages/cli/test/unit/commands/certs/generate.unit.test.ts +++ b/packages/cli/test/unit/commands/certs/generate.unit.test.ts @@ -1,8 +1,7 @@ import {expect} from 'chai' import nock from 'nock' -import * as sinon from 'sinon' -import {SinonStub} from 'sinon' -import {stdout, stderr} from 'stdout-stderr' +import sinon, {SinonStub} from 'sinon' +import {stderr, stdout} from 'stdout-stderr' import Cmd from '../../../../src/commands/certs/generate.js' import runCommand from '../../../helpers/runCommand.js' @@ -11,9 +10,11 @@ import {endpoint} from '../../../helpers/stubs/sni-endpoints.js' describe('heroku certs:generate', function () { let promptForOwnerInfoStub: SinonStub let spawnOpenSSLStub: SinonStub + let api: nock.Scope beforeEach(function () { - nock('https://api.heroku.com') + api = nock('https://api.heroku.com') + api .get('/apps/example/sni-endpoints') .reply(200, [endpoint]) @@ -27,11 +28,17 @@ describe('heroku certs:generate', function () { afterEach(function () { promptForOwnerInfoStub.restore() spawnOpenSSLStub.restore() + api.done() nock.cleanAll() }) it('# with certificate prompts emitted if no parts of subject provided', async function () { - promptForOwnerInfoStub.returns(Promise.resolve({owner: 'Heroku', country: 'US', area: 'California', city: 'San Francisco'})) + promptForOwnerInfoStub.returns(Promise.resolve({ + area: 'California', + city: 'San Francisco', + country: 'US', + owner: 'Heroku', + })) await runCommand(Cmd, [ '--app', diff --git a/packages/cli/test/unit/commands/ci/config/get.unit.test.ts b/packages/cli/test/unit/commands/ci/config/get.unit.test.ts index f509e65800..026a6368ea 100644 --- a/packages/cli/test/unit/commands/ci/config/get.unit.test.ts +++ b/packages/cli/test/unit/commands/ci/config/get.unit.test.ts @@ -10,10 +10,19 @@ const pipeline = { } describe('heroku ci:config:get', function () { - afterEach(() => nock.cleanAll()) + let api: nock.Scope - it('displays the config value', async () => { - nock('https://api.heroku.com') + beforeEach(function () { + api = nock('https://api.heroku.com') + }) + + afterEach(function () { + api.done() + nock.cleanAll() + }) + + it('displays the config value', async function () { + api .get(`/pipelines/${pipeline.id}`) .reply(200, pipeline) .get(`/pipelines/${pipeline.id}/stage/test/config-vars`) @@ -24,8 +33,8 @@ describe('heroku ci:config:get', function () { expect(stdout).to.equal(`${value}\n`) }) - it('displays config formatted for shell', async () => { - nock('https://api.heroku.com') + it('displays config formatted for shell', async function () { + api .get(`/pipelines/${pipeline.id}`) .reply(200, pipeline) .get(`/pipelines/${pipeline.id}/stage/test/config-vars`) diff --git a/packages/cli/test/unit/commands/ci/config/index.unit.test.ts b/packages/cli/test/unit/commands/ci/config/index.unit.test.ts index c4831bc6b0..97eacb27d2 100644 --- a/packages/cli/test/unit/commands/ci/config/index.unit.test.ts +++ b/packages/cli/test/unit/commands/ci/config/index.unit.test.ts @@ -9,16 +9,24 @@ describe('ci:config', function () { OTHER: 'test', RAILS_ENV: 'test', } + let api: nock.Scope - afterEach(() => nock.cleanAll()) + beforeEach(function () { + api = nock('https://api.heroku.com') + }) + + afterEach(function () { + api.done() + nock.cleanAll() + }) - it('errors when not specifying a pipeline or an app', async () => { + it('errors when not specifying a pipeline or an app', async function () { const {error} = await runCommand(['ci:config']) expect(error?.message).to.contain('Exactly one of the following must be provided: --app, --pipeline') }) - it('displays config when a pipeline is specified', async () => { - nock('https://api.heroku.com') + it('displays config when a pipeline is specified', async function () { + api .get(`/pipelines?eq[name]=${pipeline.name}`) .reply(200, [ { @@ -35,8 +43,8 @@ describe('ci:config', function () { expect(stdout).to.include('KEY1: VALUE1\nOTHER: test\nRAILS_ENV: test\n') }) - it('displays config formatted as JSON', async () => { - nock('https://api.heroku.com') + it('displays config formatted as JSON', async function () { + api .get(`/pipelines?eq[name]=${pipeline.name}`) .reply(200, [ { @@ -52,8 +60,8 @@ describe('ci:config', function () { expect(stdout).to.equal('{\n "KEY1": "VALUE1",\n "OTHER": "test",\n "RAILS_ENV": "test"\n}\n') }) - it('displays config formatted for shell', async () => { - nock('https://api.heroku.com') + it('displays config formatted for shell', async function () { + api .get(`/pipelines?eq[name]=${pipeline.name}`) .reply(200, [ { diff --git a/packages/cli/test/unit/commands/ci/config/set.unit.test.ts b/packages/cli/test/unit/commands/ci/config/set.unit.test.ts index 6f63f3e7de..bdef4e6727 100644 --- a/packages/cli/test/unit/commands/ci/config/set.unit.test.ts +++ b/packages/cli/test/unit/commands/ci/config/set.unit.test.ts @@ -2,18 +2,26 @@ import {runCommand} from '@oclif/test' import {expect} from 'chai' import nock from 'nock' -const key = 'FOO' -const value = 'bar' -const pipeline = { - id: '123e4567-e89b-12d3-a456-426655440000', - name: 'test-pipeline', -} - describe('heroku ci:config:set', function () { - afterEach(() => nock.cleanAll()) + const key = 'FOO' + const value = 'bar' + const pipeline = { + id: '123e4567-e89b-12d3-a456-426655440000', + name: 'test-pipeline', + } + let api: nock.Scope + + beforeEach(function () { + api = nock('https://api.heroku.com') + }) + + afterEach(function () { + api.done() + nock.cleanAll() + }) - it('sets new config', async () => { - nock('https://api.heroku.com') + it('sets new config', async function () { + api .get(`/pipelines/${pipeline.id}`) .reply(200, pipeline) .patch(`/pipelines/${pipeline.id}/stage/test/config-vars`) @@ -25,12 +33,12 @@ describe('heroku ci:config:set', function () { expect(stdout).to.include(value) }) - it('errors with example of valid args', async () => { + it('errors with example of valid args', async function () { const {error} = await runCommand(['ci:config:set', `--pipeline=${pipeline.id}`]) expect(error?.message).to.equal('Usage: heroku ci:config:set KEY1 [KEY2 ...]\nMust specify KEY to set.') }) - it('errors with explanation of required flags', async () => { + it('errors with explanation of required flags', async function () { const {error} = await runCommand(['ci:config:set', '--', `${key}=${value}`]) expect(error?.message).to.include('Exactly one of the following must be provided: --app, --pipeline') }) diff --git a/packages/cli/test/unit/commands/ci/config/unset.unit.test.ts b/packages/cli/test/unit/commands/ci/config/unset.unit.test.ts index aaa17d6e54..97bb875cba 100644 --- a/packages/cli/test/unit/commands/ci/config/unset.unit.test.ts +++ b/packages/cli/test/unit/commands/ci/config/unset.unit.test.ts @@ -2,17 +2,25 @@ import {runCommand} from '@oclif/test' import {expect} from 'chai' import nock from 'nock' -const key = 'FOO' -const pipeline = { - id: '123e4567-e89b-12d3-a456-426655440000', - name: 'test-pipeline', -} - describe('heroku ci:config:unset', function () { - afterEach(() => nock.cleanAll()) + const key = 'FOO' + const pipeline = { + id: '123e4567-e89b-12d3-a456-426655440000', + name: 'test-pipeline', + } + let api: nock.Scope + + beforeEach(function () { + api = nock('https://api.heroku.com') + }) + + afterEach(function () { + api.done() + nock.cleanAll() + }) - it('displays the config value key being unset', async () => { - nock('https://api.heroku.com') + it('displays the config value key being unset', async function () { + api .get(`/pipelines/${pipeline.id}`) .reply(200, pipeline) .patch(`/pipelines/${pipeline.id}/stage/test/config-vars`) @@ -23,7 +31,7 @@ describe('heroku ci:config:unset', function () { expect(stderr).to.contain('Unsetting FOO... done') }) - it('errors with example of valid args', async () => { + it('errors with example of valid args', async function () { const {error} = await runCommand(['ci:config:unset', `--pipeline=${pipeline.id}`]) expect(error?.message).to.equal('Usage: heroku ci:config:unset KEY1 [KEY2 ...]\nMust specify KEY to unset.') }) diff --git a/packages/cli/test/unit/commands/ci/index.unit.test.ts b/packages/cli/test/unit/commands/ci/index.unit.test.ts index 4314292442..c17bd918ef 100644 --- a/packages/cli/test/unit/commands/ci/index.unit.test.ts +++ b/packages/cli/test/unit/commands/ci/index.unit.test.ts @@ -2,15 +2,25 @@ import {runCommand} from '@oclif/test' import {expect} from 'chai' import nock from 'nock' import sinon from 'sinon' + +import Cmd from '../../../../src/commands/ci/index.js' import {PipelineService} from '../../../../src/lib/ci/pipelines.js' -import removeAllWhitespace from '../../../helpers/utils/remove-whitespaces.js' import customRunCommand from '../../../helpers/runCommand.js' -import Cmd from '../../../../src/commands/ci/index.js' +import removeAllWhitespace from '../../../helpers/utils/remove-whitespaces.js' describe('ci', function () { - afterEach(() => nock.cleanAll()) + let api: nock.Scope + + beforeEach(function () { + api = nock('https://api.heroku.com') + }) + + afterEach(function () { + api.done() + nock.cleanAll() + }) - it('errors when not specifying a pipeline or an app', async () => { + it('errors when not specifying a pipeline or an app', async function () { const {error} = await runCommand(['ci']) expect(error?.message).to.contain('Required flag: --pipeline PIPELINE or --app APP') }) @@ -27,9 +37,9 @@ describe('ci', function () { const chosenOption = { pipeline: { + created_at: '05/10/2023', id: '14402644-c207-43aa-9bc1-974a34914010', name: '14402644-c207-43aa-9bc1-974a34914010', - created_at: '05/10/2023', }, } @@ -46,8 +56,8 @@ describe('ci', function () { } }) - it('shows the latest 15 test runs', async () => { - nock('https://api.heroku.com') + it('shows the latest 15 test runs', async function () { + api .get(`/pipelines?eq[name]=${pipeline.name}`) .reply(200, [ { @@ -77,8 +87,8 @@ describe('ci', function () { expect(actual).not.to.contain(removeAllWhitespace(`${testRuns[4].number} ${testRuns[4].commit_sha}`)) }) - it('returns pipeline id', async () => { - nock('https://api.heroku.com') + it('returns pipeline id', async function () { + api .get(`/pipelines/${pipeline.id}`) .reply(200, { @@ -94,8 +104,8 @@ describe('ci', function () { expect(stdout).to.contain(`=== Showing latest test runs for the ${pipeline.id} pipeline`) }) - it('errors if no pipeline is found', async () => { - nock('https://api.heroku.com') + it('errors if no pipeline is found', async function () { + api .get(`/pipelines?eq[name]=${pipeline.name}`) .reply(200, []) @@ -114,24 +124,24 @@ describe('ci', function () { promptStub.restore() }) - it('selects a pipeline from the prompt', async () => { - nock('https://api.heroku.com') + it('selects a pipeline from the prompt', async function () { + api .get(`/pipelines?eq[name]=${pipeline.name}`) .reply(200, [ { + created_at: '05/10/2023', id: pipeline.id, name: pipeline.id, - created_at: '05/10/2023', }, { + created_at: '05/11/2023', id: pipeline.id, name: pipeline.id, - created_at: '05/11/2023', }, { + created_at: '05/12/2023', id: pipeline.id, name: pipeline.id, - created_at: '05/12/2023', }, ]) .get(`/pipelines/${pipeline.id}/test-runs`) @@ -143,8 +153,8 @@ describe('ci', function () { }) }) - it('shows the latest 15 test runs in json', async () => { - nock('https://api.heroku.com') + it('shows the latest 15 test runs in json', async function () { + api .get(`/pipelines?eq[name]=${pipeline.name}`) .reply(200, [ { diff --git a/packages/cli/test/unit/commands/ci/info.unit.test.ts b/packages/cli/test/unit/commands/ci/info.unit.test.ts index 668ed3f744..75c26ce721 100644 --- a/packages/cli/test/unit/commands/ci/info.unit.test.ts +++ b/packages/cli/test/unit/commands/ci/info.unit.test.ts @@ -5,15 +5,23 @@ import nock from 'nock' describe('ci:info', function () { const testRunNumber = 10 const testRun = {id: 'f53d34b4-c3a9-4608-a186-17257cf71d62', number: 10} + let api: nock.Scope - afterEach(() => nock.cleanAll()) + beforeEach(function () { + api = nock('https://api.heroku.com') + }) + + afterEach(function () { + api.done() + nock.cleanAll() + }) - it('errors when not specifying a test run', async () => { + it('errors when not specifying a test run', async function () { const {error} = await runCommand(['ci:info']) expect(error?.message).to.equal('Missing 1 required arg:\ntest-run auto-incremented test run number\nSee more help with --help') }) - it('errors when not specifying a pipeline or an app', async () => { + it('errors when not specifying a pipeline or an app', async function () { const {error} = await runCommand(['ci:info', `${testRun.number}`]) expect(error?.message).to.contain('Required flag: --pipeline PIPELINE or --app APP') }) @@ -21,8 +29,8 @@ describe('ci:info', function () { describe('when specifying a pipeline', function () { const pipeline = {id: '14402644-c207-43aa-9bc1-974a34914010', name: 'pipeline'} - it('it shows the setup, test, and final result output', async () => { - nock('https://api.heroku.com') + it('it shows the setup, test, and final result output', async function () { + api .get(`/pipelines?eq[name]=${pipeline.name}`) .reply(200, [ {id: pipeline.id}, @@ -45,13 +53,13 @@ describe('ci:info', function () { commit_branch: 'main', commit_message: 'Merge pull request #5848 from heroku/cli', commit_sha: 'b9e982a60904730510a1c9e2dd2df64aef6f0d84', + exit_code: 0, id: testRun.id, number: testRun.number, + output_stream_url: `https://test-output.heroku.com/streams/${testRun.id.slice(0, 3)}/test-runs/${testRun.id}`, pipeline: {id: pipeline.id}, - exit_code: 0, - status: 'succeeded', setup_stream_url: `https://test-setup-output.heroku.com/streams/${testRun.id.slice(0, 3)}/test-runs/${testRun.id}`, - output_stream_url: `https://test-output.heroku.com/streams/${testRun.id.slice(0, 3)}/test-runs/${testRun.id}`, + status: 'succeeded', }, ]) @@ -71,8 +79,8 @@ describe('ci:info', function () { describe('and the exit was not successful', function () { const testRunExitCode = 34 - it('it shows the setup, test, and final result output', async () => { - nock('https://api.heroku.com') + it('it shows the setup, test, and final result output', async function () { + api .get(`/pipelines?eq[name]=${pipeline.name}`) .reply(200, [ {id: pipeline.id}, @@ -95,13 +103,13 @@ describe('ci:info', function () { commit_branch: 'main', commit_message: 'Merge pull request #5848 from heroku/cli', commit_sha: 'b9e982a60904730510a1c9e2dd2df64aef6f0d84', + exit_code: testRunExitCode, id: testRun.id, number: testRun.number, + output_stream_url: `https://test-output.heroku.com/streams/${testRun.id.slice(0, 3)}/test-runs/${testRun.id}`, pipeline: {id: pipeline.id}, - exit_code: testRunExitCode, - status: 'succeeded', setup_stream_url: `https://test-setup-output.heroku.com/streams/${testRun.id.slice(0, 3)}/test-runs/${testRun.id}`, - output_stream_url: `https://test-output.heroku.com/streams/${testRun.id.slice(0, 3)}/test-runs/${testRun.id}`, + status: 'succeeded', }, ]) @@ -113,7 +121,7 @@ describe('ci:info', function () { .get(`/${testRun.id.slice(0, 3)}/test-runs/${testRun.id}`) .reply(200, 'Test output') - const {stdout, error} = await runCommand(['ci:info', `${testRun.number}`, `--pipeline=${pipeline.name}`]) + const {error, stdout} = await runCommand(['ci:info', `${testRun.number}`, `--pipeline=${pipeline.name}`]) expect(stdout).to.equal('Test setup outputTest output\n✗ #10 main:b9e982a failed\n') expect(error?.oclif?.exit).to.equal(testRunExitCode) @@ -121,8 +129,8 @@ describe('ci:info', function () { }) describe('when the pipeline has parallel test runs enabled', function () { - it('shows a result for each node', async () => { - nock('https://api.heroku.com') + it('shows a result for each node', async function () { + api .get(`/pipelines?eq[name]=${pipeline.name}`) .reply(200, [ {id: pipeline.id}, @@ -145,22 +153,22 @@ describe('ci:info', function () { commit_branch: 'main', commit_message: 'Merge pull request #5848 from heroku/cli', commit_sha: 'b9e982a60904730510a1c9e2dd2df64aef6f0d84', + exit_code: 0, id: testRun.id, + index: 0, number: testRun.number, pipeline: {id: pipeline.id}, - exit_code: 0, - index: 0, status: 'succeeded', }, { commit_branch: 'main', commit_message: 'Merge pull request #5848 from heroku/cli', commit_sha: 'b9e982a60904730510a1c9e2dd2df64aef6f0d84', + exit_code: 0, id: testRun.id, + index: 1, number: testRun.number, pipeline: {id: pipeline.id}, - exit_code: 0, - index: 1, status: 'succeeded', }, ]) @@ -171,8 +179,8 @@ describe('ci:info', function () { }) describe('and the user passes in a test node index', function () { - it('displays the setup and test output for the specified node', async () => { - nock('https://api.heroku.com') + it('displays the setup and test output for the specified node', async function () { + api .get(`/pipelines?eq[name]=${pipeline.name}`) .reply(200, [ {id: pipeline.id}, @@ -195,24 +203,24 @@ describe('ci:info', function () { commit_branch: 'main', commit_message: 'Merge pull request #5848 from heroku/cli', commit_sha: 'b9e982a60904730510a1c9e2dd2df64aef6f0d84', + exit_code: 0, id: testRun.id, + index: 0, number: testRun.number, pipeline: {id: pipeline.id}, - exit_code: 0, - index: 0, status: 'succeeded', }, { commit_branch: 'main', commit_message: 'Merge pull request #5848 from heroku/cli', commit_sha: 'b9e982a60904730510a1c9e2dd2df64aef6f0d84', + exit_code: 0, id: testRun.id, + index: 1, number: testRun.number, + output_stream_url: `https://test-output.heroku.com/streams/${testRun.id.slice(0, 3)}/test-runs/${testRun.id}`, pipeline: {id: pipeline.id}, - exit_code: 0, - index: 1, setup_stream_url: `https://test-setup-output.heroku.com/streams/${testRun.id.slice(0, 3)}/test-runs/${testRun.id}`, - output_stream_url: `https://test-output.heroku.com/streams/${testRun.id.slice(0, 3)}/test-runs/${testRun.id}`, status: 'succeeded', }, ]) @@ -231,8 +239,8 @@ describe('ci:info', function () { }) describe('and the pipeline does not have parallel tests enabled', function () { - it('displays the setup and test output for the first node and a warning', async () => { - nock('https://api.heroku.com') + it('displays the setup and test output for the first node and a warning', async function () { + api .get(`/pipelines?eq[name]=${pipeline.name}`) .reply(200, [ {id: pipeline.id}, @@ -255,13 +263,13 @@ describe('ci:info', function () { commit_branch: 'main', commit_message: 'Merge pull request #5848 from heroku/cli', commit_sha: 'b9e982a60904730510a1c9e2dd2df64aef6f0d84', + exit_code: 0, id: testRun.id, + index: 1, number: testRun.number, + output_stream_url: `https://test-output.heroku.com/streams/${testRun.id.slice(0, 3)}/test-runs/${testRun.id}`, pipeline: {id: pipeline.id}, - exit_code: 0, - index: 1, setup_stream_url: `https://test-setup-output.heroku.com/streams/${testRun.id.slice(0, 3)}/test-runs/${testRun.id}`, - output_stream_url: `https://test-output.heroku.com/streams/${testRun.id.slice(0, 3)}/test-runs/${testRun.id}`, status: 'succeeded', }, ]) @@ -274,7 +282,7 @@ describe('ci:info', function () { .get(`/${testRun.id.slice(0, 3)}/test-runs/${testRun.id}`) .reply(200, 'Test output') - const {stdout, stderr} = await runCommand(['ci:info', `${testRun.number}`, `--pipeline=${pipeline.name}`, '--node=1']) + const {stderr, stdout} = await runCommand(['ci:info', `${testRun.number}`, `--pipeline=${pipeline.name}`, '--node=1']) expect(stdout).to.equal('Test setup outputTest output\n✓ #10 main:b9e982a succeeded\n\n') expect(stderr).to.contain('Warning: This pipeline doesn\'t have parallel test runs') diff --git a/packages/cli/test/unit/commands/ci/last.unit.test.ts b/packages/cli/test/unit/commands/ci/last.unit.test.ts index 04ab9b28c8..b035809ce1 100644 --- a/packages/cli/test/unit/commands/ci/last.unit.test.ts +++ b/packages/cli/test/unit/commands/ci/last.unit.test.ts @@ -5,10 +5,18 @@ import nock from 'nock' describe('ci:last', function () { const testRunNumber = 10 const testRunId = 'f53d34b4-c3a9-4608-a186-17257cf71d62' + let api: nock.Scope - afterEach(() => nock.cleanAll()) + beforeEach(function () { + api = nock('https://api.heroku.com') + }) + + afterEach(function () { + api.done() + nock.cleanAll() + }) - it('errors when not specifying a pipeline or an app', async () => { + it('errors when not specifying a pipeline or an app', async function () { const {error} = await runCommand(['ci:last']) expect(error?.message).to.contain('Required flag: --pipeline PIPELINE or --app APP') }) @@ -17,14 +25,14 @@ describe('ci:last', function () { const application = {id: '14402644-c207-43aa-9bc1-974a34914010', name: 'pipeline'} const pipeline = {id: '45450264-b207-467a-Abc1-999c34883645', name: 'aquafresh'} - it('warns the user that there are no CI runs', async () => { - nock('https://api.heroku.com') + it('warns the user that there are no CI runs', async function () { + api .get(`/apps/${application.name}/pipeline-couplings`) .reply(200, { - id: '01234567-89ab-cdef-0123-456789abcdef', app: { id: `${application.id}`, }, + id: '01234567-89ab-cdef-0123-456789abcdef', pipeline: { id: `${pipeline.id}`, }, @@ -38,8 +46,8 @@ describe('ci:last', function () { expect(stderr).to.contain('No Heroku CI runs found for the specified app and/or pipeline.') }) - it('errors when no pipelines exist', async () => { - nock('https://api.heroku.com') + it('errors when no pipelines exist', async function () { + api .get(`/apps/${application.name}/pipeline-couplings`) .reply(200, {}) @@ -52,8 +60,8 @@ describe('ci:last', function () { describe('when specifying a pipeline', function () { const pipeline = {id: '14402644-c207-43aa-9bc1-974a34914010', name: 'pipeline'} - it('and a pipeline without parallel test runs it shows node output', async () => { - nock('https://api.heroku.com') + it('and a pipeline without parallel test runs it shows node output', async function () { + api .get(`/pipelines?eq[name]=${pipeline.name}`) .reply(200, [ {id: pipeline.id}, @@ -99,10 +107,10 @@ describe('ci:last', function () { commit_sha: 'b9e982a60904730510a1c9e2dd2df64aef6f0d84', id: testRunId, number: testRunNumber, + output_stream_url: `https://test-output.heroku.com/streams/${testRunId.slice(0, 3)}/test-runs/${testRunId}`, pipeline: {id: pipeline.id}, - status: 'succeeded', setup_stream_url: `https://test-setup-output.heroku.com/streams/${testRunId.slice(0, 3)}/test-runs/${testRunId}`, - output_stream_url: `https://test-output.heroku.com/streams/${testRunId.slice(0, 3)}/test-runs/${testRunId}`, + status: 'succeeded', }, ]) @@ -123,8 +131,8 @@ describe('ci:last', function () { describe('when test nodes is an empty array', function () { const pipeline = {id: '14402644-c207-43aa-9bc1-974a34914010', name: 'pipeline'} - it('shows an error about not test nodes found', async () => { - nock('https://api.heroku.com') + it('shows an error about not test nodes found', async function () { + api .get(`/pipelines?eq[name]=${pipeline.name}`) .reply(200, [ {id: pipeline.id}, diff --git a/packages/cli/test/unit/commands/ci/migrate-manifest.unit.test.ts b/packages/cli/test/unit/commands/ci/migrate-manifest.unit.test.ts index 1713345e2e..2e13d66a1b 100644 --- a/packages/cli/test/unit/commands/ci/migrate-manifest.unit.test.ts +++ b/packages/cli/test/unit/commands/ci/migrate-manifest.unit.test.ts @@ -1,14 +1,14 @@ import {runCommand} from '@oclif/test' import {expect} from 'chai' import {promises as fs} from 'node:fs' -const {readFile, writeFile, unlink} = fs +const {readFile, unlink, writeFile} = fs const unlinkFile = unlink describe('ci:migrate-manifest', function () { let appJsonFileContents const appJsonPath = './app.json' - afterEach(async () => { + afterEach(async function () { // Clean up any files created during tests try { await unlinkFile(appJsonPath) @@ -20,48 +20,17 @@ describe('ci:migrate-manifest', function () { }) const mockNewAppJsonFileContents = {environments: {}} const mockOldAppCiJsonFileContents = { - name: 'Small Sharp Tool', - description: 'This app does one little thing, and does it well.', - keywords: [ - 'productivity', - 'HTML5', - 'scalpel', - ], - website: 'https://small-sharp-tool.com/', - repository: 'https://github.com/jane-doe/small-sharp-tool', - logo: 'https://small-sharp-tool.com/logo.svg', - success_url: '/welcome', - scripts: { - postdeploy: 'bundle exec rake bootstrap', - }, - env: { - SECRET_TOKEN: { - description: 'A secret key for verifying the integrity of signed cookies.', - generator: 'secret', - }, - WEB_CONCURRENCY: { - description: 'The number of processes to run.', - value: '5', - }, - }, - formation: { - web: { - quantity: 1, - size: 'standard-1x', - }, - }, - image: 'heroku/ruby', addons: [ 'openredis', { - plan: 'mongolab:shared-single-small', as: 'MONGO', + plan: 'mongolab:shared-single-small', }, { - plan: 'heroku-postgresql', options: { version: '9.5', }, + plan: 'heroku-postgresql', }, ], buildpacks: [ @@ -69,6 +38,17 @@ describe('ci:migrate-manifest', function () { url: 'https://github.com/stomita/heroku-buildpack-phantomjs', }, ], + description: 'This app does one little thing, and does it well.', + env: { + SECRET_TOKEN: { + description: 'A secret key for verifying the integrity of signed cookies.', + generator: 'secret', + }, + WEB_CONCURRENCY: { + description: 'The number of processes to run.', + value: '5', + }, + }, environments: { test: { scripts: { @@ -76,53 +56,42 @@ describe('ci:migrate-manifest', function () { }, }, }, + formation: { + web: { + quantity: 1, + size: 'standard-1x', + }, + }, + image: 'heroku/ruby', + keywords: [ + 'productivity', + 'HTML5', + 'scalpel', + ], + logo: 'https://small-sharp-tool.com/logo.svg', + name: 'Small Sharp Tool', + repository: 'https://github.com/jane-doe/small-sharp-tool', + scripts: { + postdeploy: 'bundle exec rake bootstrap', + }, + success_url: '/welcome', + website: 'https://small-sharp-tool.com/', } const mockConvertedAppJSONFileContents = { environments: { test: { - name: 'Small Sharp Tool', - description: 'This app does one little thing, and does it well.', - keywords: [ - 'productivity', - 'HTML5', - 'scalpel', - ], - website: 'https://small-sharp-tool.com/', - repository: 'https://github.com/jane-doe/small-sharp-tool', - logo: 'https://small-sharp-tool.com/logo.svg', - success_url: '/welcome', - scripts: { - postdeploy: 'bundle exec rake bootstrap', - }, - env: { - SECRET_TOKEN: { - description: 'A secret key for verifying the integrity of signed cookies.', - generator: 'secret', - }, - WEB_CONCURRENCY: { - description: 'The number of processes to run.', - value: '5', - }, - }, - formation: { - web: { - quantity: 1, - size: 'standard-1x', - }, - }, - image: 'heroku/ruby', addons: [ 'openredis', { - plan: 'mongolab:shared-single-small', as: 'MONGO', + plan: 'mongolab:shared-single-small', }, { - plan: 'heroku-postgresql', options: { version: '9.5', }, + plan: 'heroku-postgresql', }, ], buildpacks: [ @@ -130,6 +99,17 @@ describe('ci:migrate-manifest', function () { url: 'https://github.com/stomita/heroku-buildpack-phantomjs', }, ], + description: 'This app does one little thing, and does it well.', + env: { + SECRET_TOKEN: { + description: 'A secret key for verifying the integrity of signed cookies.', + generator: 'secret', + }, + WEB_CONCURRENCY: { + description: 'The number of processes to run.', + value: '5', + }, + }, environments: { test: { scripts: { @@ -137,11 +117,31 @@ describe('ci:migrate-manifest', function () { }, }, }, + formation: { + web: { + quantity: 1, + size: 'standard-1x', + }, + }, + image: 'heroku/ruby', + keywords: [ + 'productivity', + 'HTML5', + 'scalpel', + ], + logo: 'https://small-sharp-tool.com/logo.svg', + name: 'Small Sharp Tool', + repository: 'https://github.com/jane-doe/small-sharp-tool', + scripts: { + postdeploy: 'bundle exec rake bootstrap', + }, + success_url: '/welcome', + website: 'https://small-sharp-tool.com/', }, }, } - it('creates an app.json file if none exists', async () => { + it('creates an app.json file if none exists', async function () { const {stdout} = await runCommand(['ci:migrate-manifest']) const fileContents = await readFile(`${process.cwd()}/app.json`, 'utf8') @@ -151,7 +151,7 @@ describe('ci:migrate-manifest', function () { expect(appJsonFileContents).to.deep.equal(mockNewAppJsonFileContents) }) - it('creates converted app.json file when app-ci.json file is present', async () => { + it('creates converted app.json file when app-ci.json file is present', async function () { await writeFile('app-ci.json', `${JSON.stringify(mockOldAppCiJsonFileContents, null, ' ')}\n`) const {stdout} = await runCommand(['ci:migrate-manifest']) diff --git a/packages/cli/test/unit/commands/ci/rerun.unit.test.ts b/packages/cli/test/unit/commands/ci/rerun.unit.test.ts index 0b87bde965..fd6b146e0d 100644 --- a/packages/cli/test/unit/commands/ci/rerun.unit.test.ts +++ b/packages/cli/test/unit/commands/ci/rerun.unit.test.ts @@ -1,18 +1,28 @@ import {expect} from 'chai' +import {got} from 'got' import nock from 'nock' -import sinon from 'sinon' import {PassThrough} from 'node:stream' +import sinon from 'sinon' +import {stdout} from 'stdout-stderr' + +import Cmd from '../../../../src/commands/ci/rerun.js' import {gitService} from '../../../../src/lib/ci/git.js' import {fileService} from '../../../../src/lib/ci/source.js' -import {got} from 'got' import customRunCommand from '../../../helpers/runCommand.js' -import Cmd from '../../../../src/commands/ci/rerun.js' -import {stdout} from 'stdout-stderr' describe('ci:rerun', function () { - afterEach(() => nock.cleanAll()) + let api: nock.Scope + + beforeEach(function () { + api = nock('https://api.heroku.com') + }) - it('errors when not specifying a pipeline or an app', async () => { + afterEach(function () { + api.done() + nock.cleanAll() + }) + + it('errors when not specifying a pipeline or an app', async function () { try { await customRunCommand(Cmd, []) } catch (error: any) { @@ -23,7 +33,7 @@ describe('ci:rerun', function () { describe('when specifying a pipeline', function () { const pipeline = {id: '14402644-c207-43aa-9bc1-974a34914010', name: 'pipeline'} const ghRepository = { - user: 'heroku-fake', repo: 'my-repo', ref: '668a5ce22eefc7b67c84c1cfe3a766f1958e0add', branch: 'my-test-branch', + branch: 'my-test-branch', ref: '668a5ce22eefc7b67c84c1cfe3a766f1958e0add', repo: 'my-repo', user: 'heroku-fake', } const oldTestRun = { commit_branch: ghRepository.branch, @@ -49,7 +59,7 @@ describe('ci:rerun', function () { sandbox = sinon.createSandbox() // Stub gitService methods - sandbox.stub(gitService, 'githubRepository').resolves({user: ghRepository.user, repo: ghRepository.repo} as any) + sandbox.stub(gitService, 'githubRepository').resolves({repo: ghRepository.repo, user: ghRepository.user} as any) sandbox.stub(gitService, 'createArchive').resolves('new-archive.tgz') // Stub fileService methods @@ -77,8 +87,8 @@ describe('ci:rerun', function () { }) describe('when not specifying a run #', function () { - it('it runs the test and displays the test output for the first node', async () => { - nock('https://api.heroku.com') + it('it runs the test and displays the test output for the first node', async function () { + api .get(`/pipelines?eq[name]=${pipeline.name}`) .reply(200, [ {id: pipeline.id}, @@ -96,17 +106,17 @@ describe('ci:rerun', function () { commit_branch: newTestRun.commit_branch, commit_message: newTestRun.commit_message, commit_sha: newTestRun.commit_sha, + exit_code: 0, id: newTestRun.id, number: newTestRun.number, + output_stream_url: `https://test-output.heroku.com/streams/${newTestRun.id.slice(0, 3)}/test-runs/${newTestRun.id}`, pipeline: {id: pipeline.id}, - exit_code: 0, - status: newTestRun.status, setup_stream_url: `https://test-setup-output.heroku.com/streams/${newTestRun.id.slice(0, 3)}/test-runs/${newTestRun.id}`, - output_stream_url: `https://test-output.heroku.com/streams/${newTestRun.id.slice(0, 3)}/test-runs/${newTestRun.id}`, + status: newTestRun.status, }, ]) .post('/sources') - .reply(200, {source_blob: {put_url: 'https://aws-puturl', get_url: 'https://aws-geturl'}}) + .reply(200, {source_blob: {get_url: 'https://aws-geturl', put_url: 'https://aws-puturl'}}) nock('https://test-setup-output.heroku.com/streams') .get(`/${newTestRun.id.slice(0, 3)}/test-runs/${newTestRun.id}`) @@ -122,10 +132,9 @@ describe('ci:rerun', function () { ci: true, organization: {id: 'e037ed63-5781-48ee-b2b7-8c55c571b63e'}, owner: { - id: '463147bf-d572-41cf-bbf4-11ebc1c0bc3b', - heroku: { - user_id: '463147bf-d572-41cf-bbf4-11ebc1c0bc3b'}, github: {user_id: 306015}, + heroku: {user_id: '463147bf-d572-41cf-bbf4-11ebc1c0bc3b'}, + id: '463147bf-d572-41cf-bbf4-11ebc1c0bc3b', }, repository: { id: 138865824, @@ -141,8 +150,8 @@ describe('ci:rerun', function () { }) describe('when specifying a run #', function () { - it('it runs the test and displays the test output for the first node', async () => { - nock('https://api.heroku.com') + it('it runs the test and displays the test output for the first node', async function () { + api .get(`/pipelines?eq[name]=${pipeline.name}`) .reply(200, [ {id: pipeline.id}, @@ -160,17 +169,17 @@ describe('ci:rerun', function () { commit_branch: newTestRun.commit_branch, commit_message: newTestRun.commit_message, commit_sha: newTestRun.commit_sha, + exit_code: 0, id: newTestRun.id, number: newTestRun.number, + output_stream_url: `https://test-output.heroku.com/streams/${newTestRun.id.slice(0, 3)}/test-runs/${newTestRun.id}`, pipeline: {id: pipeline.id}, - exit_code: 0, - status: newTestRun.status, setup_stream_url: `https://test-setup-output.heroku.com/streams/${newTestRun.id.slice(0, 3)}/test-runs/${newTestRun.id}`, - output_stream_url: `https://test-output.heroku.com/streams/${newTestRun.id.slice(0, 3)}/test-runs/${newTestRun.id}`, + status: newTestRun.status, }, ]) .post('/sources') - .reply(200, {source_blob: {put_url: 'https://aws-puturl', get_url: 'https://aws-geturl'}}) + .reply(200, {source_blob: {get_url: 'https://aws-geturl', put_url: 'https://aws-puturl'}}) nock('https://test-setup-output.heroku.com/streams') .get(`/${newTestRun.id.slice(0, 3)}/test-runs/${newTestRun.id}`) @@ -186,10 +195,9 @@ describe('ci:rerun', function () { ci: true, organization: {id: 'e037ed63-5781-48ee-b2b7-8c55c571b63e'}, owner: { - id: '463147bf-d572-41cf-bbf4-11ebc1c0bc3b', - heroku: { - user_id: '463147bf-d572-41cf-bbf4-11ebc1c0bc3b'}, github: {user_id: 306015}, + heroku: {user_id: '463147bf-d572-41cf-bbf4-11ebc1c0bc3b'}, + id: '463147bf-d572-41cf-bbf4-11ebc1c0bc3b', }, repository: { id: 138865824, diff --git a/packages/cli/test/unit/commands/ci/run.unit.test.ts b/packages/cli/test/unit/commands/ci/run.unit.test.ts index 9be7d57b63..60e6e62430 100644 --- a/packages/cli/test/unit/commands/ci/run.unit.test.ts +++ b/packages/cli/test/unit/commands/ci/run.unit.test.ts @@ -11,7 +11,14 @@ import {fileService} from '../../../../src/lib/ci/source.js' import customRunCommand from '../../../helpers/runCommand.js' describe('ci:run', function () { + let api: nock.Scope + + beforeEach(function () { + api = nock('https://api.heroku.com') + }) + afterEach(function () { + api.done() return nock.cleanAll() }) @@ -47,8 +54,8 @@ describe('ci:run', function () { sandbox = sinon.createSandbox() // Stub gitService methods - sandbox.stub(gitService, 'readCommit').resolves({branch: ghRepository.branch, ref: ghRepository.ref, message: `pushed to ${ghRepository.branch}`}) - sandbox.stub(gitService, 'githubRepository').resolves({user: ghRepository.user, repo: ghRepository.repo} as any) + sandbox.stub(gitService, 'readCommit').resolves({branch: ghRepository.branch, message: `pushed to ${ghRepository.branch}`, ref: ghRepository.ref}) + sandbox.stub(gitService, 'githubRepository').resolves({repo: ghRepository.repo, user: ghRepository.user} as any) sandbox.stub(gitService, 'createArchive').resolves('new-archive.tgz') // Stub fileService methods @@ -76,7 +83,7 @@ describe('ci:run', function () { }) it('it runs the test and displays the test output for the first node', async function () { - nock('https://api.heroku.com') + api .get(`/pipelines?eq[name]=${pipeline.name}`) .reply(200, [ {id: pipeline.id}, @@ -119,8 +126,7 @@ describe('ci:run', function () { organization: {id: 'e037ed63-5781-48ee-b2b7-8c55c571b63e'}, owner: { github: {user_id: 306015}, - heroku: { - user_id: '463147bf-d572-41cf-bbf4-11ebc1c0bc3b'}, + heroku: {user_id: '463147bf-d572-41cf-bbf4-11ebc1c0bc3b'}, id: '463147bf-d572-41cf-bbf4-11ebc1c0bc3b', }, repository: { @@ -137,7 +143,7 @@ describe('ci:run', function () { describe('when the commit is not in the remote repository', function () { it('it runs the test and displays the test output for the first node', async function () { - nock('https://api.heroku.com') + api .get(`/pipelines?eq[name]=${pipeline.name}`) .reply(200, [ {id: pipeline.id}, @@ -163,7 +169,7 @@ describe('ci:run', function () { }, ]) .post('/sources') - .reply(200, {source_blob: {put_url: 'https://aws-puturl', get_url: 'https://aws-geturl'}}) + .reply(200, {source_blob: {get_url: 'https://aws-geturl', put_url: 'https://aws-puturl'}}) nock('https://test-setup-output.heroku.com/streams') .get(`/${newTestRun.id.slice(0, 3)}/test-runs/${newTestRun.id}`)