diff --git a/extensions/systemd/systemd.js b/extensions/systemd/systemd.js index c65c8a746..fd05fd62c 100644 --- a/extensions/systemd/systemd.js +++ b/extensions/systemd/systemd.js @@ -21,8 +21,27 @@ class SystemdProcessManager extends ProcessManager { this._precheck(); const {logSuggestion} = this; - return this.ui.sudo(`systemctl start ${this.systemdName}`) - .then(() => this.ensureStarted({logSuggestion})) + const portfinder = require('portfinder'); + const socketAddress = { + port: null, + host: 'localhost' + }; + + return portfinder.getPortPromise() + .then((port) => { + socketAddress.port = port; + this.instance.config.set('bootstrap-socket', socketAddress); + return this.instance.config.save(); + }) + .then(() => { + return this.ui.sudo(`systemctl start ${this.systemdName}`) + }) + .then(() => { + return this.ensureStarted({ + logSuggestion, + socketAddress + }); + }) .catch((error) => { if (error instanceof CliError) { throw error; @@ -43,8 +62,27 @@ class SystemdProcessManager extends ProcessManager { this._precheck(); const {logSuggestion} = this; - return this.ui.sudo(`systemctl restart ${this.systemdName}`) - .then(() => this.ensureStarted({logSuggestion})) + const portfinder = require('portfinder'); + const socketAddress = { + port: null, + host: 'localhost' + }; + + return portfinder.getPortPromise() + .then((port) => { + socketAddress.port = port; + this.instance.config.set('bootstrap-socket', socketAddress); + return this.instance.config.save(); + }) + .then(() => { + return this.ui.sudo(`systemctl restart ${this.systemdName}`) + }) + .then(() => { + return this.ensureStarted({ + logSuggestion, + socketAddress + }); + }) .catch((error) => { if (error instanceof CliError) { throw error; diff --git a/extensions/systemd/test/systemd-spec.js b/extensions/systemd/test/systemd-spec.js index 55e1e28ef..248b7f965 100644 --- a/extensions/systemd/test/systemd-spec.js +++ b/extensions/systemd/test/systemd-spec.js @@ -5,6 +5,7 @@ const proxyquire = require('proxyquire').noCallThru(); const modulePath = '../systemd'; const errors = require('../../../lib/errors'); +const configStub = require('../../../test/utils/config-stub'); const Systemd = require(modulePath); const instance = { @@ -30,6 +31,7 @@ describe('Unit: Systemd > Process Manager', function () { let ext, ui; beforeEach(function () { + instance.config = configStub(); ui = {sudo: sinon.stub().resolves()}, ext = new Systemd(ui, null, instance); ext.ensureStarted = sinon.stub().resolves(); diff --git a/lib/commands/setup.js b/lib/commands/setup.js index 5e7631edd..189c08454 100644 --- a/lib/commands/setup.js +++ b/lib/commands/setup.js @@ -41,6 +41,7 @@ class SetupCommand extends Command { const os = require('os'); const url = require('url'); const path = require('path'); + const semver = require('semver'); const linux = require('../tasks/linux'); const migrate = require('../tasks/migrate'); @@ -171,10 +172,20 @@ class SetupCommand extends Command { })); if (argv.migrate !== false) { + const instance = this.system.getInstance(); + // Tack on db migration task to the end tasks.push({ title: 'Running database migrations', - task: migrate + task: migrate, + // CASE: We are about to install Ghost 2.0. We moved the execution of knex-migrator into Ghost. + enabled: () => { + if (semver.satisfies(instance.cliConfig.get('active-version'), '^2.0.0')) { + return false; + } + + return true; + } }); } diff --git a/lib/commands/update.js b/lib/commands/update.js index dfec2a765..52a07848a 100644 --- a/lib/commands/update.js +++ b/lib/commands/update.js @@ -73,7 +73,17 @@ class UpdateCommand extends Command { }, { title: 'Running database migrations', skip: (ctx) => ctx.rollback, - task: migrate + task: migrate, + // CASE: We have moved the execution of knex-migrator into Ghost 2.0.0. + // If you are already on ^2 or you update from ^1 to ^2, then skip the task. + enabled: () => { + if (semver.satisfies(instance.cliConfig.get('active-version'), '^2.0.0') || + semver.satisfies(context.version, '^2.0.0')) { + return false; + } + + return true; + } }, { title: 'Restarting Ghost', skip: () => !argv.restart, diff --git a/lib/process-manager.js b/lib/process-manager.js index 809c4287f..117d5b032 100644 --- a/lib/process-manager.js +++ b/lib/process-manager.js @@ -70,11 +70,13 @@ class ProcessManager { */ ensureStarted(options) { const portPolling = require('./utils/port-polling'); + const semver = require('semver'); options = Object.assign({ stopOnError: true, port: this.instance.config.get('server.port'), - host: this.instance.config.get('server.host', 'localhost') + host: this.instance.config.get('server.host', 'localhost'), + useNetServer: semver.satisfies(this.instance.cliConfig.get('active-version'), '^2.0.0') }, options || {}); return portPolling(options).catch((err) => { diff --git a/lib/utils/port-polling.js b/lib/utils/port-polling.js index c86134e42..19757185b 100644 --- a/lib/utils/port-polling.js +++ b/lib/utils/port-polling.js @@ -1,16 +1,71 @@ 'use strict'; -const net = require('net'); const errors = require('../errors'); -module.exports = function portPolling(options) { - options = Object.assign({ - timeoutInMS: 2000, - maxTries: 20, - delayOnConnectInMS: 3 * 2000, - logSuggestion: 'ghost log', - socketTimeoutInMS: 1000 * 60 - }, options || {}); +/** + * @TODO: in theory it could happen that other clients connect, but tbh even with the port polling it was possible: you + * could just start a server on the Ghost port + */ +const useNetServer = (options)=> { + return new Promise((resolve, reject)=> { + const net = require('net'); + let waitTimeout = null; + let ghostSocket = null; + + const server = net.createServer((socket)=> { + ghostSocket = socket; + + socket.on('data', (data) => { + let message; + + try { + message = JSON.parse(data); + } catch (err) { + message = {started: false, error: err}; + } + + if (waitTimeout) { + clearTimeout(waitTimeout); + } + + socket.destroy(); + ghostSocket = null; + + server.close(() => { + if (message.started) { + resolve(); + } else { + reject(new errors.GhostError({ + message: message.error.message, + help: message.error.help, + suggestion: options.logSuggestion + })); + } + }); + }); + }); + + waitTimeout = setTimeout(() => { + if (ghostSocket) { + ghostSocket.destroy(); + } + + ghostSocket = null; + + server.close(() => { + reject(new errors.GhostError({ + message: 'Could not communicate with Ghost', + suggestion: options.logSuggestion + })); + }); + }, options.netServerTimeoutInMS); + + server.listen({host: options.socketAddress.host, port: options.socketAddress.port}); + }); +}; + +const usePortPolling = (options)=> { + const net = require('net'); if (!options.port) { return Promise.reject(new errors.CliError({ @@ -18,7 +73,7 @@ module.exports = function portPolling(options) { })); } - const connectToGhostSocket = (() => { + const connectToGhostSocket = () => { return new Promise((resolve, reject) => { // if host is specified and is *not* 0.0.0.0 (listen on all ips), use the custom host const host = options.host && options.host !== '0.0.0.0' ? options.host : 'localhost'; @@ -35,7 +90,7 @@ module.exports = function portPolling(options) { ghostSocket.destroy(); // force retry - const err = new Error(); + const err = new Error('Socket timed out.'); err.retry = true; reject(err); })); @@ -73,7 +128,7 @@ module.exports = function portPolling(options) { reject(err); })); }); - }); + }; const startPolling = (() => { return new Promise((resolve, reject) => { @@ -87,10 +142,14 @@ module.exports = function portPolling(options) { .catch((err) => { if (err.retry && tries < options.maxTries) { tries = tries + 1; - setTimeout(retry, options.timeoutInMS); + setTimeout(retry, options.retryTimeoutInMS); return; } + if (err instanceof errors.CliError) { + return reject(err); + } + reject(new errors.GhostError({ message: 'Ghost did not start.', suggestion: options.logSuggestion, @@ -103,3 +162,25 @@ module.exports = function portPolling(options) { return startPolling(); }; + +module.exports = function portPolling(options) { + options = Object.assign({ + retryTimeoutInMS: 2000, + maxTries: 20, + delayOnConnectInMS: 3 * 2000, + logSuggestion: 'ghost log', + socketTimeoutInMS: 1000 * 60, + useNetServer: false, + netServerTimeoutInMS: 5 * 60 * 1000, + socketAddress: { + port: 1212, + host: 'localhost' + } + }, options || {}); + + if (options.useNetServer) { + return useNetServer(options); + } + + return usePortPolling(options); +}; diff --git a/test/unit/commands/setup-spec.js b/test/unit/commands/setup-spec.js index a294527af..26a3adcfd 100644 --- a/test/unit/commands/setup-spec.js +++ b/test/unit/commands/setup-spec.js @@ -221,10 +221,70 @@ describe('Unit: Commands > Setup', function () { }); }); + it('Initial stage is setup properly, but skips db migrations', function () { + const migrateStub = sinon.stub().resolves(); + const SetupCommand = proxyquire(modulePath, { + '../tasks/migrate': migrateStub + }); + + const listr = sinon.stub(); + const aIstub = sinon.stub(); + const config = configStub(); + const cliConfigStub = configStub(); + + cliConfigStub.get.withArgs('active-version').returns('2.0.0'); + + config.get.withArgs('url').returns('https://ghost.org'); + config.has.returns(false); + + const system = { + getInstance: () => { + return { + checkEnvironment: () => true, + apples: true, + config, + dir: '/var/www/ghost', + cliConfig: cliConfigStub + }; + }, + addInstance: aIstub, + hook: () => Promise.resolve() + }; + const ui = { + run: () => Promise.resolve(), + listr: listr, + confirm: () => Promise.resolve(false) + }; + const argv = { + prompt: true, + 'setup-linux-user': false + }; + + ui.listr.callsFake((tasks, ctx) => { + return Promise.each(tasks, (task) => { + if ((task.skip && task.skip(ctx)) || (task.enabled && !task.enabled(ctx))) { + return; + } + + return task.task(ctx); + }); + }); + + const setup = new SetupCommand(ui, system); + return setup.run(argv).then(() => { + expect(listr.calledOnce).to.be.true; + expect(migrateStub.called).to.be.false; + }); + }); + it('Initial stage is setup properly', function () { const listr = sinon.stub().resolves(); const aIstub = sinon.stub(); const config = configStub(); + const cliConfigStub = configStub(); + + cliConfigStub.get.withArgs('active-version').returns('1.25.0'); + config.get.withArgs('url').returns('https://ghost.org'); config.has.returns(false); @@ -234,7 +294,8 @@ describe('Unit: Commands > Setup', function () { checkEnvironment: () => true, apples: true, config, - dir: '/var/www/ghost' + dir: '/var/www/ghost', + cliConfig: cliConfigStub }; }, addInstance: aIstub, @@ -258,7 +319,7 @@ describe('Unit: Commands > Setup', function () { expect(tasks[0].title).to.equal('Setting up instance'); tasks.forEach(function (task) { expect(task.title).to.not.match(/database migrations/); - }) + }); const ctx = {}; tasks[0].task(ctx); @@ -348,7 +409,8 @@ describe('Unit: Commands > Setup', function () { log: stubs.log }; system = { - hook: () => Promise.resolve() + hook: () => Promise.resolve(), + getInstance: sinon.stub() }; }); @@ -451,7 +513,10 @@ describe('Unit: Commands > Setup', function () { listr: sinon.stub().resolves() }; const skipStub = sinon.stub(); - const system = {hook: () => Promise.resolve()}; + const system = { + hook: () => Promise.resolve(), + getInstance: sinon.stub() + }; const setup = new SetupCommand(ui, system); setup.runCommand = () => Promise.resolve(); setup.addStage('zest', () => true, null, 'Zesty'); @@ -477,7 +542,10 @@ describe('Unit: Commands > Setup', function () { confirm: sinon.stub().callsFake(confirm) }; const skipStub = sinon.stub(); - const system = {hook: () => Promise.resolve()}; + const system = { + hook: () => Promise.resolve(), + getInstance: sinon.stub() + }; const setup = new SetupCommand(ui, system); let tasks; const runCommand = sinon.stub(setup, 'runCommand').resolves(); diff --git a/test/unit/commands/update-spec.js b/test/unit/commands/update-spec.js index d946529f6..d0afa0bac 100644 --- a/test/unit/commands/update-spec.js +++ b/test/unit/commands/update-spec.js @@ -37,6 +37,130 @@ describe('Unit: Commands > Update', function () { }); describe('run', function () { + it('doesn\'t run database migrations if active blog version is ^2.0.0', function () { + const migrateStub = sinon.stub().resolves(); + const UpdateCommand = proxyquire(modulePath, { + '../tasks/migrate': migrateStub + }); + const config = configStub(); + config.get.withArgs('cli-version').returns('1.8.0'); + config.get.withArgs('active-version').returns('2.0.0'); + const ui = {log: sinon.stub(), listr: sinon.stub(), run: sinon.stub()}; + const system = {getInstance: sinon.stub()}; + + ui.run.callsFake(fn => fn()); + ui.listr.callsFake((tasks, ctx) => { + return Promise.each(tasks, (task) => { + if ((task.skip && task.skip(ctx)) || (task.enabled && !task.enabled(ctx))) { + return; + } + + return task.task(ctx); + }); + }); + + class TestInstance extends Instance { + get cliConfig() { return config; } + } + const fakeInstance = sinon.stub(new TestInstance(ui, system, '/var/www/ghost')); + system.getInstance.returns(fakeInstance); + fakeInstance.running.resolves(true); + const cmdInstance = new UpdateCommand(ui, system); + + const versionStub = sinon.stub(cmdInstance, 'version').resolves(true); + const runCommandStub = sinon.stub(cmdInstance, 'runCommand').resolves(); + const downloadStub = sinon.stub(cmdInstance, 'downloadAndUpdate').resolves(); + const removeOldVersionsStub = sinon.stub(cmdInstance, 'removeOldVersions').resolves(); + const linkStub = sinon.stub(cmdInstance, 'link').resolves(); + const stopStub = sinon.stub(cmdInstance, 'stop').resolves(); + + return cmdInstance.run({version: '2.0.1', force: false, zip: ''}).then(() => { + expect(runCommandStub.calledTwice).to.be.true; + expect(ui.run.calledOnce).to.be.true; + expect(versionStub.calledOnce).to.be.true; + expect(versionStub.args[0][0]).to.deep.equal({ + version: '2.0.1', + force: false, + instance: fakeInstance, + activeVersion: '2.0.0', + zip: '' + }); + expect(ui.log.calledOnce).to.be.false; + expect(ui.listr.calledOnce).to.be.true; + expect(removeOldVersionsStub.calledOnce).to.be.true; + expect(stopStub.calledOnce).to.be.true; + expect(linkStub.calledOnce).to.be.true; + expect(downloadStub.calledOnce).to.be.true; + expect(fakeInstance.running.calledOnce).to.be.true; + expect(fakeInstance.loadRunningEnvironment.calledOnce).to.be.true; + expect(fakeInstance.checkEnvironment.calledOnce).to.be.true; + + expect(migrateStub.called).to.be.false; + }); + }); + + it('doesn\'t run database migrations if version to migrate to is ^2.0.0', function () { + const migrateStub = sinon.stub().resolves(); + const UpdateCommand = proxyquire(modulePath, { + '../tasks/migrate': migrateStub + }); + const config = configStub(); + config.get.withArgs('cli-version').returns('1.8.0'); + config.get.withArgs('active-version').returns('1.25.0'); + const ui = {log: sinon.stub(), listr: sinon.stub(), run: sinon.stub()}; + const system = {getInstance: sinon.stub()}; + + ui.run.callsFake(fn => fn()); + ui.listr.callsFake((tasks, ctx) => { + return Promise.each(tasks, (task) => { + if ((task.skip && task.skip(ctx)) || (task.enabled && !task.enabled(ctx))) { + return; + } + + return task.task(ctx); + }); + }); + + class TestInstance extends Instance { + get cliConfig() { return config; } + } + const fakeInstance = sinon.stub(new TestInstance(ui, system, '/var/www/ghost')); + system.getInstance.returns(fakeInstance); + fakeInstance.running.resolves(true); + const cmdInstance = new UpdateCommand(ui, system); + + const versionStub = sinon.stub(cmdInstance, 'version').resolves(true); + const runCommandStub = sinon.stub(cmdInstance, 'runCommand').resolves(); + const downloadStub = sinon.stub(cmdInstance, 'downloadAndUpdate').resolves(); + const removeOldVersionsStub = sinon.stub(cmdInstance, 'removeOldVersions').resolves(); + const linkStub = sinon.stub(cmdInstance, 'link').resolves(); + const stopStub = sinon.stub(cmdInstance, 'stop').resolves(); + + return cmdInstance.run({version: '2.0.0', force: false, zip: ''}).then(() => { + expect(runCommandStub.calledTwice).to.be.true; + expect(ui.run.calledOnce).to.be.true; + expect(versionStub.calledOnce).to.be.true; + expect(versionStub.args[0][0]).to.deep.equal({ + version: '2.0.0', + force: false, + instance: fakeInstance, + activeVersion: '1.25.0', + zip: '' + }); + expect(ui.log.calledOnce).to.be.false; + expect(ui.listr.calledOnce).to.be.true; + expect(removeOldVersionsStub.calledOnce).to.be.true; + expect(stopStub.calledOnce).to.be.true; + expect(linkStub.calledOnce).to.be.true; + expect(downloadStub.calledOnce).to.be.true; + expect(fakeInstance.running.calledOnce).to.be.true; + expect(fakeInstance.loadRunningEnvironment.calledOnce).to.be.true; + expect(fakeInstance.checkEnvironment.calledOnce).to.be.true; + + expect(migrateStub.called).to.be.false; + }); + }); + it('doesn\'t run tasks if no new versions are available', function () { const UpdateCommand = require(modulePath); const config = configStub(); diff --git a/test/unit/process-manager-spec.js b/test/unit/process-manager-spec.js index 77388b0ce..93444cafa 100644 --- a/test/unit/process-manager-spec.js +++ b/test/unit/process-manager-spec.js @@ -67,12 +67,15 @@ describe('Unit: Process Manager', function () { }); it('calls portPolling with options', function () { + const cliConfig = getConfigStub(); const config = getConfigStub(); + + cliConfig.get.withArgs('active-version').returns('1.25.0'); config.get.withArgs('server.port').returns(2368); config.get.withArgs('server.host').returns('10.0.1.0'); portPollingStub.resolves(); - const instance = new ProcessManager({}, {}, {config: config}); + const instance = new ProcessManager({}, {}, {config, cliConfig}); const stopStub = sinon.stub(instance, 'stop').resolves(); return instance.ensureStarted({logSuggestion: 'test'}).then(() => { @@ -82,19 +85,23 @@ describe('Unit: Process Manager', function () { logSuggestion: 'test', stopOnError: true, port: 2368, - host: '10.0.1.0' + host: '10.0.1.0', + useNetServer: false })).to.be.true; expect(stopStub.called).to.be.false; }); }); it('throws error without stopping if stopOnError is false', function () { + const cliConfig = getConfigStub(); const config = getConfigStub(); + + cliConfig.get.withArgs('active-version').returns('1.25.0'); config.get.withArgs('server.port').returns(2368); config.get.withArgs('server.host').returns('localhost'); portPollingStub.rejects(new Error('test error')); - const instance = new ProcessManager({}, {}, {config: config}); + const instance = new ProcessManager({}, {}, {config, cliConfig}); const stopStub = sinon.stub(instance, 'stop').resolves(); return instance.ensureStarted({stopOnError: false}).then(() => { @@ -105,19 +112,23 @@ describe('Unit: Process Manager', function () { expect(portPollingStub.calledWithExactly({ stopOnError: false, port: 2368, - host: 'localhost' + host: 'localhost', + useNetServer: false })).to.be.true; expect(stopStub.called).to.be.false; }); }); it('throws error and calls stop if stopOnError is true', function () { + const cliConfig = getConfigStub(); const config = getConfigStub(); + + cliConfig.get.withArgs('active-version').returns('1.25.0'); config.get.withArgs('server.port').returns(2368); config.get.withArgs('server.host').returns('localhost'); portPollingStub.rejects(new Error('test error')); - const instance = new ProcessManager({}, {}, {config: config}); + const instance = new ProcessManager({}, {}, {config, cliConfig}); const stopStub = sinon.stub(instance, 'stop').resolves(); return instance.ensureStarted({}).then(() => { @@ -129,19 +140,23 @@ describe('Unit: Process Manager', function () { expect(portPollingStub.calledWithExactly({ stopOnError: true, port: 2368, - host: 'localhost' + host: 'localhost', + useNetServer: false })).to.be.true; expect(stopStub.calledOnce).to.be.true; }); }); it('throws error and calls stop (swallows stop error) if stopOnError is true', function () { + const cliConfig = getConfigStub(); const config = getConfigStub(); + + cliConfig.get.withArgs('active-version').returns('1.25.0'); config.get.withArgs('server.port').returns(2368); config.get.withArgs('server.host').returns('localhost'); portPollingStub.rejects(new Error('test error')); - const instance = new ProcessManager({}, {}, {config: config}); + const instance = new ProcessManager({}, {}, {config, cliConfig}); const stopStub = sinon.stub(instance, 'stop').rejects(new Error('test error 2')); return instance.ensureStarted().then(() => { @@ -153,7 +168,8 @@ describe('Unit: Process Manager', function () { expect(portPollingStub.calledWithExactly({ stopOnError: true, port: 2368, - host: 'localhost' + host: 'localhost', + useNetServer: false })).to.be.true; expect(stopStub.calledOnce).to.be.true; }); diff --git a/test/unit/utils/port-polling-spec.js b/test/unit/utils/port-polling-spec.js index a527e5b9d..ef1dbdd78 100644 --- a/test/unit/utils/port-polling-spec.js +++ b/test/unit/utils/port-polling-spec.js @@ -20,178 +20,338 @@ describe('Unit: Utils > portPolling', function () { }); }); - it('Ghost does never start', function () { - const netStub = sinon.stub(); - - netStub.setTimeout = sinon.stub(); - netStub.destroy = sinon.stub(); - netStub.on = function (event, cb) { - if (event === 'error') { - cb(new Error('whoops')); - } - }; - - const connectStub = sinon.stub(net, 'connect').returns(netStub); - - return portPolling({port: 1111, maxTries: 3, timeoutInMS: 100}) - .then(() => { - throw new Error('Expected error'); - }) - .catch((err) => { - expect(err.options.suggestion).to.exist; - expect(err.message).to.eql('Ghost did not start.'); - expect(err.err.message).to.eql('whoops'); - expect(connectStub.callCount).to.equal(4); - expect(connectStub.calledWithExactly(1111, 'localhost'), 'uses localhost by default').to.be.true; - expect(netStub.destroy.callCount).to.eql(4); + describe('useNetServer is enabled', function () { + it('Ghost does start', function () { + const netStub = sinon.stub(); + const socketStub = sinon.stub(); + + socketStub.on = sinon.stub().callsFake((event, cb) => { + if (event === 'data') { + cb(JSON.stringify({started: true})); + } }); - }); - - it('Ghost does start, but falls over', function () { - const netStub = sinon.stub(); - netStub.setTimeout = sinon.stub(); - netStub.destroy = sinon.stub(); + socketStub.destroy = sinon.stub(); - let i = 0; - netStub.on = function (event, cb) { - i = i + 1; - - if (event === 'close') { - cb(); - } else if (event === 'error' && i === 3) { - cb(new Error()); - } else if (event === 'connect' && i === 5) { + netStub.listen = sinon.stub(); + netStub.close = sinon.stub().callsFake((cb)=> { cb(); - } - }; - - const connectStub = sinon.stub(net, 'connect').returns(netStub); - - return portPolling({port: 1111, maxTries: 3, timeoutInMS: 100, delayOnConnectInMS: 150, host: '0.0.0.0'}) - .then(() => { - throw new Error('Expected error'); - }) - .catch((err) => { - expect(err.options.suggestion).to.exist; - expect(err.message).to.eql('Ghost did not start.'); - expect(connectStub.calledTwice).to.be.true; - expect(connectStub.calledWithExactly(1111, 'localhost'), 'uses localhost if host is 0.0.0.0').to.be.true; - expect(netStub.destroy.callCount).to.eql(2); }); - }); - - it('Ghost does start', function () { - const netStub = sinon.stub(); - - netStub.setTimeout = sinon.stub(); - netStub.destroy = sinon.stub(); - - let i = 0; - netStub.on = function (event, cb) { - i = i + 1; - if (i === 6) { - expect(event).to.eql('close'); - } else if (i === 5 && event === 'connect') { - cb(); - } else if (i === 3 && event === 'error') { - cb(new Error()); - } - }; + sinon.stub(net, 'createServer').callsFake((fn) => { + setTimeout(() => { + fn(socketStub); + }, 100); - const connectStub = sinon.stub(net, 'connect').returns(netStub); + return netStub; + }); - return portPolling({port: 1111, maxTries: 3, timeoutInMS: 100, delayOnConnectInMS: 150, host: '10.0.1.0'}) - .then(() => { - expect(connectStub.calledTwice).to.be.true; - expect(connectStub.calledWithExactly(1111, '10.0.1.0'), 'uses custom host').to.be.true; - expect(netStub.destroy.callCount).to.eql(2); - }) - .catch((err) => { + return portPolling({ + netServerTimeoutInMS: 1000, + useNetServer: true + }).then(() => { + expect(net.createServer.calledOnce).to.be.true; + expect(netStub.listen.callCount).to.eql(1); + expect(netStub.listen.calledWithExactly({host: 'localhost', port: 1212})).to.be.true; + expect(netStub.close.callCount).to.eql(1); + + expect(socketStub.destroy.callCount).to.eql(1); + expect(socketStub.on.callCount).to.eql(1); + }).catch((err) => { throw err; }); - }); + }); - it('Ghost does start, skip delay on connect', function () { - const netStub = sinon.stub(); + it('Ghost didn\'t start', function () { + const netStub = sinon.stub(); + const socketStub = sinon.stub(); - netStub.setTimeout = sinon.stub(); - netStub.destroy = sinon.stub(); + socketStub.on = sinon.stub().callsFake((event, cb) => { + if (event === 'data') { + cb(JSON.stringify({false: true, error: {message: 'Syntax Error'}})); + } + }); - netStub.on = function (event, cb) { - expect(event).to.not.eql('close'); + socketStub.destroy = sinon.stub(); - if (event === 'connect') { + netStub.listen = sinon.stub(); + netStub.close = sinon.stub().callsFake((cb)=> { cb(); - } - }; + }); - sinon.stub(net, 'connect').returns(netStub); + sinon.stub(net, 'createServer').callsFake((fn) => { + setTimeout(() => { + fn(socketStub); + }, 100); - return portPolling({port: 1111, maxTries: 3, timeoutInMS: 100, delayOnConnectInMS: false}) - .then(() => { - expect(netStub.destroy.callCount).to.eql(1); - }) - .catch((err) => { - throw err; + return netStub; }); - }); - it('socket times out', function () { - const netStub = sinon.stub(); + return portPolling({ + netServerTimeoutInMS: 1000, + useNetServer: true + }).then(() => { + expect('1').to.equal(1, 'Ghost should not start.'); + }).catch((err) => { + expect(err.message).to.eql('Syntax Error'); + expect(net.createServer.calledOnce).to.be.true; + expect(netStub.listen.calledOnce).to.be.true; + expect(netStub.listen.calledWithExactly({host: 'localhost', port: 1212})).to.be.true; + expect(netStub.close.callCount).to.eql(1); + + expect(socketStub.destroy.callCount).to.eql(1); + expect(socketStub.on.callCount).to.eql(1); + }); + }); - netStub.setTimeout = sinon.stub(); - netStub.destroy = sinon.stub(); + it('Ghost does not communicate, expect timeout', function () { + const netStub = sinon.stub(); + const socketStub = sinon.stub(); - netStub.on = function (event, cb) { - if (event === 'timeout') { + socketStub.on = sinon.stub(); + socketStub.destroy = sinon.stub(); + + netStub.listen = sinon.stub(); + netStub.close = sinon.stub().callsFake((cb)=> { cb(); - } - }; + }); - sinon.stub(net, 'connect').returns(netStub); + sinon.stub(net, 'createServer').callsFake(() => { + return netStub; + }); - return portPolling({port: 1111, maxTries: 3, timeoutInMS: 100, socketTimeoutInMS: 300}) - .then(() => { - throw new Error('Expected error'); - }) - .catch((err) => { - expect(err.options.suggestion).to.exist; - expect(err.message).to.eql('Ghost did not start.'); - expect(netStub.destroy.callCount).to.eql(4); + return portPolling({ + netServerTimeoutInMS: 500, + useNetServer: true + }).then(() => { + expect('1').to.equal(1, 'Ghost should not start.'); + }).catch((err) => { + expect(err.message).to.eql('Could not communicate with Ghost'); + expect(net.createServer.calledOnce).to.be.true; + expect(netStub.listen.calledOnce).to.be.true; + expect(netStub.listen.calledWithExactly({host: 'localhost', port: 1212})).to.be.true; + expect(netStub.close.callCount).to.eql(1); + + expect(socketStub.destroy.callCount).to.eql(0); + expect(socketStub.on.callCount).to.eql(0); }); - }); + }); - it('Ghost connects, but socket times out kicks in', function () { - const netStub = sinon.stub(); + it('Ghost does not answer, expect timeout', function () { + const netStub = sinon.stub(); + const socketStub = sinon.stub(); - netStub.setTimeout = sinon.stub(); - netStub.destroy = sinon.stub(); + socketStub.on = sinon.stub(); + socketStub.destroy = sinon.stub(); - const events = {}; - netStub.on = function (event, cb) { - if (event === 'connect') { + netStub.listen = sinon.stub(); + netStub.close = sinon.stub().callsFake((cb)=> { cb(); + }); + sinon.stub(net, 'createServer').callsFake((fn) => { setTimeout(() => { - events.timeout(); + fn(socketStub); }, 100); - } - - events[event] = cb; - }; - sinon.stub(net, 'connect').returns(netStub); + return netStub; + }); - return portPolling({port: 1111, maxTries: 2, timeoutInMS: 100, socketTimeoutInMS: 300}) - .then(() => { - throw new Error('Expected error'); - }) - .catch((err) => { - expect(err.options.suggestion).to.exist; - expect(err.message).to.eql('Ghost did not start.'); - expect(netStub.destroy.callCount).to.eql(3); + return portPolling({ + netServerTimeoutInMS: 500, + useNetServer: true + }).then(() => { + expect('1').to.equal(1, 'Ghost should not start.'); + }).catch((err) => { + expect(err.message).to.eql('Could not communicate with Ghost'); + expect(net.createServer.calledOnce).to.be.true; + expect(netStub.listen.calledOnce).to.be.true; + expect(netStub.listen.calledWithExactly({host: 'localhost', port: 1212})).to.be.true; + expect(netStub.close.callCount).to.eql(1); + + expect(socketStub.destroy.callCount).to.eql(1); + expect(socketStub.on.callCount).to.eql(1); }); + }); + }); + + describe('useNetServer is disabled', function () { + it('Ghost does never start', function () { + const netStub = sinon.stub(); + + netStub.setTimeout = sinon.stub(); + netStub.destroy = sinon.stub(); + netStub.on = function (event, cb) { + if (event === 'error') { + cb(new Error('whoops')); + } + }; + + const connectStub = sinon.stub(net, 'connect').returns(netStub); + + return portPolling({port: 1111, maxTries: 3, retryTimeoutInMS: 100}) + .then(() => { + throw new Error('Expected error'); + }) + .catch((err) => { + expect(err.options.suggestion).to.exist; + expect(err.message).to.eql('Ghost did not start.'); + expect(err.err.message).to.eql('whoops'); + expect(connectStub.callCount).to.equal(4); + expect(connectStub.calledWithExactly(1111, 'localhost'), 'uses localhost by default').to.be.true; + expect(netStub.destroy.callCount).to.eql(4); + }); + }); + + it('Ghost does start, but falls over', function () { + const netStub = sinon.stub(); + + netStub.setTimeout = sinon.stub(); + netStub.destroy = sinon.stub(); + + let i = 0; + netStub.on = function (event, cb) { + i = i + 1; + + if (event === 'close') { + cb(); + } else if (event === 'error' && i === 3) { + cb(new Error()); + } else if (event === 'connect' && i === 5) { + cb(); + } + }; + + const connectStub = sinon.stub(net, 'connect').returns(netStub); + + return portPolling({port: 1111, maxTries: 3, retryTimeoutInMS: 100, delayOnConnectInMS: 150, host: '0.0.0.0'}) + .then(() => { + throw new Error('Expected error'); + }) + .catch((err) => { + expect(err.options.suggestion).to.exist; + expect(err.message).to.eql('Ghost did not start.'); + expect(err.err.message).to.eql('Ghost died.'); + expect(connectStub.calledTwice).to.be.true; + expect(connectStub.calledWithExactly(1111, 'localhost'), 'uses localhost if host is 0.0.0.0').to.be.true; + expect(netStub.destroy.callCount).to.eql(2); + }); + }); + + it('Ghost does start', function () { + const netStub = sinon.stub(); + + netStub.setTimeout = sinon.stub(); + netStub.destroy = sinon.stub(); + + let i = 0; + netStub.on = function (event, cb) { + i = i + 1; + + if (i === 6) { + expect(event).to.eql('close'); + } else if (i === 5 && event === 'connect') { + cb(); + } else if (i === 3 && event === 'error') { + cb(new Error()); + } + }; + + const connectStub = sinon.stub(net, 'connect').returns(netStub); + + return portPolling({port: 1111, maxTries: 3, retryTimeoutInMS: 100, delayOnConnectInMS: 150, host: '10.0.1.0'}) + .then(() => { + expect(connectStub.calledTwice).to.be.true; + expect(connectStub.calledWithExactly(1111, '10.0.1.0'), 'uses custom host').to.be.true; + expect(netStub.destroy.callCount).to.eql(2); + }) + .catch((err) => { + throw err; + }); + }); + + it('Ghost does start, skip delay on connect', function () { + const netStub = sinon.stub(); + + netStub.setTimeout = sinon.stub(); + netStub.destroy = sinon.stub(); + + netStub.on = function (event, cb) { + expect(event).to.not.eql('close'); + + if (event === 'connect') { + cb(); + } + }; + + sinon.stub(net, 'connect').returns(netStub); + + return portPolling({port: 1111, maxTries: 3, retryTimeoutInMS: 100, delayOnConnectInMS: false}) + .then(() => { + expect(netStub.destroy.callCount).to.eql(1); + }) + .catch((err) => { + throw err; + }); + }); + + it('socket times out', function () { + const netStub = sinon.stub(); + + netStub.setTimeout = sinon.stub(); + netStub.destroy = sinon.stub(); + + netStub.on = function (event, cb) { + if (event === 'timeout') { + cb(); + } + }; + + sinon.stub(net, 'connect').returns(netStub); + + return portPolling({port: 1111, maxTries: 3, retryTimeoutInMS: 100, socketTimeoutInMS: 300}) + .then(() => { + throw new Error('Expected error'); + }) + .catch((err) => { + expect(err.options.suggestion).to.exist; + expect(err.message).to.eql('Ghost did not start.'); + expect(err.err.message).to.eql('Socket timed out.'); + expect(netStub.destroy.callCount).to.eql(4); + }); + }); + + it('Ghost connects, but socket times out kicks in', function () { + const netStub = sinon.stub(); + + netStub.setTimeout = sinon.stub(); + netStub.destroy = sinon.stub(); + + const events = {}; + netStub.on = function (event, cb) { + if (event === 'connect') { + cb(); + + setTimeout(() => { + events.timeout(); + }, 100); + } + + events[event] = cb; + }; + + sinon.stub(net, 'connect').returns(netStub); + + return portPolling({port: 1111, maxTries: 2, retryTimeoutInMS: 100, socketTimeoutInMS: 300}) + .then(() => { + throw new Error('Expected error'); + }) + .catch((err) => { + expect(err.options.suggestion).to.exist; + expect(err.message).to.eql('Ghost did not start.'); + expect(err.err.message).to.eql('Socket timed out.'); + expect(netStub.destroy.callCount).to.eql(3); + }); + }); }); });