diff --git a/packages/cli/src/commands/ci/run.ts b/packages/cli/src/commands/ci/run.ts index abf4827a98..f4bdc22743 100644 --- a/packages/cli/src/commands/ci/run.ts +++ b/packages/cli/src/commands/ci/run.ts @@ -3,7 +3,7 @@ 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' @@ -25,7 +25,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 cb32c5ac77..050946630d 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,32 @@ 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() + } + + async readCommit(commit: string) { + return readCommit(commit) + } +} + +// Export a shared instance for use across commands +export const gitService = new GitService() + 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..84023ef0a1 100644 --- a/packages/cli/src/lib/ci/source.ts +++ b/packages/cli/src/lib/ci/source.ts @@ -1,20 +1,33 @@ 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') +// 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 +36,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 +59,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/certs/add.unit.test.ts.skip b/packages/cli/test/unit/commands/certs/add.unit.test.ts similarity index 85% 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..7d341bf8a0 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,35 +1,36 @@ -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: []} 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[]) => { @@ -42,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', }) @@ -65,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', }) @@ -86,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', }) @@ -110,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) @@ -157,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']} @@ -191,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?') }) @@ -207,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']} @@ -260,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') @@ -308,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', + }, { cname: null, + hostname: 'foo.example e.org', + kind: 'custom', status: 'none', - }, {kind: 'custom', hostname: 'bar.example.org', cname: null, status: 'none'}, { + }, { + cname: null, + hostname: 'bar.example.org', kind: 'custom', - hostname: 'biz.example.com', + 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', }, ]) @@ -401,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', }, ]) @@ -434,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.skip b/packages/cli/test/unit/commands/certs/generate.unit.test.ts similarity index 94% 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..8c16336af4 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,19 +1,20 @@ +import {expect} from 'chai' +import nock from 'nock' +import sinon, {SinonStub} from 'sinon' +import {stderr, stdout} from 'stdout-stderr' + import Cmd from '../../../../src/commands/certs/generate.js' -import {stdout, stderr} from 'stdout-stderr' import runCommand from '../../../helpers/runCommand.js' -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' 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,10 +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 new file mode 100644 index 0000000000..026a6368ea --- /dev/null +++ b/packages/cli/test/unit/commands/ci/config/get.unit.test.ts @@ -0,0 +1,47 @@ +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 () { + let api: nock.Scope + + 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`) + .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 function () { + api + .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`) - }) -}) 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..97eacb27d2 --- /dev/null +++ b/packages/cli/test/unit/commands/ci/config/index.unit.test.ts @@ -0,0 +1,79 @@ +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', + } + 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 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 function () { + api + .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 function () { + api + .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 function () { + api + .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') - }) -}) 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..bdef4e6727 --- /dev/null +++ b/packages/cli/test/unit/commands/ci/config/set.unit.test.ts @@ -0,0 +1,45 @@ +import {runCommand} from '@oclif/test' +import {expect} from 'chai' +import nock from 'nock' + +describe('heroku ci:config:set', function () { + 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 function () { + api + .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 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 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/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') -}) 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..97bb875cba --- /dev/null +++ b/packages/cli/test/unit/commands/ci/config/unset.unit.test.ts @@ -0,0 +1,38 @@ +import {runCommand} from '@oclif/test' +import {expect} from 'chai' +import nock from 'nock' + +describe('heroku ci:config:unset', function () { + 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 function () { + api + .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 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/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') -}) 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..c17bd918ef --- /dev/null +++ b/packages/cli/test/unit/commands/ci/index.unit.test.ts @@ -0,0 +1,180 @@ +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 customRunCommand from '../../../helpers/runCommand.js' +import removeAllWhitespace from '../../../helpers/utils/remove-whitespaces.js' + +describe('ci', function () { + 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 function () { + 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: { + created_at: '05/10/2023', + id: '14402644-c207-43aa-9bc1-974a34914010', + name: '14402644-c207-43aa-9bc1-974a34914010', + }, + } + + 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 function () { + api + .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 function () { + api + .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 function () { + api + .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 function () { + api + .get(`/pipelines?eq[name]=${pipeline.name}`) + .reply(200, [ + { + 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', + id: pipeline.id, + name: pipeline.id, + }, + ]) + .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 function () { + api + .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) - } - }) - }) -}) 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..75c26ce721 --- /dev/null +++ b/packages/cli/test/unit/commands/ci/info.unit.test.ts @@ -0,0 +1,294 @@ +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} + let api: nock.Scope + + beforeEach(function () { + api = nock('https://api.heroku.com') + }) + + afterEach(function () { + api.done() + nock.cleanAll() + }) + + 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 function () { + 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 function () { + api + .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', + 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}, + setup_stream_url: `https://test-setup-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}`]) + + 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 function () { + api + .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', + 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}, + setup_stream_url: `https://test-setup-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 {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) + }) + }) + + describe('when the pipeline has parallel test runs enabled', function () { + it('shows a result for each node', async function () { + api + .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', + exit_code: 0, + id: testRun.id, + index: 0, + number: testRun.number, + pipeline: {id: pipeline.id}, + 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}, + 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 function () { + api + .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', + exit_code: 0, + id: testRun.id, + index: 0, + number: testRun.number, + pipeline: {id: pipeline.id}, + 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}, + setup_stream_url: `https://test-setup-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 function () { + api + .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', + 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}, + setup_stream_url: `https://test-setup-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 {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/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') - }) - }) - }) - }) - }) -}) 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..b035809ce1 --- /dev/null +++ b/packages/cli/test/unit/commands/ci/last.unit.test.ts @@ -0,0 +1,172 @@ +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' + 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 function () { + 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 function () { + api + .get(`/apps/${application.name}/pipeline-couplings`) + .reply(200, { + app: { + id: `${application.id}`, + }, + id: '01234567-89ab-cdef-0123-456789abcdef', + 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 function () { + api + .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 function () { + api + .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, + output_stream_url: `https://test-output.heroku.com/streams/${testRunId.slice(0, 3)}/test-runs/${testRunId}`, + pipeline: {id: pipeline.id}, + setup_stream_url: `https://test-setup-output.heroku.com/streams/${testRunId.slice(0, 3)}/test-runs/${testRunId}`, + status: 'succeeded', + }, + ]) + + 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 function () { + api + .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') - }) -}) 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 71% 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..2e13d66a1b 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,13 +1,14 @@ -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 {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) @@ -19,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: [ @@ -68,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: { @@ -75,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: [ @@ -129,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: { @@ -136,32 +117,49 @@ 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/', }, }, } - 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 function () { + const {stdout} = await runCommand(['ci:migrate-manifest']) + + 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) - }) + 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 function () { + await writeFile('app-ci.json', `${JSON.stringify(mockOldAppCiJsonFileContents, null, ' ')}\n`) - 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) + 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) + }) }) 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..fd6b146e0d --- /dev/null +++ b/packages/cli/test/unit/commands/ci/rerun.unit.test.ts @@ -0,0 +1,215 @@ +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/rerun.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:rerun', function () { + 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 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 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({repo: ghRepository.repo, user: ghRepository.user} 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 function () { + api + .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, + 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('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 function () { + api + .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, + 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, [`${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') - }) - }) - }) -}) 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..60e6e62430 --- /dev/null +++ b/packages/cli/test/unit/commands/ci/run.unit.test.ts @@ -0,0 +1,205 @@ +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 {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 () { + let api: nock.Scope + + beforeEach(function () { + api = nock('https://api.heroku.com') + }) + + afterEach(function () { + api.done() + 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, 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 + 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 () { + api + .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 () { + api + .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') + }) + }) + }) +}) 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') - }) - }) - }) -})