UNPKG

navy

Version:

Quick and powerful development environments using Docker and Docker Compose

1,524 lines (1,520 loc) 56.9 kB
"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