Skip to content

Commit

Permalink
Test suite for lib/tasks/fastboot-server
Browse files Browse the repository at this point in the history
  • Loading branch information
pwfisher committed May 27, 2016
1 parent 132f92f commit 276793c
Show file tree
Hide file tree
Showing 5 changed files with 256 additions and 9 deletions.
1 change: 1 addition & 0 deletions lib/commands/fastboot.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
'use strict';

const RSVP = require('rsvp');
const getPort = RSVP.denodeify(require('portfinder').getPort);
const ServerTask = require('../tasks/fastboot-server');
Expand Down
16 changes: 11 additions & 5 deletions lib/tasks/fastboot-server.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
'use strict';

const RSVP = require('rsvp');
const CoreObject = require('core-object');
const debug = require('debug')('ember-cli-fastboot/server');
const exec = RSVP.denodeify(require('child_process').exec);
const http = require('http');
const path = require('path');
const debug = require('debug')('ember-cli-fastboot/server');

module.exports = CoreObject.extend({

exec,
sockets: {},
http,
httpServer: null,
nextSocketId: 0,
require,
restartAgain: false,
restartPromise: null,
sockets: {},

run(options) {
debug('run');
Expand All @@ -24,8 +30,8 @@ module.exports = CoreObject.extend({

return this.exec('npm install', { cwd: options.outputPath })
.then(() => {
const middleware = require('fastboot-express-middleware')(options.outputPath);
const express = require('express');
const middleware = this.require('fastboot-express-middleware')(options.outputPath);
const express = this.require('express');
const app = express();

if (options.serveAssets) {
Expand All @@ -35,7 +41,7 @@ module.exports = CoreObject.extend({
app.get('/*', middleware);
app.use((req, res) => res.sendStatus(404));

this.httpServer = http.createServer(app);
this.httpServer = this.http.createServer(app);

// Track open sockets for fast restart
this.httpServer.on('connection', (socket) => {
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
"loader.js": "^4.0.1",
"mocha": "^2.2.4",
"request": "^2.55.0",
"sinon": "^1.17.4",
"symlink-or-copy": "^1.0.1",
"temp": "^0.8.3",
"walk-sync": "^0.2.5"
Expand Down
7 changes: 3 additions & 4 deletions test/lib-commands-fastboot-test.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
'use strict';

const http = require('http');
const CoreObject = require('core-object');
const RSVP = require('rsvp');
const expect = require('chai').expect;
const FastbootCommand = CoreObject.extend(require('../lib/commands/fastboot'));
const camelize = require('ember-cli-string-utils').camelize;
const defaults = require('lodash.defaults');
const expect = require('chai').expect;
const FastbootCommand = CoreObject.extend(require('../lib/commands/fastboot'));
const RSVP = require('rsvp');

function CommandOptions(options) {
const defaultOptions = {};
Expand Down
240 changes: 240 additions & 0 deletions test/lib-tasks-fastboot-server-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
'use strict';

const CoreObject = require('core-object');
const camelize = require('ember-cli-string-utils').camelize;
const defaults = require('lodash.defaults');
const EventEmitter = require('events').EventEmitter;
const expect = require('chai').expect;
const FastbootCommand = CoreObject.extend(require('../lib/commands/fastboot'));
const FastbootServerTask = require('../lib/tasks/fastboot-server');
const http = require('http');
const RSVP = require('rsvp');
const sinon = require('sinon');

function CommandOptions(options) {
const defaultOptions = {};
new FastbootCommand().availableOptions.forEach(o => {
defaultOptions[camelize(o.name)] = o.default;
});
return defaults(options || {}, defaultOptions);
};
function MockServer() {
EventEmitter.apply(this, arguments);
this.listen = () => {};
}
MockServer.prototype = Object.create(EventEmitter.prototype);
const mockUI = { writeLine() {} };

describe('fastboot server task', function() {
let options, task;

beforeEach(function() {
this.sinon = sinon.sandbox.create();
task = new FastbootServerTask({
ui: mockUI,
});
options = new CommandOptions();
});

afterEach(function() {
this.sinon.restore();
});

describe('run', function() {
it('calls restart on SIGHUP', function() {
const restartStub = this.sinon.stub(task, 'restart');
task.run(options);
process.emit('SIGHUP');
expect(restartStub.called).to.be.ok;
});
});

describe('restart', function() {
let restartSpy, stopStub, clearRequireCacheStub, startStub;

beforeEach(function() {
restartSpy = this.sinon.spy(task, 'restart');
stopStub = this.sinon.stub(task, 'stop').returns(RSVP.resolve());
clearRequireCacheStub = this.sinon.stub(task, 'clearRequireCache');
startStub = this.sinon.stub(task, 'start');
});

it('calls stop, clearRequireCache, and start', function() {
return task.restart(options).then(() => {
expect(restartSpy.callCount).to.equal(1);
expect(stopStub.callCount).to.equal(1);
expect(clearRequireCacheStub.callCount).to.equal(1);
expect(startStub.callCount).to.equal(1);
});
});

it('can restart multiple times', function() {
const restartPromise = task.restart(options);
return restartPromise
.then(() => task.restart(options))
.then(() => {
expect(restartSpy.callCount).to.equal(2);
expect(stopStub.callCount).to.equal(2);
expect(clearRequireCacheStub.callCount).to.equal(2);
expect(startStub.callCount).to.equal(2);
});
});

// when outputReady while server is starting
// (e.g. app file change during server npm install)
// - wait on start, then reload
// when outputReady multiple times during startup
// (e.g. fast app build, slow server npm install)
// - call reload only once
it('restarts again just once for all calls during startup', function() {
const restartPromise = task.restart(options);
expect(task.restartPromise).to.equal(restartPromise);
expect(task.restartAgain).to.equal(false);
task.restart(options);
task.restart(options);
expect(task.restartPromise).to.equal(restartPromise);
expect(task.restartAgain).to.equal(true);
return restartPromise
.then(() => {
expect(task.restartPromise).to.not.equal(restartPromise);
expect(task.restartAgain).to.equal(false);
return task.restartPromise;
})
.then(() => {
expect(task.restartPromise).to.equal(null);
expect(task.restartAgain).to.equal(false);
expect(restartSpy.callCount).to.equal(4);
expect(stopStub.callCount).to.equal(2);
expect(clearRequireCacheStub.callCount).to.equal(2);
expect(startStub.callCount).to.equal(2);
});
});

it('can restart again after immediate restart completes', function() {
const restartPromise = task.restart(options);
expect(task.restartPromise).to.equal(restartPromise);
expect(task.restartAgain).to.equal(false);
task.restart(options);
task.restart(options);
expect(task.restartPromise).to.equal(restartPromise);
expect(task.restartAgain).to.equal(true);
return restartPromise
.then(() => {
expect(task.restartPromise).to.not.equal(restartPromise);
expect(task.restartAgain).to.equal(false);
return task.restartPromise;
})
.then(() => {
expect(task.restartPromise).to.equal(null);
expect(task.restartAgain).to.equal(false);
return task.restart(options);
})
.then(() => {
expect(task.restartPromise).to.equal(null);
expect(task.restartAgain).to.equal(false);
expect(restartSpy.callCount).to.equal(5);
expect(stopStub.callCount).to.equal(3);
expect(clearRequireCacheStub.callCount).to.equal(3);
expect(startStub.callCount).to.equal(3);
});
});
});

describe('start', function() {
let createServerStub, execStub, mockServer, requireSpy, useStub;
const mockApp = { get() {}, use() {} };
const mockExpress = () => mockApp;
const mockExpressStatic = {};
mockExpress.static = () => mockExpressStatic;
const mockRequire = (which) => {
if (which === 'express') { return mockExpress; }
if (which === 'fastboot-express-middleware') return () => {};
};

beforeEach(function() {
mockServer = new MockServer();
createServerStub = this.sinon.stub(task.http, 'createServer').returns(mockServer);
execStub = this.sinon.stub(task, 'exec').returns(RSVP.resolve());
task.require = mockRequire;
requireSpy = this.sinon.spy(task, 'require');
useStub = this.sinon.stub(mockApp, 'use');
});

it('runs npm install in server root', function() {
return task.start(options)
.then(() => {
expect(execStub.calledWith('npm install', { cwd: 'dist' })).to.equal(true);
});
});

it('requires server dependencies', function() {
return task.start(options)
.then(() => {
expect(requireSpy.calledWith('fastboot-express-middleware')).to.equal(true);
expect(requireSpy.calledWith('express')).to.equal(true);
});
});

it('uses express.static when serve-assets=true', function() {
options = new CommandOptions({ serveAssets: true });
return task.start(options)
.then(() => {
expect(useStub.calledWith(mockExpressStatic)).to.equal(true);
});
});

it('tracks open sockets using connection and close events', function() {
return task.start(options)
.then(() => {
expect(Object.keys(task.sockets).length).to.equal(0);
expect(task.nextSocketId).to.equal(0);
let socket = new EventEmitter();
mockServer.emit('connection', socket);
expect(Object.keys(task.sockets).length).to.equal(1);
expect(task.sockets[0]).to.equal(socket);
expect(task.nextSocketId).to.equal(1);
socket.emit('close');
expect(Object.keys(task.sockets).length).to.equal(0);
expect(task.sockets[0]).to.equal(undefined);
mockServer.emit('connection', socket);
expect(Object.keys(task.sockets).length).to.equal(1);
expect(task.sockets[1]).to.equal(socket);
expect(task.nextSocketId).to.equal(2);
})
});
});

describe('stop', function() {
it('is safe to call before start', function() {
let stopPromise;
const callStop = () => { stopPromise = task.stop(options); };
expect(callStop).to.not.throw();
return stopPromise;
});

// Sequence of calls
// 1. server.close (stop opening sockets and set close callback)
// 2. sockets[i].destroy
// 3. close callback
it('destroys open sockets after calling server.close', function() {
let closeCallback;
const destroyPromise = new RSVP.Promise(resolve => { });
const mockServer = { close(cb) { closeCallback = cb; } };
const closeSpy = this.sinon.spy(mockServer, 'close');
const mockSocket = {
destroy() {
expect(closeSpy.called).to.equal(true);
if (closeCallback) closeCallback();
}
};
const destroySpy = this.sinon.spy(mockSocket, 'destroy');
task.httpServer = mockServer;
task.sockets[0] = mockSocket;
task.stop(options)
.then(() => {
expect(destroySpy.called).to.equal(true);
expect(task.httpServer).to.equal(null);
});
});
});
});

0 comments on commit 276793c

Please sign in to comment.