UNPKG

navy

Version:

Quick and powerful development environments using Docker and Docker Compose

587 lines (583 loc) 23.6 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 */ describe('cli/program', function () { let sandbox; let fakeProgram; let actionsByCommand; let getConfigStub; let startDriverLoggingStub; let stopDriverLoggingStub; let getImportCommandLineOptionsStub; let getNavyStub; let navyStub; let consoleLogStub; let consoleErrorStub; let processExitStub; let processOnStub; let originalEnvName; let helpHandlersByCommand; function loadModule() { actionsByCommand = {}; helpHandlersByCommand = {}; fakeProgram = { option: sandbox.stub().returnsThis(), enablePositionalOptions: sandbox.stub().returnsThis(), command(spec) { const name = spec.split(' ')[0]; const cmd = { option: sandbox.stub().returnsThis(), description: sandbox.stub().returnsThis(), action(handler) { actionsByCommand[name] = handler; return cmd; }, on(event, handler) { if (event === '--help') { helpHandlersByCommand[name] = handler; } return cmd; } }; return cmd; } }; return _proxyquire.default.noCallThru()('../program', { commander: { program: fakeProgram }, '../errors': { NavyError: _errors.NavyError }, '../config': { getConfig: getConfigStub }, '../driver-logging': { startDriverLogging: startDriverLoggingStub, stopDriverLogging: stopDriverLoggingStub }, '../config-provider': { getImportCommandLineOptions: getImportCommandLineOptionsStub }, '../navy': { getNavy: getNavyStub }, './import': () => Promise.resolve('imported'), './launch': () => Promise.resolve('launched'), './ps': () => Promise.resolve(undefined), './updates': () => Promise.resolve(undefined), './logs': () => Promise.resolve(undefined), './health': () => Promise.resolve(undefined), './wait-for-healthy': () => Promise.resolve(undefined), './https': () => Promise.resolve(undefined), './open': () => Promise.resolve(undefined), './develop': () => Promise.resolve(undefined), './live': () => Promise.resolve(undefined), './run': () => Promise.resolve(undefined), './refresh-config': () => Promise.resolve(undefined), './status': () => Promise.resolve(undefined), './doctor': () => Promise.resolve(undefined), './config/wrapper': () => Promise.resolve(undefined), './external-ip': () => Promise.resolve(undefined), './lan-ip': () => Promise.resolve(undefined), './local-ip': () => Promise.resolve(undefined) }); } beforeEach(function () { sandbox = _sinon.default.createSandbox(); getConfigStub = sandbox.stub().returns({ defaultNavy: 'dev' }); startDriverLoggingStub = sandbox.stub(); stopDriverLoggingStub = sandbox.stub(); getImportCommandLineOptionsStub = sandbox.stub().returns([['-c, --config-provider [provider]', 'Config provider to use']]); navyStub = { ensurePluginsLoaded: sandbox.stub().resolves(), emitAsync: sandbox.stub().resolves(), destroy: sandbox.stub().resolves(), kill: sandbox.stub().resolves(), start: sandbox.stub().resolves(), stop: sandbox.stub().resolves(), url: sandbox.stub().resolves('http://web'), getAvailableServiceNames: sandbox.stub().resolves(['web', 'api']) }; getNavyStub = sandbox.stub().returns(navyStub); consoleLogStub = sandbox.stub(console, 'log'); consoleErrorStub = sandbox.stub(console, 'error'); processExitStub = sandbox.stub(process, 'exit'); processOnStub = sandbox.stub(process, 'on'); originalEnvName = process.env.NAVY_NAME; }); afterEach(function () { if (originalEnvName === undefined) { delete process.env.NAVY_NAME; } else { process.env.NAVY_NAME = originalEnvName; } sandbox.restore(); }); describe('module side effects', function () { it('should register all known commands on the program', function () { loadModule(); (0, _chai.expect)(actionsByCommand).to.have.keys('import', 'launch', 'destroy', 'delete', 'ps', 'start', 'stop', 'restart', 'kill', 'rm', 'update', 'updates', 'logs', 'health', 'wait-for-healthy', 'use-tag', 'reset-tag', 'https', 'use-port', 'reset-port', 'url', 'open', 'port', 'available-services', 'develop', 'live', 'run', 'refresh-config', 'status', 'doctor', 'config', 'external-ip', 'use-lan-ip', 'use-local-ip'); }); it('should derive the default navy from NAVY_NAME env when set', function () { process.env.NAVY_NAME = 'foo-env'; loadModule(); (0, _chai.expect)(actionsByCommand.import).to.be.a('function'); }); it('should fall back to getConfig().defaultNavy when env is not set', function () { delete process.env.NAVY_NAME; loadModule(); (0, _chai.expect)(getConfigStub.called).to.equal(true); }); it('should add import command-line options from getImportCommandLineOptions', function () { loadModule(); (0, _chai.expect)(getImportCommandLineOptionsStub.calledOnce).to.equal(true); }); it('should export the program as default', function () { const mod = loadModule(); (0, _chai.expect)(mod).to.equal(fakeProgram); }); it('should print examples when each command --help handler is invoked', function () { loadModule(); Object.keys(helpHandlersByCommand).forEach(name => { helpHandlersByCommand[name](); }); (0, _chai.expect)(consoleLogStub.called).to.equal(true); }); it('should register a global -e, --navy option on the root program', function () { loadModule(); (0, _chai.expect)(fakeProgram.option.calledWith('-e, --navy [env]', _sinon.default.match.string, 'dev')).to.equal(true); }); it('should enable positional options so unknown subcommand options are rejected', function () { loadModule(); (0, _chai.expect)(fakeProgram.enablePositionalOptions.calledOnce).to.equal(true); }); }); describe('lazyRequire wrapped action', function () { it('should pass through arguments to the underlying module', async function () { loadModule(); const result = actionsByCommand.import({ navy: 'env-1' }); return (0, _chai.expect)(result).to.exist; }); it('should call stopDriverLogging and prettyPrint when the action throws a NavyError', async function () { const err = new _errors.NavyError('boom'); const prettyPrintStub = sandbox.stub(err, 'prettyPrint'); _proxyquire.default.noCallThru()('../program', { commander: { program: createCapturingProgram() }, '../errors': { NavyError: _errors.NavyError }, '../config': { getConfig: getConfigStub }, '../driver-logging': { startDriverLogging: startDriverLoggingStub, stopDriverLogging: stopDriverLoggingStub }, '../config-provider': { getImportCommandLineOptions: getImportCommandLineOptionsStub }, '../navy': { getNavy: getNavyStub }, './import': () => Promise.reject(err), './launch': () => Promise.resolve(), './ps': () => Promise.resolve(), './updates': () => Promise.resolve(), './logs': () => Promise.resolve(), './health': () => Promise.resolve(), './wait-for-healthy': () => Promise.resolve(), './https': () => Promise.resolve(), './open': () => Promise.resolve(), './develop': () => Promise.resolve(), './live': () => Promise.resolve(), './run': () => Promise.resolve(), './refresh-config': () => Promise.resolve(), './status': () => Promise.resolve(), './doctor': () => Promise.resolve(), './config/wrapper': () => Promise.resolve(), './external-ip': () => Promise.resolve(), './lan-ip': () => Promise.resolve(), './local-ip': () => Promise.resolve() }); capturedActions.import({ navy: 'env-1' }); await new Promise(resolve => setImmediate(resolve)); (0, _chai.expect)(prettyPrintStub.calledOnce).to.equal(true); (0, _chai.expect)(processExitStub.calledWith(1)).to.equal(true); (0, _chai.expect)(stopDriverLoggingStub.calledWith({ success: false })).to.equal(true); }); it('should pretty-print invariant violations with troubleshooting hint', async function () { const err = new Error('CODE: something went wrong'); err.name = 'Invariant Violation'; _proxyquire.default.noCallThru()('../program', { commander: { program: createCapturingProgram() }, '../errors': { NavyError: _errors.NavyError }, '../config': { getConfig: getConfigStub }, '../driver-logging': { startDriverLogging: startDriverLoggingStub, stopDriverLogging: stopDriverLoggingStub }, '../config-provider': { getImportCommandLineOptions: getImportCommandLineOptionsStub }, '../navy': { getNavy: getNavyStub }, './import': () => Promise.reject(err), './launch': () => Promise.resolve(), './ps': () => Promise.resolve(), './updates': () => Promise.resolve(), './logs': () => Promise.resolve(), './health': () => Promise.resolve(), './wait-for-healthy': () => Promise.resolve(), './https': () => Promise.resolve(), './open': () => Promise.resolve(), './develop': () => Promise.resolve(), './live': () => Promise.resolve(), './run': () => Promise.resolve(), './refresh-config': () => Promise.resolve(), './status': () => Promise.resolve(), './doctor': () => Promise.resolve(), './config/wrapper': () => Promise.resolve(), './external-ip': () => Promise.resolve(), './lan-ip': () => Promise.resolve(), './local-ip': () => Promise.resolve() }); capturedActions.import({ navy: 'env-1' }); await new Promise(resolve => setImmediate(resolve)); const printed = consoleLogStub.getCalls().map(c => c.args[0] || '').join('\n'); (0, _chai.expect)(printed).to.contain('Invariant Violation'); (0, _chai.expect)(printed).to.contain('navy doctor'); (0, _chai.expect)(processExitStub.calledWith(1)).to.equal(true); }); it('should print stack for non-NavyError, non-Invariant errors', async function () { const err = new Error('something broke'); _proxyquire.default.noCallThru()('../program', { commander: { program: createCapturingProgram() }, '../errors': { NavyError: _errors.NavyError }, '../config': { getConfig: getConfigStub }, '../driver-logging': { startDriverLogging: startDriverLoggingStub, stopDriverLogging: stopDriverLoggingStub }, '../config-provider': { getImportCommandLineOptions: getImportCommandLineOptionsStub }, '../navy': { getNavy: getNavyStub }, './import': () => Promise.reject(err), './launch': () => Promise.resolve(), './ps': () => Promise.resolve(), './updates': () => Promise.resolve(), './logs': () => Promise.resolve(), './health': () => Promise.resolve(), './wait-for-healthy': () => Promise.resolve(), './https': () => Promise.resolve(), './open': () => Promise.resolve(), './develop': () => Promise.resolve(), './live': () => Promise.resolve(), './run': () => Promise.resolve(), './refresh-config': () => Promise.resolve(), './status': () => Promise.resolve(), './doctor': () => Promise.resolve(), './config/wrapper': () => Promise.resolve(), './external-ip': () => Promise.resolve(), './lan-ip': () => Promise.resolve(), './local-ip': () => Promise.resolve() }); capturedActions.import({ navy: 'env-1' }); await new Promise(resolve => setImmediate(resolve)); (0, _chai.expect)(consoleErrorStub.calledWith(err.stack)).to.equal(true); }); // NOTE: The internal wrapper in program.js does `if (res.catch)` without // null-checking, so passing a non-promise return value would throw. All // production action handlers return promises, so this code path is not // exercised in practice. We do not test it because asserting that // behaviour would lock in a latent bug. }); describe('normaliseLazyRequireArgs (via lazyRequire wrapped action)', function () { let importSpy; function loadWithImportSpy() { importSpy = sandbox.stub().resolves('imported'); return _proxyquire.default.noCallThru()('../program', { commander: { program: createCapturingProgram() }, '../errors': { NavyError: _errors.NavyError }, '../config': { getConfig: getConfigStub }, '../driver-logging': { startDriverLogging: startDriverLoggingStub, stopDriverLogging: stopDriverLoggingStub }, '../config-provider': { getImportCommandLineOptions: getImportCommandLineOptionsStub }, '../navy': { getNavy: getNavyStub }, './import': importSpy, './launch': () => Promise.resolve(), './ps': () => Promise.resolve(), './updates': () => Promise.resolve(), './logs': () => Promise.resolve(), './health': () => Promise.resolve(), './wait-for-healthy': () => Promise.resolve(), './https': () => Promise.resolve(), './open': () => Promise.resolve(), './develop': () => Promise.resolve(), './live': () => Promise.resolve(), './run': () => Promise.resolve(), './refresh-config': () => Promise.resolve(), './status': () => Promise.resolve(), './doctor': () => Promise.resolve(), './config/wrapper': () => Promise.resolve(), './external-ip': () => Promise.resolve(), './lan-ip': () => Promise.resolve(), './local-ip': () => Promise.resolve() }); } it('should pass through unchanged when invoked with no arguments at all', async function () { loadWithImportSpy(); capturedActions.import(); await new Promise(resolve => setImmediate(resolve)); (0, _chai.expect)(importSpy.calledOnce).to.equal(true); (0, _chai.expect)(importSpy.firstCall.args).to.eql([]); }); it('should strip the trailing Command instance when no other args were supplied', async function () { loadWithImportSpy(); const fakeCommand = { optsWithGlobals: () => ({ navy: 'global' }) }; capturedActions.import(fakeCommand); await new Promise(resolve => setImmediate(resolve)); (0, _chai.expect)(importSpy.calledOnce).to.equal(true); (0, _chai.expect)(importSpy.firstCall.args).to.eql([]); }); it('should merge inherited globals into the trailing options object before forwarding', async function () { loadWithImportSpy(); const fakeCommand = { optsWithGlobals: () => ({ navy: 'from-global', extra: 1 }), getOptionValueSource: () => 'default' }; capturedActions.import({ navy: 'subcommand-default' }, fakeCommand); await new Promise(resolve => setImmediate(resolve)); (0, _chai.expect)(importSpy.calledOnce).to.equal(true); (0, _chai.expect)(importSpy.firstCall.args).to.have.lengthOf(1); (0, _chai.expect)(importSpy.firstCall.args[0]).to.eql({ navy: 'from-global', extra: 1 }); }); it('should leave non-object trailing arguments untouched when stripping the command', async function () { loadWithImportSpy(); const fakeCommand = { optsWithGlobals: () => ({ navy: 'global' }) }; capturedActions.import(['web', 'api'], fakeCommand); await new Promise(resolve => setImmediate(resolve)); (0, _chai.expect)(importSpy.calledOnce).to.equal(true); (0, _chai.expect)(importSpy.firstCall.args).to.eql([['web', 'api']]); }); }); describe('basicCliWrapper action', function () { beforeEach(function () { loadModule(); }); it('should call the named navy method with the supplied service list', async function () { navyStub.start = sandbox.stub().resolves(); await actionsByCommand.start(['web', 'api'], { navy: 'env-1' }, {}); (0, _chai.expect)(navyStub.start.calledOnce).to.equal(true); (0, _chai.expect)(navyStub.start.firstCall.args[0]).to.eql(['web', 'api']); (0, _chai.expect)(navyStub.ensurePluginsLoaded.calledOnce).to.equal(true); }); it('should merge optsWithGlobals so global navy is used when Commander passes the command', async function () { navyStub.start = sandbox.stub().resolves(); const fakeCommand = { optsWithGlobals: () => ({ navy: 'from-global' }) }; await actionsByCommand.start(['web'], { navy: 'subcommand-default' }, fakeCommand); (0, _chai.expect)(getNavyStub.firstCall.args[0]).to.equal('from-global'); }); it('should fall back to maybeServices as opts when only an opts object and command are passed', async function () { navyStub.getAvailableServiceNames = sandbox.stub().resolves([]); const fakeCommand = { optsWithGlobals: () => ({ navy: 'from-global' }) }; await actionsByCommand['available-services']({ navy: 'subcommand-default' }, fakeCommand); (0, _chai.expect)(getNavyStub.firstCall.args[0]).to.equal('from-global'); }); it('should pass undefined when called with an empty service list', async function () { navyStub.start = sandbox.stub().resolves(); await actionsByCommand.start([], { navy: 'env-1' }, {}); (0, _chai.expect)(navyStub.start.firstCall.args[0]).to.equal(undefined); }); it('should drive logging by default', async function () { navyStub.start = sandbox.stub().resolves(); await actionsByCommand.start(['web'], { navy: 'env-1' }, {}); (0, _chai.expect)(startDriverLoggingStub.calledWith('Starting services...')).to.equal(true); (0, _chai.expect)(stopDriverLoggingStub.calledOnce).to.equal(true); }); it('should skip driver logging for the url command (driverLogging: false)', async function () { navyStub.url = sandbox.stub().resolves('http://web'); await actionsByCommand.url('web', { navy: 'env-1' }, {}); (0, _chai.expect)(startDriverLoggingStub.called).to.equal(false); (0, _chai.expect)(stopDriverLoggingStub.called).to.equal(false); }); it('should print arrays joined by newlines', async function () { navyStub.getAvailableServiceNames = sandbox.stub().resolves(['web', 'api']); await actionsByCommand['available-services']({ navy: 'env-1' }, {}); (0, _chai.expect)(consoleLogStub.calledWith('web\napi')).to.equal(true); }); it('should print non-null scalar return values', async function () { navyStub.url = sandbox.stub().resolves('http://web'); await actionsByCommand.url('web', { navy: 'env-1' }, {}); (0, _chai.expect)(consoleLogStub.calledWith('http://web')).to.equal(true); }); it('should not print when the navy method returns null', async function () { navyStub.start = sandbox.stub().resolves(null); await actionsByCommand.start(['web'], { navy: 'env-1' }, {}); (0, _chai.expect)(consoleLogStub.called).to.equal(false); }); it('should redirect destroy with services to kill via serviceBasedAlias', async function () { navyStub.kill = sandbox.stub().resolves(); await actionsByCommand.destroy('web api', { navy: 'env-1' }, {}); (0, _chai.expect)(navyStub.kill.calledOnce).to.equal(true); (0, _chai.expect)(navyStub.kill.firstCall.args[0]).to.eql(['web', 'api']); const printed = consoleLogStub.getCalls().map(c => c.args[0] || '').join('\n'); (0, _chai.expect)(printed).to.contain('should not be called with a list'); }); it('should emit cli.before.<fn> and cli.after.<fn>', async function () { navyStub.start = sandbox.stub().resolves(); await actionsByCommand.start(['web'], { navy: 'env-1' }, {}); const events = navyStub.emitAsync.getCalls().map(c => c.args[0]); (0, _chai.expect)(events).to.include('cli.before.start'); (0, _chai.expect)(events).to.include('cli.after.start'); }); it('should register an unhandledRejection handler', async function () { navyStub.start = sandbox.stub().resolves(); await actionsByCommand.start(['web'], { navy: 'env-1' }, {}); (0, _chai.expect)(processOnStub.calledWith('unhandledRejection')).to.equal(true); }); describe('unhandledRejection handler', function () { it('should pretty-print NavyError and exit', async function () { navyStub.start = sandbox.stub().resolves(); await actionsByCommand.start(['web'], { navy: 'env-1' }, {}); const handlers = processOnStub.getCalls().filter(c => c.args[0] === 'unhandledRejection').map(c => c.args[1]); const handler = handlers[handlers.length - 1]; const err = new _errors.NavyError('boom'); const prettyPrintStub = sandbox.stub(err, 'prettyPrint'); handler(err); (0, _chai.expect)(prettyPrintStub.calledOnce).to.equal(true); (0, _chai.expect)(processExitStub.called).to.equal(true); }); it('should print stack for non-NavyError and exit', async function () { navyStub.start = sandbox.stub().resolves(); await actionsByCommand.start(['web'], { navy: 'env-1' }, {}); const handlers = processOnStub.getCalls().filter(c => c.args[0] === 'unhandledRejection').map(c => c.args[1]); const handler = handlers[handlers.length - 1]; const err = new Error('boom'); handler(err); (0, _chai.expect)(consoleErrorStub.calledWith(err.stack)).to.equal(true); (0, _chai.expect)(processExitStub.called).to.equal(true); }); }); }); // Helper to capture program actions for the failure-path tests let capturedActions; function createCapturingProgram() { capturedActions = {}; return { option: sandbox.stub().returnsThis(), enablePositionalOptions: sandbox.stub().returnsThis(), command(spec) { const name = spec.split(' ')[0]; const cmd = { option: sandbox.stub().returnsThis(), description: sandbox.stub().returnsThis(), action(handler) { capturedActions[name] = handler; return cmd; }, on: sandbox.stub().returnsThis() }; return cmd; } }; } });