From 4e638d7fcd0c81212592216d8de3cd993716cf35 Mon Sep 17 00:00:00 2001 From: Justin Wilaby Date: Thu, 14 Mar 2024 07:05:09 -0700 Subject: [PATCH] certs-v5(W-14161962): migrate certs:generate --- packages/certs-v5/commands/certs/generate.js | 168 --------------- .../unit/commands/certs/generate.unit.test.js | 191 ----------------- packages/cli/src/commands/certs/generate.ts | 122 +++++++++++ .../unit/commands/certs/generate.unit.test.ts | 196 ++++++++++++++++++ 4 files changed, 318 insertions(+), 359 deletions(-) delete mode 100644 packages/certs-v5/commands/certs/generate.js delete mode 100644 packages/certs-v5/test/unit/commands/certs/generate.unit.test.js create mode 100644 packages/cli/src/commands/certs/generate.ts create mode 100644 packages/cli/test/unit/commands/certs/generate.unit.test.ts diff --git a/packages/certs-v5/commands/certs/generate.js b/packages/certs-v5/commands/certs/generate.js deleted file mode 100644 index e6a236a6e4..0000000000 --- a/packages/certs-v5/commands/certs/generate.js +++ /dev/null @@ -1,168 +0,0 @@ -'use strict' - -let cli = require('heroku-cli-util') - -let openssl = require('../../lib/openssl.js') -let endpoints = require('../../lib/endpoints.js').all - -function valueEmpty(value) { - if (value) { - return value.length === 0 - } - - return true -} - -function getSubject(context) { - let domain = context.args.domain - - let owner = context.flags.owner - let country = context.flags.country - let area = context.flags.area - let city = context.flags.city - - let subject = context.flags.subject - - if (valueEmpty(subject)) { - subject = '' - if (!valueEmpty(country)) { - subject += `/C=${country}` - } - - if (!valueEmpty(area)) { - subject += `/ST=${area}` - } - - if (!valueEmpty(city)) { - subject += `/L=${city}` - } - - if (!valueEmpty(owner)) { - subject += `/O=${owner}` - } - - subject += `/CN=${domain}` - } - - return subject -} - -function requiresPrompt(context) { - if (valueEmpty(context.flags.subject)) { - let args = [context.flags.owner, context.flags.country, context.flags.area, context.flags.city] - if (!context.flags.now && args.every(function (v) { - return valueEmpty(v) - })) { - return true - } - } - - return false -} - -function getCommand(certs, domain) { - if (certs.find(function (f) { - return f.ssl_cert.cert_domains.find(function (d) { - return d === domain - }) - })) { - return 'update' - } - - return 'add' -} - -async function run(context, heroku) { - if (requiresPrompt(context)) { - context.flags.owner = await cli.prompt('Owner of this certificate') - context.flags.country = await cli.prompt('Country of owner (two-letter ISO code)') - context.flags.area = await cli.prompt('State/province/etc. of owner') - context.flags.city = await cli.prompt('City of owner') - } - - let subject = getSubject(context) - - let domain = context.args.domain - let keysize = context.flags.keysize || 2048 - let keyfile = `${domain}.key` - - let certs = await endpoints(context.app, heroku) - - var command = getCommand(certs, domain) - - if (context.flags.selfsigned) { - let crtfile = `${domain}.crt` - - await openssl.spawn(['req', '-new', '-newkey', `rsa:${keysize}`, '-nodes', '-keyout', keyfile, '-out', crtfile, '-subj', subject, '-x509']) - - cli.console.error('Your key and self-signed certificate have been generated.') - cli.console.error('Next, run:') - cli.console.error(`$ heroku certs:${command} ${crtfile} ${keyfile}`) - } else { - let csrfile = `${domain}.csr` - - await openssl.spawn(['req', '-new', '-newkey', `rsa:${keysize}`, '-nodes', '-keyout', keyfile, '-out', csrfile, '-subj', subject]) - - cli.console.error('Your key and certificate signing request have been generated.') - cli.console.error(`Submit the CSR in '${csrfile}' to your preferred certificate authority.`) - cli.console.error("When you've received your certificate, run:") - cli.console.error(`$ heroku certs:${command} CERTFILE ${keyfile}`) - } -} - -module.exports = { - topic: 'certs', - command: 'generate', - args: [ - {name: 'domain', optional: false}, - ], - flags: [ - { - name: 'selfsigned', - optional: true, - hasValue: false, - description: 'generate a self-signed certificate instead of a CSR', - }, { - name: 'keysize', - optional: true, - hasValue: true, - description: 'RSA key size in bits (default: 2048)', - }, { - name: 'owner', - optional: true, - hasValue: true, - description: 'name of organization certificate belongs to', - }, { - name: 'country', - optional: true, - hasValue: true, - description: 'country of owner, as a two-letter ISO country code', - }, { - name: 'area', - optional: true, - hasValue: true, - description: 'sub-country area (state, province, etc.) of owner', - }, { - name: 'city', - optional: true, - hasValue: true, - description: 'city of owner', - }, { - name: 'subject', - optional: true, - hasValue: true, - description: 'specify entire certificate subject', - }, { - name: 'now', - optional: true, - hasValue: false, - description: 'do not prompt for any owner information', - }, - ], - description: 'generate a key and a CSR or self-signed certificate', - help: 'Generate a key and certificate signing request (or self-signed certificate)\nfor an app. Prompts for information to put in the certificate unless --now\nis used, or at least one of the --subject, --owner, --country, --area, or\n--city options is specified.', - examples: '$ heroku certs:generate example.com', - needsApp: true, - needsAuth: true, - run: cli.command(run), -} diff --git a/packages/certs-v5/test/unit/commands/certs/generate.unit.test.js b/packages/certs-v5/test/unit/commands/certs/generate.unit.test.js deleted file mode 100644 index 0cb54adc8c..0000000000 --- a/packages/certs-v5/test/unit/commands/certs/generate.unit.test.js +++ /dev/null @@ -1,191 +0,0 @@ -/* eslint-env mocha */ -'use strict' - -let chai = require('chai') -let expect = require('chai').expect -let nock = require('nock') -let sinon = require('sinon') -let sinonChai = require('sinon-chai') -chai.should() -chai.use(sinonChai) - -let cli = require('heroku-cli-util') -let childProcess = require('child_process') - -let certs = require('../../../../commands/certs/generate.js') -let endpoint = require('../../../stubs/sni-endpoints.js').endpoint - -let EventEmitter = require('events').EventEmitter - -function mockPrompt(argument, returns) { - return cli.prompt.withArgs(argument).returns(new Promise(function (resolve) { - resolve(returns) - })) -} - -describe('heroku certs:generate', function () { - beforeEach(function () { - cli.mockConsole() - - nock('https://api.heroku.com') - .get('/apps/example/sni-endpoints') - .reply(200, [endpoint]) - - // stub cli here using sinon - // if this works, remove proxyquire - sinon.stub(cli, 'prompt') - - sinon.stub(childProcess, 'spawn', function () { - let eventEmitter = new EventEmitter() - process.nextTick(function () { - eventEmitter.emit('close', 0) - }) - return eventEmitter - }) - }) - - afterEach(function () { - cli.prompt.restore() - childProcess.spawn.restore() - }) - - it('# with certificate prompts emitted if no parts of subject provided', function () { - let owner = mockPrompt('Owner of this certificate', 'Heroku') - let country = mockPrompt('Country of owner (two-letter ISO code)', 'US') - let area = mockPrompt('State/province/etc. of owner', 'California') - let city = mockPrompt('City of owner', 'San Francisco') - - return certs.run({app: 'example', args: {domain: 'example.com'}, flags: {}}).then(function () { - expect(owner).to.have.been.called - expect(country).to.have.been.called - expect(area).to.have.been.called - expect(city).to.have.been.called - - expect(cli.stdout).to.equal('') - - expect(childProcess.spawn).to.have.been.calledWith('openssl', ['req', '-new', '-newkey', 'rsa:2048', '-nodes', '-keyout', 'example.com.key', '-out', 'example.com.csr', '-subj', '/C=US/ST=California/L=San Francisco/O=Heroku/CN=example.com']) - }) - }) - - it('# not emitted if any part of subject is specified', function () { - return certs.run({app: 'example', args: {domain: 'example.com'}, flags: {owner: 'Heroku'}}).then(function () { - expect(cli.prompt).to.have.not.been.called - - expect(cli.stdout).to.equal('') - - expect(childProcess.spawn).to.have.been.calledWith('openssl', ['req', '-new', '-newkey', 'rsa:2048', '-nodes', '-keyout', 'example.com.key', '-out', 'example.com.csr', '-subj', '/O=Heroku/CN=example.com']) - }) - }) - - it('# not emitted if --now is specified', function () { - return certs.run({app: 'example', args: {domain: 'example.com'}, flags: {now: true}}).then(function () { - expect(cli.prompt).to.have.not.been.called - - expect(cli.stdout).to.equal('') - - expect(childProcess.spawn).to.have.been.calledWith('openssl', ['req', '-new', '-newkey', 'rsa:2048', '-nodes', '-keyout', 'example.com.key', '-out', 'example.com.csr', '-subj', '/CN=example.com']) - }) - }) - - it('# not emitted if --subject is specified', function () { - return certs.run({app: 'example', args: {domain: 'example.com'}, flags: {subject: 'SOMETHING'}}).then(function () { - expect(cli.prompt).to.have.not.been.called - - expect(cli.stdout).to.equal('') - - expect(childProcess.spawn).to.have.been.calledWith('openssl', ['req', '-new', '-newkey', 'rsa:2048', '-nodes', '-keyout', 'example.com.key', '-out', 'example.com.csr', '-subj', 'SOMETHING']) - }) - }) - - it('# without --selfsigned does not request a self-signed certificate', function () { - return certs.run({app: 'example', args: {domain: 'example.com'}, flags: {now: true}}).then(function () { - expect(cli.prompt).to.have.not.been.called - - expect(cli.stdout).to.equal('') - - expect(cli.stderr).to.equal( - `Your key and certificate signing request have been generated. -Submit the CSR in 'example.com.csr' to your preferred certificate authority. -When you've received your certificate, run: -$ heroku certs:add CERTFILE example.com.key -`) - - expect(childProcess.spawn).to.have.been.calledWith('openssl', ['req', '-new', '-newkey', 'rsa:2048', '-nodes', '-keyout', 'example.com.key', '-out', 'example.com.csr', '-subj', '/CN=example.com']) - }) - }) - - it('# with --selfsigned does request a self-signed certificate', function () { - return certs.run({app: 'example', args: {domain: 'example.com'}, flags: {now: true, selfsigned: true}}).then(function () { - expect(cli.prompt).to.have.not.been.called - - expect(cli.stdout).to.equal('') - - expect(cli.stderr).to.equal( - `Your key and self-signed certificate have been generated. -Next, run: -$ heroku certs:add example.com.crt example.com.key -`) - - expect(childProcess.spawn).to.have.been.calledWith('openssl', ['req', '-new', '-newkey', 'rsa:2048', '-nodes', '-keyout', 'example.com.key', '-out', 'example.com.crt', '-subj', '/CN=example.com', '-x509']) - }) - }) - - it('# suggests next step should be certs:update when domain is known in sni', function () { - return certs.run({app: 'example', args: {domain: 'example.org'}, flags: {now: true}}).then(function () { - expect(cli.prompt).to.have.not.been.called - - expect(cli.stdout).to.equal('') - - expect(cli.stderr).to.equal( - `Your key and certificate signing request have been generated. -Submit the CSR in 'example.org.csr' to your preferred certificate authority. -When you've received your certificate, run: -$ heroku certs:update CERTFILE example.org.key -`) - - expect(childProcess.spawn).to.have.been.calledWith('openssl', ['req', '-new', '-newkey', 'rsa:2048', '-nodes', '-keyout', 'example.org.key', '-out', 'example.org.csr', '-subj', '/CN=example.org']) - }) - }) - - it('# suggests next step should be certs:update when domain is known in ssl', function () { - nock.cleanAll() - - nock('https://api.heroku.com') - .get('/apps/example/sni-endpoints') - .reply(200, []) - - return certs.run({app: 'example', args: {domain: 'example.org'}, flags: {now: true}}).then(function () { - expect(cli.prompt).to.have.not.been.called - - expect(cli.stdout).to.equal('') - - expect(cli.stderr).to.equal( - `Your key and certificate signing request have been generated. -Submit the CSR in 'example.org.csr' to your preferred certificate authority. -When you've received your certificate, run: -$ heroku certs:add CERTFILE example.org.key -`) - - expect(childProcess.spawn).to.have.been.calledWith('openssl', ['req', '-new', '-newkey', 'rsa:2048', '-nodes', '-keyout', 'example.org.key', '-out', 'example.org.csr', '-subj', '/CN=example.org']) - }) - }) - - it('# key size can be changed using keysize', function () { - return certs.run({app: 'example', args: {domain: 'example.org'}, flags: {now: true, keysize: '4096'}}).then(function () { - expect(cli.prompt).to.have.not.been.called - - expect(cli.stdout).to.equal('') - - expect(cli.stderr).to.equal( - `Your key and certificate signing request have been generated. -Submit the CSR in 'example.org.csr' to your preferred certificate authority. -When you've received your certificate, run: -$ heroku certs:update CERTFILE example.org.key -`) - - expect(childProcess.spawn).to.have.been.calledWith('openssl', ['req', '-new', '-newkey', 'rsa:4096', '-nodes', '-keyout', 'example.org.key', '-out', 'example.org.csr', '-subj', '/CN=example.org']) - }) - }) -}) - -/* jshint +W030 */ diff --git a/packages/cli/src/commands/certs/generate.ts b/packages/cli/src/commands/certs/generate.ts new file mode 100644 index 0000000000..d38629f665 --- /dev/null +++ b/packages/cli/src/commands/certs/generate.ts @@ -0,0 +1,122 @@ +import {Command, flags} from '@heroku-cli/command' +import {Args} from '@oclif/core' +import * as Heroku from '@heroku-cli/schema' +import {all} from '../../lib/certs/endpoints' +import {spawn} from 'node:child_process' +import * as inquirer from 'inquirer' + +function getCommand(certs: Heroku.SniEndpoint[], domain: string): 'update' | 'add' { + const shouldUpdate = certs + .map(cert => cert?.ssl_cert?.cert_domains) + .filter(certDomains => certDomains?.length) + .flat() + .includes(domain) + + return shouldUpdate ? 'update' : 'add' +} + +export default class Generate extends Command { + static topic = 'certs' + static description = 'generate a key and a CSR or self-signed certificate' + static help = 'Generate a key and certificate signing request (or self-signed certificate)\nfor an app. Prompts for information to put in the certificate unless --now\nis used, or at least one of the --subject, --owner, --country, --area, or\n--city options is specified.' + static flags = { + selfsigned: flags.boolean({required: false, description: 'generate a self-signed certificate instead of a CSR'}), + keysize: flags.string({optional: true, description: 'RSA key size in bits (default: 2048)'}), + owner: flags.string({optional: true, description: 'name of organization certificate belongs to'}), + country: flags.string({optional: true, description: 'country of owner, as a two-letter ISO country code'}), + area: flags.string({optional: true, description: 'sub-country area (state, province, etc.) of owner'}), + city: flags.string({optional: true, description: 'city of owner'}), + subject: flags.string({optional: true, description: 'specify entire certificate subject'}), + now: flags.boolean({required: false, description: 'do not prompt for any owner information'}), + app: flags.app({required: true}), + } + + static args = { + domain: Args.string({required: true}), + } + + private parsed = this.parse(Generate) + + public async run(): Promise { + const {flags, args} = await this.parsed + const {app, selfsigned} = flags + if (this.requiresPrompt(flags)) { + const {owner, country, area, city} = await inquirer.prompt([ + {type: 'input', message: 'Owner of this certificate', name: 'owner'}, + {type: 'input', message: 'Country of owner (two-letter ISO code)', name: 'country'}, + {type: 'input', message: 'State/province/etc. of owner', name: 'area'}, + {type: 'input', message: 'City of owner', name: 'city'}, + ]) + Object.assign(flags, {owner, country, area, city}) + } + + const subject = this.getSubject(args, flags) + const domain = args.domain + const keysize = flags.keysize || 2048 + const keyfile = `${domain}.key` + const certs = await all(app, this.heroku) + const command = getCommand(certs, domain) + if (selfsigned) { + const crtfile = `${domain}.crt` + await this.spawnOpenSSL(['req', '-new', '-newkey', `rsa:${keysize}`, '-nodes', '-keyout', keyfile, '-out', crtfile, '-subj', subject, '-x509']) + console.error('Your key and self-signed certificate have been generated.') + console.error('Next, run:') + console.error(`$ heroku certs:${command} ${crtfile} ${keyfile}`) + } else { + const csrfile = `${domain}.csr` + await this.spawnOpenSSL(['req', '-new', '-newkey', `rsa:${keysize}`, '-nodes', '-keyout', keyfile, '-out', csrfile, '-subj', subject]) + console.error('Your key and certificate signing request have been generated.') + console.error(`Submit the CSR in '${csrfile}' to your preferred certificate authority.`) + console.error('When you\'ve received your certificate, run:') + console.error(`$ heroku certs:${command} CERTFILE ${keyfile}`) + } + } + + protected requiresPrompt(flags: Awaited['flags']) { + if (flags.subject) { + return false + } + + const args = [flags.owner, flags.country, flags.area, flags.city] + if (!flags.now && args.every((arg: string | undefined) => !arg)) { + return true + } + } + + protected getSubject(args: Awaited['args'], flags: Awaited['flags']) { + const {domain} = args + const {owner, country, area, city, subject} = flags + if (subject) { + return subject + } + + let constructedSubject = '' + if (country) { + constructedSubject += `/C=${country}` + } + + if (area) { + constructedSubject += `/ST=${area}` + } + + if (city) { + constructedSubject += `/L=${city}` + } + + if (owner) { + constructedSubject += `/O=${owner}` + } + + constructedSubject += `/CN=${domain}` + + return constructedSubject + } + + protected async spawnOpenSSL(args: ReadonlyArray) { + return new Promise((resolve, reject) => { + const process = spawn('openssl', args, {stdio: 'inherit'}) + process.once('error', reject) + process.once('close', (code: number) => code ? reject(new Error(`Non zero openssl error ${code}`)) : resolve(code)) + }) + } +} diff --git a/packages/cli/test/unit/commands/certs/generate.unit.test.ts b/packages/cli/test/unit/commands/certs/generate.unit.test.ts new file mode 100644 index 0000000000..e8f5544f7b --- /dev/null +++ b/packages/cli/test/unit/commands/certs/generate.unit.test.ts @@ -0,0 +1,196 @@ +import Cmd from '../../../../src/commands/certs/generate' +import {stdout, stderr} from 'stdout-stderr' +import runCommand from '../../../helpers/runCommand' +import * as nock from 'nock' +import {endpoint} from '../../../helpers/stubs/sni-endpoints' +import * as sinon from 'sinon' +import {expect} from '@oclif/test' +import {QuestionCollection} from 'inquirer' +import * as inquirer from 'inquirer' +import * as childProcess from 'node:child_process' +import {SinonStub} from 'sinon' + +describe('heroku certs:generate', function () { + let childProcessStub: SinonStub + let stubbedPrompt: SinonStub + let stubbedPromptReturnValue: unknown = {} + let questionsReceived: ReadonlyArray | undefined + beforeEach(() => { + nock('https://api.heroku.com') + .get('/apps/example/sni-endpoints') + .reply(200, [endpoint]) + + stubbedPrompt = sinon.stub(inquirer, 'prompt') + stubbedPrompt.callsFake((questions: QuestionCollection) => { + questionsReceived = questions as ReadonlyArray + return Promise.resolve(stubbedPromptReturnValue) as ReturnType + }) + + questionsReceived = undefined + }) + + before(function () { + childProcessStub = sinon.stub(childProcess, 'spawn') + childProcessStub.callsFake(() => { + return { + once: (event: string, cb: CallableFunction) => { + if (event === 'close') { + cb() + } + }, + unref: () => {}, + } + }) + }) + + afterEach(() => { + stubbedPrompt.restore() + }) + + after(function () { + childProcessStub.restore() + }) + + it('# with certificate prompts emitted if no parts of subject provided', async () => { + stubbedPromptReturnValue = {owner: 'Heroku', country: 'US', area: 'California', city: 'San Francisco'} + + await runCommand(Cmd, [ + '--app', + 'example', + 'example.com', + ]) + expect(questionsReceived).to.deep.equal([ + { + type: 'input', + message: 'Owner of this certificate', + name: 'owner', + }, + { + type: 'input', + message: 'Country of owner (two-letter ISO code)', + name: 'country', + }, + { + type: 'input', + message: 'State/province/etc. of owner', + name: 'area', + }, + { + type: 'input', + message: 'City of owner', + name: 'city', + }, + ]) + expect(stdout.output).to.equal('') + expect(childProcessStub.calledWith('openssl', ['req', '-new', '-newkey', 'rsa:2048', '-nodes', '-keyout', 'example.com.key', '-out', 'example.com.csr', '-subj', '/C=US/ST=California/L=San Francisco/O=Heroku/CN=example.com'])).to.be.true + }) + + it('# not emitted if any part of subject is specified', async () => { + await runCommand(Cmd, [ + '--app', + 'example', + '--owner', + 'Heroku', + 'example.com', + ]) + expect(stubbedPrompt.called).to.be.false + expect(stdout.output).to.equal('') + expect(childProcessStub.calledWith('openssl', ['req', '-new', '-newkey', 'rsa:2048', '-nodes', '-keyout', 'example.com.key', '-out', 'example.com.csr', '-subj', '/O=Heroku/CN=example.com'])).to.be.true + }) + + it('# not emitted if --now is specified', async () => { + await runCommand(Cmd, [ + '--app', + 'example', + '--now', + 'example.com', + ]) + expect(stubbedPrompt.called).to.be.false + expect(stdout.output).to.equal('') + expect(childProcessStub.calledWith('openssl', ['req', '-new', '-newkey', 'rsa:2048', '-nodes', '-keyout', 'example.com.key', '-out', 'example.com.csr', '-subj', '/CN=example.com'])).to.be.true + }) + + it('# not emitted if --subject is specified', async () => { + await runCommand(Cmd, [ + '--app', + 'example', + '--subject', + 'SOMETHING', + 'example.com', + ]) + expect(stubbedPrompt.called).to.be.false + expect(stdout.output).to.equal('') + expect(childProcessStub.calledWith('openssl', ['req', '-new', '-newkey', 'rsa:2048', '-nodes', '-keyout', 'example.com.key', '-out', 'example.com.csr', '-subj', 'SOMETHING'])).to.be.true + }) + it('# without --selfsigned does not request a self-signed certificate', async () => { + await runCommand(Cmd, [ + '--app', + 'example', + '--now', + 'example.com', + ]) + expect(stubbedPrompt.called).to.be.false + expect(stdout.output).to.equal('') + expect(stderr.output).to.equal('Your key and certificate signing request have been generated.\nSubmit the CSR in \'example.com.csr\' to your preferred certificate authority.\nWhen you\'ve received your certificate, run:\n$ heroku certs:add CERTFILE example.com.key\n') + expect(childProcessStub.calledWith('openssl', ['req', '-new', '-newkey', 'rsa:2048', '-nodes', '-keyout', 'example.com.key', '-out', 'example.com.csr', '-subj', '/CN=example.com'])).to.be.true + }) + + it('# with --selfsigned does request a self-signed certificate', async () => { + await runCommand(Cmd, [ + '--app', + 'example', + '--now', + '--selfsigned', + 'example.com', + ]) + expect(stubbedPrompt.called).to.be.false + expect(stdout.output).to.equal('') + expect(stderr.output).to.equal('Your key and self-signed certificate have been generated.\nNext, run:\n$ heroku certs:add example.com.crt example.com.key\n') + expect(childProcessStub.calledWith('openssl', ['req', '-new', '-newkey', 'rsa:2048', '-nodes', '-keyout', 'example.com.key', '-out', 'example.com.crt', '-subj', '/CN=example.com', '-x509'])).to.be.true + }) + + it('# suggests next step should be certs:update when domain is known in sni', async () => { + await runCommand(Cmd, [ + '--app', + 'example', + '--now', + 'example.org', + ]) + expect(stubbedPrompt.called).to.be.false + expect(stdout.output).to.equal('') + expect(stderr.output).to.equal('Your key and certificate signing request have been generated.\nSubmit the CSR in \'example.org.csr\' to your preferred certificate authority.\nWhen you\'ve received your certificate, run:\n$ heroku certs:update CERTFILE example.org.key\n') + expect(childProcessStub.calledWith('openssl', ['req', '-new', '-newkey', 'rsa:2048', '-nodes', '-keyout', 'example.org.key', '-out', 'example.org.csr', '-subj', '/CN=example.org'])).to.be.true + }) + + it('# suggests next step should be certs:update when domain is known in ssl', async () => { + nock.cleanAll() + nock('https://api.heroku.com') + .get('/apps/example/sni-endpoints') + .reply(200, []) + await runCommand(Cmd, [ + '--app', + 'example', + '--now', + 'example.org', + ]) + expect(stubbedPrompt.called).to.be.false + expect(stdout.output).to.equal('') + expect(stderr.output).to.equal('Your key and certificate signing request have been generated.\nSubmit the CSR in \'example.org.csr\' to your preferred certificate authority.\nWhen you\'ve received your certificate, run:\n$ heroku certs:add CERTFILE example.org.key\n') + expect(childProcessStub.calledWith('openssl', ['req', '-new', '-newkey', 'rsa:2048', '-nodes', '-keyout', 'example.org.key', '-out', 'example.org.csr', '-subj', '/CN=example.org'])).to.be.true + }) + + it('# key size can be changed using keysize', async () => { + await runCommand(Cmd, [ + '--app', + 'example', + '--now', + '--keysize', + '4096', + 'example.org', + ]) + expect(stubbedPrompt.called).to.be.false + expect(stdout.output).to.equal('') + expect(stderr.output).to.equal('Your key and certificate signing request have been generated.\nSubmit the CSR in \'example.org.csr\' to your preferred certificate authority.\nWhen you\'ve received your certificate, run:\n$ heroku certs:update CERTFILE example.org.key\n') + expect(childProcessStub.calledWith('openssl', ['req', '-new', '-newkey', 'rsa:4096', '-nodes', '-keyout', 'example.org.key', '-out', 'example.org.csr', '-subj', '/CN=example.org'])).to.be.true + }) +})