navy
Version:
Quick and powerful development environments using Docker and Docker Compose
402 lines (400 loc) • 15.8 kB
JavaScript
;
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
var _chai = require("chai");
var _sinon = _interopRequireDefault(require("sinon"));
var _proxyquire = _interopRequireDefault(require("proxyquire"));
var _errors = require("../../errors");
/* eslint-env mocha */
describe('cli/https', function () {
let sandbox;
let getNavyStub;
let navyStub;
let getConfigStub;
let getConfigDirStub;
let DEFAULT_TLS_ROOT_CA_DIR;
let reconfigureHTTPProxyStub;
let createCertStub;
let generateRootCaStub;
let removeCertStub;
let issueLeafCertForUrlStub;
let installRootCaToTrustStoreStub;
let fsStub;
let consoleLogStub;
let httpsCli;
beforeEach(function () {
sandbox = _sinon.default.createSandbox();
navyStub = {
isInitialised: sandbox.stub().resolves(true),
getNavyFile: sandbox.stub().resolves({
ports: 'whatever'
}),
ensurePluginsLoaded: sandbox.stub().resolves(),
getAvailableServiceNames: sandbox.stub().resolves(['web', 'api']),
url: sandbox.stub().resolves('https://web.local')
};
getNavyStub = sandbox.stub().resolves(navyStub);
getConfigStub = sandbox.stub().returns({});
getConfigDirStub = sandbox.stub().returns('/cfg');
DEFAULT_TLS_ROOT_CA_DIR = '/default-ca-dir';
reconfigureHTTPProxyStub = sandbox.stub().resolves();
createCertStub = sandbox.stub().resolves();
generateRootCaStub = sandbox.stub().resolves();
removeCertStub = sandbox.stub().resolves();
issueLeafCertForUrlStub = sandbox.stub().resolves({
certPath: '/out/host.crt',
keyPath: '/out/host.key',
caCopyPath: '/out/navy-root-ca.crt'
});
installRootCaToTrustStoreStub = sandbox.stub();
fsStub = {
existsSync: sandbox.stub().returns(true),
readdirSync: sandbox.stub().returns(['web.local.crt', 'api.local.crt', 'something.txt'])
};
consoleLogStub = sandbox.stub(console, 'log');
httpsCli = _proxyquire.default.noCallThru()('../https', {
'../errors': {
NavyError: _errors.NavyError
},
'../navy': {
getNavy: getNavyStub
},
'../config': {
getConfig: getConfigStub,
getConfigDir: getConfigDirStub,
DEFAULT_TLS_ROOT_CA_DIR
},
'../http-proxy': {
reconfigureHTTPProxy: reconfigureHTTPProxyStub
},
'../util/https': {
createCert: createCertStub,
generateRootCa: generateRootCaStub,
removeCert: removeCertStub,
issueLeafCertForUrl: issueLeafCertForUrlStub
},
'../util/install-root-ca': {
installRootCaToTrustStore: installRootCaToTrustStoreStub
},
fs: fsStub
});
});
afterEach(function () {
sandbox.restore();
});
describe('default export', function () {
describe('disable mode', function () {
it('should remove the cert and reconfigure the proxy', async function () {
await httpsCli([], {
navy: 'env-1',
disable: 'web'
});
(0, _chai.expect)(removeCertStub.calledOnce).to.equal(true);
(0, _chai.expect)(reconfigureHTTPProxyStub.calledOnce).to.equal(true);
(0, _chai.expect)(reconfigureHTTPProxyStub.firstCall.args[0].restart).to.equal(true);
});
it('should pass navyFile to reconfigureHTTPProxy when the navy is initialised', async function () {
navyStub.isInitialised.resolves(true);
navyStub.getNavyFile.resolves({
foo: 'bar'
});
await httpsCli([], {
navy: 'env-1',
disable: 'web'
});
(0, _chai.expect)(reconfigureHTTPProxyStub.firstCall.args[0].navyFile).to.eql({
foo: 'bar'
});
});
it('should pass undefined navyFile when the navy is not initialised', async function () {
navyStub.isInitialised.resolves(false);
await httpsCli([], {
navy: 'env-1',
disable: 'web'
});
(0, _chai.expect)(reconfigureHTTPProxyStub.firstCall.args[0].navyFile).to.equal(undefined);
(0, _chai.expect)(navyStub.getNavyFile.called).to.equal(false);
});
it('should print a confirmation message naming the disabled service', async function () {
await httpsCli([], {
navy: 'env-1',
disable: 'web'
});
const printed = consoleLogStub.getCalls().map(c => c.args[0] || '').join('\n');
(0, _chai.expect)(printed).to.contain('web');
(0, _chai.expect)(printed).to.contain('disabled');
});
});
describe('list mode (no services and no disable)', function () {
it('should return early when the certs path does not exist', async function () {
fsStub.existsSync.returns(false);
await httpsCli([], {
navy: 'env-1'
});
(0, _chai.expect)(fsStub.readdirSync.called).to.equal(false);
(0, _chai.expect)(generateRootCaStub.called).to.equal(false);
});
it('should print only .crt urls when listing', async function () {
fsStub.existsSync.callsFake(p => p === '/cfg/tls-certs');
await httpsCli([], {
navy: 'env-1'
});
const printed = consoleLogStub.getCalls().map(c => c.args[0] || '').join('\n');
(0, _chai.expect)(printed).to.contain('https://web.local');
(0, _chai.expect)(printed).to.contain('https://api.local');
(0, _chai.expect)(printed).to.not.contain('something.txt');
});
it('should also handle a null services arg as list mode', async function () {
fsStub.existsSync.returns(false);
await httpsCli(null, {
navy: 'env-1'
});
(0, _chai.expect)(generateRootCaStub.called).to.equal(false);
});
});
describe('enable mode', function () {
it('should generate the root CA when the CA cert/key are missing', async function () {
fsStub.existsSync.callsFake(p => !p.endsWith('ca.crt'));
await httpsCli(['web'], {
navy: 'env-1'
});
(0, _chai.expect)(generateRootCaStub.calledOnce).to.equal(true);
});
it('should skip generating the root CA when ca.crt and ca.key both exist', async function () {
fsStub.existsSync.returns(true);
await httpsCli(['web'], {
navy: 'env-1'
});
(0, _chai.expect)(generateRootCaStub.called).to.equal(false);
});
it('should create a cert for each available service', async function () {
await httpsCli(['web', 'api'], {
navy: 'env-1'
});
(0, _chai.expect)(createCertStub.callCount).to.equal(2);
});
it('should skip services that are not in the available service list', async function () {
await httpsCli(['web', 'unknown'], {
navy: 'env-1'
});
(0, _chai.expect)(createCertStub.callCount).to.equal(1);
const printed = consoleLogStub.getCalls().map(c => c.args[0] || '').join('\n');
(0, _chai.expect)(printed).to.contain('unknown');
(0, _chai.expect)(printed).to.contain('not found');
});
it('should rethrow as NavyError when createCert fails', async function () {
createCertStub.rejects(new Error('cert error'));
let caught;
try {
await httpsCli(['web'], {
navy: 'env-1'
});
} catch (err) {
caught = err;
}
(0, _chai.expect)(caught).to.be.instanceof(_errors.NavyError);
(0, _chai.expect)(caught.message).to.contain('Could not generate TLS cert');
});
it('should reconfigure HTTPS proxy after enabling and print services list', async function () {
await httpsCli(['web', 'api'], {
navy: 'env-1'
});
(0, _chai.expect)(reconfigureHTTPProxyStub.calledOnce).to.equal(true);
const printed = consoleLogStub.getCalls().map(c => c.args[0] || '').join('\n');
(0, _chai.expect)(printed).to.contain('web');
(0, _chai.expect)(printed).to.contain('api');
(0, _chai.expect)(printed).to.contain('HTTPS');
});
it('should use a custom tlsRootCaDir when set in config', async function () {
getConfigStub.returns({
tlsRootCaDir: '/custom-ca'
});
fsStub.existsSync.callsFake(p => p === '/custom-ca/ca.crt' || p === '/custom-ca/ca.key');
await httpsCli(['web'], {
navy: 'env-1'
});
const checkedPaths = fsStub.existsSync.getCalls().map(c => c.args[0]);
(0, _chai.expect)(checkedPaths.some(p => p === '/custom-ca/ca.crt')).to.equal(true);
});
it('should accept a single string service name', async function () {
await httpsCli('web', {
navy: 'env-1'
});
(0, _chai.expect)(createCertStub.callCount).to.equal(1);
});
it('should ignore non-string entries in a services array', async function () {
await httpsCli(['web', '', null, 42, 'api'], {
navy: 'env-1'
});
(0, _chai.expect)(createCertStub.callCount).to.equal(2);
});
it('should treat unsupported service arg types as an empty list', async function () {
fsStub.existsSync.returns(false);
await httpsCli(42, {
navy: 'env-1'
});
(0, _chai.expect)(createCertStub.called).to.equal(false);
(0, _chai.expect)(generateRootCaStub.called).to.equal(false);
});
it('should treat an empty string service arg as list mode', async function () {
fsStub.existsSync.returns(false);
await httpsCli('', {
navy: 'env-1'
});
(0, _chai.expect)(createCertStub.called).to.equal(false);
});
});
describe('validation', function () {
async function expectNavyError(promise, messagePattern) {
let caught;
try {
await promise;
} catch (err) {
caught = err;
}
(0, _chai.expect)(caught).to.be.instanceof(_errors.NavyError);
(0, _chai.expect)(caught.message).to.match(messagePattern);
}
it('should reject combining disable with setup', async function () {
await expectNavyError(httpsCli([], {
disable: 'web',
setup: true
}), /cannot be combined/);
});
it('should reject combining disable with all', async function () {
await expectNavyError(httpsCli([], {
disable: 'web',
all: true
}), /cannot be combined/);
});
it('should reject combining disable with issue', async function () {
await expectNavyError(httpsCli([], {
disable: 'web',
issue: 'https://host'
}), /cannot be combined/);
});
it('should reject combining setup with all', async function () {
await expectNavyError(httpsCli([], {
setup: true,
all: true
}), /Use only one/);
});
it('should reject combining setup with issue', async function () {
await expectNavyError(httpsCli([], {
setup: true,
issue: 'https://host'
}), /Use only one/);
});
it('should reject combining all with issue', async function () {
await expectNavyError(httpsCli([], {
all: true,
issue: 'https://host'
}), /Use only one/);
});
});
describe('setup mode', function () {
it('should generate the root CA and install it in the trust store', async function () {
getConfigStub.returns({
tlsRootCaDir: '/custom-ca'
});
await httpsCli([], {
setup: true
});
(0, _chai.expect)(generateRootCaStub.calledOnce).to.equal(true);
(0, _chai.expect)(installRootCaToTrustStoreStub.calledOnce).to.equal(true);
(0, _chai.expect)(installRootCaToTrustStoreStub.firstCall.args[0]).to.equal('/custom-ca/ca.crt');
const printed = consoleLogStub.getCalls().map(c => c.args[0] || '').join('\n');
(0, _chai.expect)(printed).to.contain('trust store');
});
it('should reject service names with setup', async function () {
let caught;
try {
await httpsCli(['web'], {
setup: true
});
} catch (err) {
caught = err;
}
(0, _chai.expect)(caught).to.be.instanceof(_errors.NavyError);
(0, _chai.expect)(caught.message).to.contain('does not take service names');
});
});
describe('all mode', function () {
it('should enable HTTPS for every available service', async function () {
await httpsCli([], {
all: true,
navy: 'env-1'
});
(0, _chai.expect)(createCertStub.callCount).to.equal(2);
(0, _chai.expect)(reconfigureHTTPProxyStub.calledOnce).to.equal(true);
const printed = consoleLogStub.getCalls().map(c => c.args[0] || '').join('\n');
(0, _chai.expect)(printed).to.contain('web');
(0, _chai.expect)(printed).to.contain('api');
});
it('should generate the root CA when cert files are missing', async function () {
fsStub.existsSync.callsFake(p => !p.endsWith('ca.crt') && !p.endsWith('ca.key'));
await httpsCli([], {
all: true,
navy: 'env-1'
});
(0, _chai.expect)(generateRootCaStub.calledOnce).to.equal(true);
});
it('should warn and return when no services are available', async function () {
navyStub.getAvailableServiceNames.resolves([]);
await httpsCli([], {
all: true,
navy: 'env-1'
});
(0, _chai.expect)(createCertStub.called).to.equal(false);
const printed = consoleLogStub.getCalls().map(c => c.args[0] || '').join('\n');
(0, _chai.expect)(printed).to.contain('No services found');
});
it('should reject service names with all', async function () {
let caught;
try {
await httpsCli(['web'], {
all: true
});
} catch (err) {
caught = err;
}
(0, _chai.expect)(caught).to.be.instanceof(_errors.NavyError);
(0, _chai.expect)(caught.message).to.contain('does not take service names');
});
});
describe('issue mode', function () {
it('should issue a leaf cert for an external URL', async function () {
const cwdStub = sandbox.stub(process, 'cwd').returns('/current');
await httpsCli([], {
issue: 'https://external.local',
navy: 'env-1'
});
(0, _chai.expect)(issueLeafCertForUrlStub.calledOnce).to.equal(true);
(0, _chai.expect)(issueLeafCertForUrlStub.firstCall.args).to.eql(['https://external.local', '/current']);
const printed = consoleLogStub.getCalls().map(c => c.args[0] || '').join('\n');
(0, _chai.expect)(printed).to.contain('/out/host.crt');
(0, _chai.expect)(printed).to.contain('/out/host.key');
(0, _chai.expect)(printed).to.contain('/out/navy-root-ca.crt');
cwdStub.restore();
});
it('should honour issueOut when provided', async function () {
await httpsCli([], {
issue: 'https://external.local',
issueOut: '/custom/out'
});
(0, _chai.expect)(issueLeafCertForUrlStub.firstCall.args[1]).to.equal('/custom/out');
});
it('should reject service names with issue', async function () {
let caught;
try {
await httpsCli(['web'], {
issue: 'https://external.local'
});
} catch (err) {
caught = err;
}
(0, _chai.expect)(caught).to.be.instanceof(_errors.NavyError);
(0, _chai.expect)(caught.message).to.contain('does not take service names');
});
});
});
});