navy
Version:
Quick and powerful development environments using Docker and Docker Compose
1,524 lines (1,520 loc) • 56.9 kB
JavaScript
"use strict";
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 */
function loadNavyModule(stubs = {}) {
return _proxyquire.default.noCallThru()('../', {
'../util/fs': {
readdirAsync: _sinon.default.stub().resolves([]),
lstatSync: _sinon.default.stub().returns({
isDirectory: () => true
}),
...stubs.fs
},
'./state': {
getState: _sinon.default.stub().resolves(null),
saveState: _sinon.default.stub().resolves(),
deleteState: _sinon.default.stub().resolves(),
pathToNavys: _sinon.default.stub().returns('/home/test/.navy/navies'),
pathToNavy: _sinon.default.stub().callsFake(n => `/home/test/.navy/navies/${n}`),
...stubs.state
},
'../driver': {
resolveDriverFromName: _sinon.default.stub().returns(null),
...stubs.driver
},
'../config-provider': {
resolveConfigProviderFromName: _sinon.default.stub().returns(null),
...stubs.configProvider
},
'../config': {
getConfig: _sinon.default.stub().returns({
externalIP: null
}),
...stubs.config
},
'./plugin-interface': {
loadPlugins: _sinon.default.stub().resolves([]),
...stubs.pluginInterface
},
'./middleware': {
middlewareRunner: _sinon.default.stub().resolves(),
...stubs.middleware
},
'../http-proxy': {
reconfigureHTTPProxy: _sinon.default.stub().resolves(),
...stubs.httpProxy
},
'../util/external-ip': {
getExternalIP: _sinon.default.stub().resolves('127.0.0.1'),
...stubs.externalIP
},
'../util/service-host': {
createUrlForService: _sinon.default.stub().resolves('http://svc.local'),
getUrlFromService: _sinon.default.stub().returns(null),
...stubs.serviceHost
}
});
}
function makeDriver(overrides = {}) {
return {
launch: _sinon.default.stub().resolves(),
destroy: _sinon.default.stub().resolves(),
ps: _sinon.default.stub().resolves([]),
start: _sinon.default.stub().resolves(),
stop: _sinon.default.stub().resolves(),
restart: _sinon.default.stub().resolves(),
kill: _sinon.default.stub().resolves(),
rm: _sinon.default.stub().resolves(),
update: _sinon.default.stub().resolves(),
spawnLogStream: _sinon.default.stub().resolves(),
port: _sinon.default.stub().resolves(null),
writeConfig: _sinon.default.stub().resolves(),
getConfig: _sinon.default.stub().resolves({
services: {}
}),
getLaunchedServiceNames: _sinon.default.stub().resolves([]),
getAvailableServiceNames: _sinon.default.stub().resolves([]),
...overrides
};
}
describe('navy/index', function () {
describe('Navy class', function () {
it('should normalise the navy name and store it', function () {
const {
Navy
} = loadNavyModule();
const navy = new Navy('My Env-1');
(0, _chai.expect)(navy.name).to.equal('My Env-1');
(0, _chai.expect)(navy.normalisedName).to.equal('MyEnv1');
});
});
describe('getNavy', function () {
it('should return a Navy instance for the given name', function () {
const {
getNavy
} = loadNavyModule();
const navy = getNavy('myenv');
(0, _chai.expect)(navy.name).to.equal('myenv');
});
it('should throw when navyName is not provided', function () {
const {
getNavy
} = loadNavyModule();
(0, _chai.expect)(() => getNavy()).to.throw(/NO_NAVY_PROVIDED/);
(0, _chai.expect)(() => getNavy(null)).to.throw(/NO_NAVY_PROVIDED/);
(0, _chai.expect)(() => getNavy('')).to.throw(/NO_NAVY_PROVIDED/);
});
});
describe('isInitialised', function () {
it('should return true when state exists', async function () {
const {
Navy
} = loadNavyModule({
state: {
getState: _sinon.default.stub().resolves({
driver: 'docker-compose'
})
}
});
(0, _chai.expect)(await new Navy('env').isInitialised()).to.equal(true);
});
it('should return false when state is null', async function () {
const {
Navy
} = loadNavyModule({
state: {
getState: _sinon.default.stub().resolves(null)
}
});
(0, _chai.expect)(await new Navy('env').isInitialised()).to.equal(false);
});
});
describe('getState/saveState/initialise/delete', function () {
it('should delegate getState to state module', async function () {
const stub = _sinon.default.stub().resolves({
driver: 'docker-compose'
});
const {
Navy
} = loadNavyModule({
state: {
getState: stub
}
});
const result = await new Navy('env').getState();
(0, _chai.expect)(result).to.eql({
driver: 'docker-compose'
});
(0, _chai.expect)(stub.firstCall.args[0]).to.equal('env');
});
it('saveState should write state then run middleware', async function () {
const saveStub = _sinon.default.stub().resolves();
const middlewareStub = _sinon.default.stub().resolves();
const {
Navy
} = loadNavyModule({
state: {
saveState: saveStub
},
middleware: {
middlewareRunner: middlewareStub
}
});
const navy = new Navy('env');
const state = {
driver: 'docker-compose'
};
await navy.saveState(state);
(0, _chai.expect)(saveStub.calledWith('env', state)).to.equal(true);
(0, _chai.expect)(middlewareStub.calledOnce).to.equal(true);
});
it('initialise should write state with driver=docker-compose and provided opts', async function () {
const saveStub = _sinon.default.stub().resolves();
const {
Navy
} = loadNavyModule({
state: {
saveState: saveStub
}
});
await new Navy('env').initialise({
services: {
foo: {}
},
configProvider: 'filesystem'
});
const writtenState = saveStub.firstCall.args[1];
(0, _chai.expect)(writtenState).to.eql({
services: {
foo: {}
},
configProvider: 'filesystem',
driver: 'docker-compose'
});
});
it('delete should call deleteState', async function () {
const stub = _sinon.default.stub().resolves();
const {
Navy
} = loadNavyModule({
state: {
deleteState: stub
}
});
await new Navy('env').delete();
(0, _chai.expect)(stub.calledWith('env')).to.equal(true);
});
});
describe('getDriver', function () {
it('should return null when state is missing', async function () {
const {
Navy
} = loadNavyModule();
(0, _chai.expect)(await new Navy('env').getDriver()).to.equal(null);
});
it('should return null when state has no driver', async function () {
const {
Navy
} = loadNavyModule({
state: {
getState: _sinon.default.stub().resolves({})
}
});
(0, _chai.expect)(await new Navy('env').getDriver()).to.equal(null);
});
it('should return null when the driver name is unknown', async function () {
const {
Navy
} = loadNavyModule({
state: {
getState: _sinon.default.stub().resolves({
driver: 'unknown'
})
},
driver: {
resolveDriverFromName: _sinon.default.stub().returns(null)
}
});
(0, _chai.expect)(await new Navy('env').getDriver()).to.equal(null);
});
it('should instantiate and return the driver from resolveDriverFromName', async function () {
const driverInstance = makeDriver();
const factory = _sinon.default.stub().returns(driverInstance);
const {
Navy
} = loadNavyModule({
state: {
getState: _sinon.default.stub().resolves({
driver: 'docker-compose'
})
},
driver: {
resolveDriverFromName: _sinon.default.stub().returns(factory)
}
});
const navy = new Navy('env');
(0, _chai.expect)(await navy.getDriver()).to.equal(driverInstance);
(0, _chai.expect)(factory.firstCall.args[0]).to.equal(navy);
});
});
describe('safeGetDriver', function () {
it('should throw NavyNotInitialisedError when not initialised', async function () {
const {
Navy
} = loadNavyModule();
let caught;
try {
await new Navy('env').safeGetDriver();
} catch (e) {
caught = e;
}
(0, _chai.expect)(caught).to.be.instanceof(_errors.NavyNotInitialisedError);
});
it('should throw an invariant violation when state exists but no driver could be resolved', async function () {
const {
Navy
} = loadNavyModule({
state: {
getState: _sinon.default.stub().resolves({
driver: 'unknown'
})
},
driver: {
resolveDriverFromName: _sinon.default.stub().returns(null)
}
});
let caught;
try {
await new Navy('env').safeGetDriver();
} catch (e) {
caught = e;
}
(0, _chai.expect)(caught.message).to.match(/NO_DRIVER/);
});
it('should return the driver when initialised and resolvable', async function () {
const driverInstance = makeDriver();
const {
Navy
} = loadNavyModule({
state: {
getState: _sinon.default.stub().resolves({
driver: 'docker-compose'
})
},
driver: {
resolveDriverFromName: _sinon.default.stub().returns(() => driverInstance)
}
});
(0, _chai.expect)(await new Navy('env').safeGetDriver()).to.equal(driverInstance);
});
});
describe('getConfigProvider', function () {
it('should return null when state has no configProvider', async function () {
const {
Navy
} = loadNavyModule({
state: {
getState: _sinon.default.stub().resolves({})
}
});
(0, _chai.expect)(await new Navy('env').getConfigProvider()).to.equal(null);
});
it('should return null when state is missing', async function () {
const {
Navy
} = loadNavyModule();
(0, _chai.expect)(await new Navy('env').getConfigProvider()).to.equal(null);
});
it('should return null when configProvider is unknown', async function () {
const {
Navy
} = loadNavyModule({
state: {
getState: _sinon.default.stub().resolves({
configProvider: 'unknown'
})
},
configProvider: {
resolveConfigProviderFromName: _sinon.default.stub().returns(null)
}
});
(0, _chai.expect)(await new Navy('env').getConfigProvider()).to.equal(null);
});
it('should instantiate the config provider from resolveConfigProviderFromName', async function () {
const provider = {
getNavyPath: async () => '/path'
};
const factory = _sinon.default.stub().returns(provider);
const {
Navy
} = loadNavyModule({
state: {
getState: _sinon.default.stub().resolves({
configProvider: 'filesystem'
})
},
configProvider: {
resolveConfigProviderFromName: _sinon.default.stub().returns(factory)
}
});
(0, _chai.expect)(await new Navy('env').getConfigProvider()).to.equal(provider);
});
});
describe('getNavyFile', function () {
it('should throw NavyNotInitialisedError when not initialised', async function () {
const {
Navy
} = loadNavyModule();
let caught;
try {
await new Navy('env').getNavyFile();
} catch (e) {
caught = e;
}
(0, _chai.expect)(caught).to.be.instanceof(_errors.NavyNotInitialisedError);
});
it('should throw NO_CONFIG_PROVIDER when initialised but no provider', async function () {
const {
Navy
} = loadNavyModule({
state: {
getState: _sinon.default.stub().resolves({})
}
});
let caught;
try {
await new Navy('env').getNavyFile();
} catch (e) {
caught = e;
}
(0, _chai.expect)(caught.message).to.match(/NO_CONFIG_PROVIDER/);
});
it('should require the navy file path and return its module export', async function () {
const fakePath = '/abs/Navyfile.js';
const navyFile = {
plugins: ['p']
};
const provider = {
getNavyFilePath: async () => fakePath
};
const {
Navy
} = loadNavyModule({
state: {
getState: _sinon.default.stub().resolves({
configProvider: 'filesystem'
})
},
configProvider: {
resolveConfigProviderFromName: _sinon.default.stub().returns(() => provider)
}
});
const navy = new Navy('env');
const proxiedRequire = require('proxyquire').noCallThru();
const NavyMod = proxiedRequire('../', {
'../util/fs': {
readdirAsync: _sinon.default.stub().resolves([]),
lstatSync: _sinon.default.stub().returns({
isDirectory: () => true
})
},
'./state': {
getState: _sinon.default.stub().resolves({
configProvider: 'filesystem'
}),
saveState: _sinon.default.stub(),
deleteState: _sinon.default.stub(),
pathToNavys: _sinon.default.stub().returns('/'),
pathToNavy: _sinon.default.stub()
},
'../driver': {
resolveDriverFromName: _sinon.default.stub().returns(null)
},
'../config-provider': {
resolveConfigProviderFromName: _sinon.default.stub().returns(() => provider)
},
'../config': {
getConfig: _sinon.default.stub().returns({
externalIP: null
})
},
'./plugin-interface': {
loadPlugins: _sinon.default.stub().resolves([])
},
'./middleware': {
middlewareRunner: _sinon.default.stub().resolves()
},
'../http-proxy': {
reconfigureHTTPProxy: _sinon.default.stub().resolves()
},
'../util/external-ip': {
getExternalIP: _sinon.default.stub().resolves('127.0.0.1')
},
'../util/service-host': {
createUrlForService: _sinon.default.stub(),
getUrlFromService: _sinon.default.stub()
},
[fakePath]: navyFile
});
const fresh = new NavyMod.Navy('env');
(0, _chai.expect)(await fresh.getNavyFile()).to.equal(navyFile);
// also check that not-required path returns null
provider.getNavyFilePath = async () => '/does-not-exist/Navyfile.js';
(0, _chai.expect)(await navy.getNavyFile()).to.equal(null);
});
});
describe('ensurePluginsLoaded', function () {
it('should be a no-op the second time when plugins are already loaded', async function () {
const loadStub = _sinon.default.stub().resolves([]);
const {
Navy
} = loadNavyModule({
pluginInterface: {
loadPlugins: loadStub
}
});
const navy = new Navy('env');
navy.getNavyFile = _sinon.default.stub().resolves({
plugins: []
});
await navy.ensurePluginsLoaded();
await navy.ensurePluginsLoaded();
(0, _chai.expect)(loadStub.calledOnce).to.equal(true);
(0, _chai.expect)(navy._pluginsLoaded).to.equal(true);
});
it('should not load plugins when navyFile is null', async function () {
const loadStub = _sinon.default.stub().resolves([]);
const provider = {
getNavyFilePath: async () => '/missing/Navyfile.js'
};
const {
Navy
} = loadNavyModule({
state: {
getState: _sinon.default.stub().resolves({
configProvider: 'filesystem'
})
},
configProvider: {
resolveConfigProviderFromName: _sinon.default.stub().returns(() => provider)
},
pluginInterface: {
loadPlugins: loadStub
}
});
await new Navy('env').ensurePluginsLoaded();
(0, _chai.expect)(loadStub.called).to.equal(false);
});
it('should call loadPlugins and mark _pluginsLoaded when navyFile is returned', async function () {
const loadStub = _sinon.default.stub().resolves([]);
const {
Navy
} = loadNavyModule({
pluginInterface: {
loadPlugins: loadStub
}
});
const navy = new Navy('env');
const navyFile = {
plugins: ['my-plugin']
};
navy.getNavyFile = _sinon.default.stub().resolves(navyFile);
await navy.ensurePluginsLoaded();
(0, _chai.expect)(loadStub.calledOnce).to.equal(true);
(0, _chai.expect)(loadStub.firstCall.args[0]).to.equal(navy);
(0, _chai.expect)(loadStub.firstCall.args[1]).to.equal(navyFile);
(0, _chai.expect)(navy._pluginsLoaded).to.equal(true);
});
});
describe('registerCommand / invokeCommand', function () {
it('should run the registered action when invoked', async function () {
const {
Navy
} = loadNavyModule();
const navy = new Navy('env');
const action = _sinon.default.stub().resolves();
navy.registerCommand('foo', action);
await navy.invokeCommand('foo', ['a', 'b']);
(0, _chai.expect)(action.calledOnce).to.equal(true);
(0, _chai.expect)(action.firstCall.args[0]).to.eql(['a', 'b']);
});
it('should throw a NavyError for unknown commands', async function () {
const {
Navy
} = loadNavyModule();
let caught;
try {
await new Navy('env').invokeCommand('missing', []);
} catch (e) {
caught = e;
}
(0, _chai.expect)(caught).to.be.instanceof(_errors.NavyError);
(0, _chai.expect)(caught.message).to.contain('Unknown command "missing"');
});
});
describe('registerMiddleware', function () {
it('should append the middleware to _registeredMiddleware', function () {
const {
Navy
} = loadNavyModule();
const navy = new Navy('env');
const fn = () => null;
navy.registerMiddleware(fn);
(0, _chai.expect)(navy._registeredMiddleware).to.eql([fn]);
});
});
describe('launch', function () {
function setupForLaunch({
available = ['api', 'web'],
launched = []
} = {}) {
const driver = makeDriver({
getAvailableServiceNames: _sinon.default.stub().resolves(available),
getLaunchedServiceNames: _sinon.default.stub().resolves(launched)
});
const provider = {
getNavyFilePath: async () => '/missing'
};
const reconfigureHTTPProxy = _sinon.default.stub().resolves();
const {
Navy
} = loadNavyModule({
state: {
getState: _sinon.default.stub().resolves({
driver: 'docker-compose',
configProvider: 'filesystem'
})
},
driver: {
resolveDriverFromName: _sinon.default.stub().returns(() => driver)
},
configProvider: {
resolveConfigProviderFromName: _sinon.default.stub().returns(() => provider)
},
httpProxy: {
reconfigureHTTPProxy
}
});
return {
driver,
Navy,
reconfigureHTTPProxy
};
}
it('should default to launching the currently launched services when no list is given', async function () {
const {
driver,
Navy
} = setupForLaunch({
available: ['api'],
launched: ['api']
});
await new Navy('env').launch();
(0, _chai.expect)(driver.launch.calledOnce).to.equal(true);
(0, _chai.expect)(driver.launch.firstCall.args[0]).to.eql(['api']);
});
it('should filter out unknown service names with a warning', async function () {
const log = _sinon.default.stub(console, 'log');
try {
const {
driver,
Navy
} = setupForLaunch({
available: ['api']
});
await new Navy('env').launch(['api', 'unknown']);
(0, _chai.expect)(driver.launch.firstCall.args[0]).to.eql(['api']);
(0, _chai.expect)(log.calledWith('Unknown service name: unknown')).to.equal(true);
} finally {
log.restore();
}
});
it('should return early without launching when all requested services are invalid', async function () {
const log = _sinon.default.stub(console, 'log');
try {
const {
driver,
Navy
} = setupForLaunch({
available: ['api']
});
await new Navy('env').launch(['unknown1', 'unknown2']);
(0, _chai.expect)(driver.launch.called).to.equal(false);
} finally {
log.restore();
}
});
it('should reconfigure the HTTP proxy after launch', async function () {
const {
Navy,
reconfigureHTTPProxy
} = setupForLaunch({
available: ['api'],
launched: ['api']
});
await new Navy('env').launch(['api']);
(0, _chai.expect)(reconfigureHTTPProxy.calledOnce).to.equal(true);
});
});
describe('reconfigure / relaunch', function () {
it('reconfigure should be a no-op when no services are launched', async function () {
const {
Navy
} = loadNavyModule({
state: {
getState: _sinon.default.stub().resolves({
driver: 'docker-compose'
})
},
driver: {
resolveDriverFromName: _sinon.default.stub().returns(() => makeDriver({
getLaunchedServiceNames: _sinon.default.stub().resolves([])
}))
}
});
await new Navy('env').reconfigure();
});
it('reconfigure should call launch when there are running services', async function () {
const driver = makeDriver({
getLaunchedServiceNames: _sinon.default.stub().resolves(['api']),
getAvailableServiceNames: _sinon.default.stub().resolves(['api'])
});
const provider = {
getNavyFilePath: async () => '/missing'
};
const {
Navy
} = loadNavyModule({
state: {
getState: _sinon.default.stub().resolves({
driver: 'docker-compose',
configProvider: 'filesystem'
})
},
driver: {
resolveDriverFromName: _sinon.default.stub().returns(() => driver)
},
configProvider: {
resolveConfigProviderFromName: _sinon.default.stub().returns(() => provider)
}
});
await new Navy('env').reconfigure();
(0, _chai.expect)(driver.launch.calledOnce).to.equal(true);
});
it('relaunch should delegate to reconfigure', async function () {
const driver = makeDriver({
getLaunchedServiceNames: _sinon.default.stub().resolves([])
});
const {
Navy
} = loadNavyModule({
state: {
getState: _sinon.default.stub().resolves({
driver: 'docker-compose'
})
},
driver: {
resolveDriverFromName: _sinon.default.stub().returns(() => driver)
}
});
await new Navy('env').relaunch();
});
});
describe('destroy', function () {
it('should throw NavyNotInitialisedError when not initialised', async function () {
const {
Navy
} = loadNavyModule();
let caught;
try {
await new Navy('env').destroy();
} catch (e) {
caught = e;
}
(0, _chai.expect)(caught).to.be.instanceof(_errors.NavyNotInitialisedError);
});
it('should reconfigure proxy excluding self, destroy the driver, and delete state', async function () {
const driver = makeDriver();
const provider = {
getNavyFilePath: async () => '/missing'
};
const reconfigureHTTPProxy = _sinon.default.stub().resolves();
const deleteState = _sinon.default.stub().resolves();
const {
Navy
} = loadNavyModule({
state: {
getState: _sinon.default.stub().resolves({
driver: 'docker-compose',
configProvider: 'filesystem'
}),
deleteState
},
driver: {
resolveDriverFromName: _sinon.default.stub().returns(() => driver)
},
configProvider: {
resolveConfigProviderFromName: _sinon.default.stub().returns(() => provider)
},
httpProxy: {
reconfigureHTTPProxy
},
fs: {
readdirAsync: _sinon.default.stub().resolves(['env', 'other'])
}
});
await new Navy('env').destroy();
(0, _chai.expect)(reconfigureHTTPProxy.calledOnce).to.equal(true);
(0, _chai.expect)(reconfigureHTTPProxy.firstCall.args[0].navies).to.eql(['other']);
(0, _chai.expect)(driver.destroy.calledOnce).to.equal(true);
(0, _chai.expect)(deleteState.calledOnce).to.equal(true);
});
it('should pass navyFile from a remaining navy when reconfiguring the proxy', async function () {
const driver = makeDriver();
const envPath = '/abs/env/Navyfile.js';
const otherPath = '/abs/other/Navyfile.js';
const destroyedNavyFile = {
httpProxyImage: 'destroyed/proxy:1'
};
const remainingNavyFile = {
httpProxyImage: 'remaining/proxy:1',
httpProxyEnv: {
FOO: 'bar'
}
};
const createProvider = path => ({
getNavyFilePath: async () => path
});
const resolveConfigProvider = _sinon.default.stub().callsFake(() => navy => createProvider(navy.normalisedName === 'other' ? otherPath : envPath));
const reconfigureHTTPProxy = _sinon.default.stub().resolves();
const deleteState = _sinon.default.stub().resolves();
const {
Navy
} = _proxyquire.default.noCallThru()('../', {
'../util/fs': {
readdirAsync: _sinon.default.stub().resolves(['env', 'other']),
lstatSync: _sinon.default.stub().returns({
isDirectory: () => true
})
},
'./state': {
getState: _sinon.default.stub().resolves({
driver: 'docker-compose',
configProvider: 'filesystem'
}),
saveState: _sinon.default.stub().resolves(),
deleteState,
pathToNavys: _sinon.default.stub().returns('/home/test/.navy/navies'),
pathToNavy: _sinon.default.stub().callsFake(n => `/home/test/.navy/navies/${n}`)
},
'../driver': {
resolveDriverFromName: _sinon.default.stub().returns(() => driver)
},
'../config-provider': {
resolveConfigProviderFromName: resolveConfigProvider
},
'../config': {
getConfig: _sinon.default.stub().returns({
externalIP: null
})
},
'./plugin-interface': {
loadPlugins: _sinon.default.stub().resolves([])
},
'./middleware': {
middlewareRunner: _sinon.default.stub().resolves()
},
'../http-proxy': {
reconfigureHTTPProxy
},
'../util/external-ip': {
getExternalIP: _sinon.default.stub().resolves('127.0.0.1')
},
'../util/service-host': {
createUrlForService: _sinon.default.stub().resolves('http://svc.local'),
getUrlFromService: _sinon.default.stub().returns(null)
},
[envPath]: destroyedNavyFile,
[otherPath]: remainingNavyFile
});
await new Navy('env').destroy();
(0, _chai.expect)(reconfigureHTTPProxy.calledOnce).to.equal(true);
(0, _chai.expect)(reconfigureHTTPProxy.firstCall.args[0].navies).to.eql(['other']);
(0, _chai.expect)(reconfigureHTTPProxy.firstCall.args[0].navyFile).to.eql(remainingNavyFile);
});
it('should skip uninitialised remaining navies when resolving navyFile for the proxy', async function () {
const driver = makeDriver();
const otherPath = '/abs/other/Navyfile.js';
const remainingNavyFile = {
httpProxyImage: 'remaining/proxy:1'
};
const createProvider = path => ({
getNavyFilePath: async () => path
});
const resolveConfigProvider = _sinon.default.stub().callsFake(() => navy => createProvider(navy.normalisedName === 'other' ? otherPath : '/abs/env/Navyfile.js'));
const reconfigureHTTPProxy = _sinon.default.stub().resolves();
const deleteState = _sinon.default.stub().resolves();
const getState = _sinon.default.stub().callsFake(name => {
if (name === 'uninit') return Promise.resolve(null);
return Promise.resolve({
driver: 'docker-compose',
configProvider: 'filesystem'
});
});
const {
Navy
} = _proxyquire.default.noCallThru()('../', {
'../util/fs': {
readdirAsync: _sinon.default.stub().resolves(['env', 'uninit', 'other']),
lstatSync: _sinon.default.stub().returns({
isDirectory: () => true
})
},
'./state': {
getState,
saveState: _sinon.default.stub().resolves(),
deleteState,
pathToNavys: _sinon.default.stub().returns('/home/test/.navy/navies'),
pathToNavy: _sinon.default.stub().callsFake(n => `/home/test/.navy/navies/${n}`)
},
'../driver': {
resolveDriverFromName: _sinon.default.stub().returns(() => driver)
},
'../config-provider': {
resolveConfigProviderFromName: resolveConfigProvider
},
'../config': {
getConfig: _sinon.default.stub().returns({
externalIP: null
})
},
'./plugin-interface': {
loadPlugins: _sinon.default.stub().resolves([])
},
'./middleware': {
middlewareRunner: _sinon.default.stub().resolves()
},
'../http-proxy': {
reconfigureHTTPProxy
},
'../util/external-ip': {
getExternalIP: _sinon.default.stub().resolves('127.0.0.1')
},
'../util/service-host': {
createUrlForService: _sinon.default.stub().resolves('http://svc.local'),
getUrlFromService: _sinon.default.stub().returns(null)
},
[otherPath]: remainingNavyFile
});
await new Navy('env').destroy();
(0, _chai.expect)(reconfigureHTTPProxy.calledOnce).to.equal(true);
(0, _chai.expect)(reconfigureHTTPProxy.firstCall.args[0].navies).to.eql(['uninit', 'other']);
(0, _chai.expect)(reconfigureHTTPProxy.firstCall.args[0].navyFile).to.eql(remainingNavyFile);
});
it('should still delete state even when driver.destroy throws', async function () {
const driver = makeDriver({
destroy: _sinon.default.stub().rejects(new Error('boom'))
});
const provider = {
getNavyFilePath: async () => '/missing'
};
const deleteState = _sinon.default.stub().resolves();
const {
Navy
} = loadNavyModule({
state: {
getState: _sinon.default.stub().resolves({
driver: 'docker-compose',
configProvider: 'filesystem'
}),
deleteState
},
driver: {
resolveDriverFromName: _sinon.default.stub().returns(() => driver)
},
configProvider: {
resolveConfigProviderFromName: _sinon.default.stub().returns(() => provider)
},
httpProxy: {
reconfigureHTTPProxy: _sinon.default.stub().resolves()
}
});
await new Navy('env').destroy();
(0, _chai.expect)(deleteState.calledOnce).to.equal(true);
});
});
describe('start/stop/restart/kill/rm/update/spawnLogStream', function () {
function setup(driverOverrides = {}) {
const driver = makeDriver(driverOverrides);
const provider = {
getNavyFilePath: async () => '/missing'
};
const {
Navy
} = loadNavyModule({
state: {
getState: _sinon.default.stub().resolves({
driver: 'docker-compose',
configProvider: 'filesystem'
})
},
driver: {
resolveDriverFromName: _sinon.default.stub().returns(() => driver)
},
configProvider: {
resolveConfigProviderFromName: _sinon.default.stub().returns(() => provider)
}
});
return {
driver,
navy: new Navy('env')
};
}
;
[{
method: 'start',
explicit: ['api']
}, {
method: 'stop',
explicit: ['api']
}, {
method: 'restart',
explicit: ['api']
}, {
method: 'kill',
explicit: ['api']
}].forEach(({
method,
explicit
}) => {
it(`${method} should pass the explicit list through to the driver`, async function () {
const {
driver,
navy
} = setup();
await navy[method](explicit);
(0, _chai.expect)(driver[method].calledOnce).to.equal(true);
(0, _chai.expect)(driver[method].firstCall.args[0]).to.eql(explicit);
});
it(`${method} should default to currently launched services when none provided`, async function () {
const {
driver,
navy
} = setup({
getLaunchedServiceNames: _sinon.default.stub().resolves(['api', 'web'])
});
await navy[method]();
(0, _chai.expect)(driver[method].firstCall.args[0]).to.eql(['api', 'web']);
});
});
it('rm should default to currently launched services when none provided', async function () {
const {
driver,
navy
} = setup({
getLaunchedServiceNames: _sinon.default.stub().resolves(['x'])
});
await navy.rm();
(0, _chai.expect)(driver.rm.firstCall.args[0]).to.eql(['x']);
});
it('rm should accept an explicit list', async function () {
const {
driver,
navy
} = setup();
await navy.rm(['x']);
(0, _chai.expect)(driver.rm.firstCall.args[0]).to.eql(['x']);
});
it('update should default to all available services', async function () {
const {
driver,
navy
} = setup({
getAvailableServiceNames: _sinon.default.stub().resolves(['a', 'b'])
});
await navy.update();
(0, _chai.expect)(driver.update.firstCall.args[0]).to.eql(['a', 'b']);
});
it('update should accept an explicit list', async function () {
const {
driver,
navy
} = setup();
await navy.update(['x']);
(0, _chai.expect)(driver.update.firstCall.args[0]).to.eql(['x']);
});
it('spawnLogStream should default to currently launched services when none provided', async function () {
const {
driver,
navy
} = setup({
getLaunchedServiceNames: _sinon.default.stub().resolves(['l'])
});
await navy.spawnLogStream();
(0, _chai.expect)(driver.spawnLogStream.firstCall.args[0]).to.eql(['l']);
});
it('spawnLogStream should accept an explicit list', async function () {
const {
driver,
navy
} = setup();
await navy.spawnLogStream(['l']);
(0, _chai.expect)(driver.spawnLogStream.firstCall.args[0]).to.eql(['l']);
});
it('start should reconfigure the HTTP proxy', async function () {
const driver = makeDriver({
getLaunchedServiceNames: _sinon.default.stub().resolves(['api'])
});
const provider = {
getNavyFilePath: async () => '/missing'
};
const reconfigure = _sinon.default.stub().resolves();
const {
Navy
} = loadNavyModule({
state: {
getState: _sinon.default.stub().resolves({
driver: 'docker-compose',
configProvider: 'filesystem'
})
},
driver: {
resolveDriverFromName: _sinon.default.stub().returns(() => driver)
},
configProvider: {
resolveConfigProviderFromName: _sinon.default.stub().returns(() => provider)
},
httpProxy: {
reconfigureHTTPProxy: reconfigure
}
});
await new Navy('env').start(['api']);
(0, _chai.expect)(reconfigure.calledOnce).to.equal(true);
});
});
describe('useTag/resetTag/usePort/resetPort', function () {
function setupTagPort({
initialState = {
driver: 'docker-compose',
configProvider: 'filesystem',
services: {}
}
} = {}) {
const driver = makeDriver({
getAvailableServiceNames: _sinon.default.stub().resolves(['api']),
getLaunchedServiceNames: _sinon.default.stub().resolves([])
});
let currentState = initialState;
const getStateStub = _sinon.default.stub().callsFake(async () => currentState);
const saveStub = _sinon.default.stub().callsFake(async (envName, state) => {
currentState = state;
});
const provider = {
getNavyFilePath: async () => '/missing'
};
const {
Navy
} = loadNavyModule({
state: {
getState: getStateStub,
saveState: saveStub
},
driver: {
resolveDriverFromName: _sinon.default.stub().returns(() => driver)
},
configProvider: {
resolveConfigProviderFromName: _sinon.default.stub().returns(() => provider)
}
});
return {
Navy,
driver,
saveStub
};
}
it('useTag should record the tag in state, kill the service, and update it', async function () {
const {
Navy,
driver,
saveStub
} = setupTagPort();
await new Navy('env').useTag('api', 'v2');
const written = saveStub.firstCall.args[1];
(0, _chai.expect)(written.services.api._tag).to.equal('v2');
(0, _chai.expect)(driver.kill.firstCall.args[0]).to.eql(['api']);
(0, _chai.expect)(driver.update.firstCall.args[0]).to.eql(['api']);
});
// NOTE: useTag/resetTag/usePort/resetPort all start with
// `(await this.getState()) || {}` and write the result back as the new
// state. If state was never initialised, the saved state has no `driver`
// key, and the subsequent kill/launch then fails with a NO_DRIVER invariant.
// We therefore exercise these methods only with a properly initialised state.
it('useTag should preserve other services in state.services when modifying one', async function () {
const initial = {
driver: 'docker-compose',
configProvider: 'filesystem',
services: {
other: {
_tag: 'keep-me'
}
}
};
const {
Navy,
saveStub
} = setupTagPort({
initialState: initial
});
await new Navy('env').useTag('api', 'v2');
const written = saveStub.firstCall.args[1];
(0, _chai.expect)(written.services.other).to.eql({
_tag: 'keep-me'
});
(0, _chai.expect)(written.services.api._tag).to.equal('v2');
});
it('resetTag should clear the tag and relaunch the service with noDeps', async function () {
const {
Navy,
driver,
saveStub
} = setupTagPort({
initialState: {
driver: 'docker-compose',
configProvider: 'filesystem',
services: {
api: {
_tag: 'old'
}
}
}
});
await new Navy('env').resetTag('api');
(0, _chai.expect)(saveStub.firstCall.args[1].services.api._tag).to.equal(undefined);
(0, _chai.expect)(driver.launch.firstCall.args[1]).to.eql({
noDeps: true
});
});
it('resetTag should preserve services other than the one being reset', async function () {
const {
Navy,
saveStub
} = setupTagPort({
initialState: {
driver: 'docker-compose',
configProvider: 'filesystem',
services: {
api: {
_tag: 'old'
},
other: {
_tag: 'keep'
}
}
}
});
await new Navy('env').resetTag('api');
const written = saveStub.firstCall.args[1];
(0, _chai.expect)(written.services.other).to.eql({
_tag: 'keep'
});
(0, _chai.expect)(written.services.api._tag).to.equal(undefined);
});
it('usePort should record port mapping in state', async function () {
const {
Navy,
saveStub
} = setupTagPort();
await new Navy('env').usePort('api', 80, 8080);
(0, _chai.expect)(saveStub.firstCall.args[1].services.api._ports).to.eql({
80: 8080
});
});
it('usePort should merge the new port mapping with existing ports for the service', async function () {
const {
Navy,
saveStub
} = setupTagPort({
initialState: {
driver: 'docker-compose',
configProvider: 'filesystem',
services: {
api: {
_ports: {
443: 4433
}
}
}
}
});
await new Navy('env').usePort('api', 80, 8080);
const ports = saveStub.firstCall.args[1].services.api._ports;
(0, _chai.expect)(ports).to.eql({
80: 8080,
443: 4433
});
});
it('resetPort should set the port to undefined', async function () {
const {
Navy,
saveStub
} = setupTagPort({
initialState: {
driver: 'docker-compose',
configProvider: 'filesystem',
services: {
api: {
_ports: {
80: 8080
}
}
}
}
});
await new Navy('env').resetPort('api', 80);
(0, _chai.expect)(saveStub.firstCall.args[1].services.api._ports[80]).to.equal(undefined);
});
it('resetPort should preserve other ports on the same service', async function () {
const {
Navy,
saveStub
} = setupTagPort({
initialState: {
driver: 'docker-compose',
configProvider: 'filesystem',
services: {
api: {
_ports: {
80: 8080,
443: 4433
}
}
}
}
});
await new Navy('env').resetPort('api', 80);
const ports = saveStub.firstCall.args[1].services.api._ports;
(0, _chai.expect)(ports[80]).to.equal(undefined);
(0, _chai.expect)(ports[443]).to.equal(4433);
});
it('useTag should throw NavyNotInitialisedError when state is missing', async function () {
const {
Navy
} = loadNavyModule();
let caught;
try {
await new Navy('env').useTag('api', 'v1');
} catch (e) {
caught = e;
}
(0, _chai.expect)(caught).to.be.instanceof(_errors.NavyNotInitialisedError);
});
it('resetTag should throw NavyNotInitialisedError when state is missing', async function () {
const {
Navy
} = loadNavyModule();
let caught;
try {
await new Navy('env').resetTag('api');
} catch (e) {
caught = e;
}
(0, _chai.expect)(caught).to.be.instanceof(_errors.NavyNotInitialisedError);
});
it('usePort should throw NavyNotInitialisedError when state is missing', async function () {
const {
Navy
} = loadNavyModule();
let caught;
try {
await new Navy('env').usePort('api', 80, 8080);
} catch (e) {
caught = e;
}
(0, _chai.expect)(caught).to.be.instanceof(_errors.NavyNotInitialisedError);
});
it('resetPort should throw NavyNotInitialisedError when state is missing', async function () {
const {
Navy
} = loadNavyModule();
let caught;
try {
await new Navy('env').resetPort('api', 80);
} catch (e) {
caught = e;
}
(0, _chai.expect)(caught).to.be.instanceof(_errors.NavyNotInitialisedError);
});
it('useTag should treat missing state.services as an empty services map', async function () {
const {
Navy,
saveStub
} = setupTagPort({
initialState: {
driver: 'docker-compose',
configProvider: 'filesystem'
}
});
await new Navy('env').useTag('api', 'v2');
(0, _chai.expect)(saveStub.firstCall.args[1].services.api._tag).to.equal('v2');
});
it('resetTag should treat missing state.services as an empty services map', async function () {
const {
Navy,
saveStub
} = setupTagPort({
initialState: {
driver: 'docker-compose',
configProvider: 'filesystem'
}
});
await new Navy('env').resetTag('api');
(0, _chai.expect)(saveStub.firstCall.args[1].services.api._tag).to.equal(undefined);
});
it('usePort should treat missing state.services as an empty services map', async function () {
const {
Navy,
saveStub
} = setupTagPort({
initialState: {
driver: 'docker-compose',
configProvider: 'filesystem'
}
});
await new Navy('env').usePort('api', 80, 8080);
(0, _chai.expect)(saveStub.firstCall.args[1].services.api._ports).to.eql({
80: 8080
});
});
it('resetPort should treat missing state.services as an empty services map', async function () {
const {
Navy,
saveStub
} = setupTagPort({
initialState: {
driver: 'docker-compose',
configProvider: 'filesystem'
}
});
await new Navy('env').resetPort('api', 80);
(0, _chai.expect)(saveStub.firstCall.args[1].services.api._ports[80]).to.equal(undefined);
});
});
describe('externalIP', function () {
it('should return the IP from getExternalIP using config.externalIP', async function () {
const {
Navy
} = loadNavyModule({
config: {
getConfig: _sinon.default.stub().returns({
externalIP: 'configured.host'
})
},
externalIP: {
getExternalIP: _sinon.default.stub().resolves('1.2.3.4')
}
});
(0, _chai.expect)(await new Navy('env').externalIP()).to.equal('1.2.3.4');
});
});
describe('port', function () {
it('should delegate to driver.port with the privatePort and index defaulting to 1', async function () {
const driver = makeDriver({
port: _sinon.default.stub().resolves(8080)
});
const {
Navy
} = loadNavyModule({
state: {
getState: _sinon.default.stub().resolves({
driver: 'docker-compose'
})
},
driver: {
resolveDriverFromName: _sinon.default.stub().returns(() => driver)
}
});
(0, _chai.expect)(await new Navy('env').port('api', 80)).to.equal(8080);
(0, _chai.expect)(driver.port.firstCall.args).to.eql(['api', 80, 1]);
});
});
describe('url', function () {
it('should return getUrlFromService when there is a running service URL', async function () {
const driver = makeDriver({
ps: _sinon.default.stub().resolves([{
name: 'api'
}])
});
const {
Navy
} = loadNavyModule({
state: {
getState: _sinon.default.stub().resolves({
driver: 'docker-compose'
})
},
driver: {
resolveDriverFromName: _sinon.default.stub().returns(() => driver)
},
serviceHost: {
getUrlFromService: _sinon.default.stub().returns('http://from-service'),
createUrlForService: _sinon.default.stub().resolves('http://fallback')
}
});
(0, _chai.expect)(await new Navy('env').url('api')).to.equal('http://from-service');
});
it('should fall back to createUrlForService when getUrlFromService returns null', async function () {
const driver = makeDriver({
ps: _sinon.default.stub().resolves([{
name: 'api'
}])
});
const {
Navy
} = loadNavyModule({
state: {
getState: _sinon.default.stub().resolves({
driver: 'docker-compose'
})
},
driver: {
resolveDriverFromName: _sinon.default.stub().returns(() => driver)
},
serviceHost: {
getUrlFromService: _sinon.default.stub().returns(null),
createUrlForService: _sinon.default.stub().resolves('http://fallback')
},
externalIP: {
getExternalIP: _sinon.default.stub().resolves('1.2.3.4')
}
});
(0, _chai.expect)(await new Navy('env').url('api')).to.equal('http://fallback');
});
});
describe('ps / getLaunchedServiceNames / getAvailableServiceNames', function () {
it('ps should delegate to the driver', async function () {
const driver = makeDriver({
ps: _sinon.default.stub().resolves([{
name: 'api'
}])
});
const {
Navy
} = loadNavyModule({
state: {
getState: _sinon.default.stub().resolves({
driver: 'docker-compose'
})
},
driver: {
resolveDriverFromName: _sinon.default.stub().returns(() => driver)
}
});
(0, _chai.expect)(await new Navy('env').ps()).to.eql([{
name: 'api'
}]);
});
it('g