Skip to content

Commit 276793c

Browse files
committed
Test suite for lib/tasks/fastboot-server
1 parent 132f92f commit 276793c

5 files changed

Lines changed: 256 additions & 9 deletions

File tree

lib/commands/fastboot.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
'use strict';
2+
23
const RSVP = require('rsvp');
34
const getPort = RSVP.denodeify(require('portfinder').getPort);
45
const ServerTask = require('../tasks/fastboot-server');

lib/tasks/fastboot-server.js

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,22 @@
11
'use strict';
2+
23
const RSVP = require('rsvp');
34
const CoreObject = require('core-object');
5+
const debug = require('debug')('ember-cli-fastboot/server');
46
const exec = RSVP.denodeify(require('child_process').exec);
57
const http = require('http');
68
const path = require('path');
7-
const debug = require('debug')('ember-cli-fastboot/server');
89

910
module.exports = CoreObject.extend({
1011

1112
exec,
12-
sockets: {},
13+
http,
14+
httpServer: null,
1315
nextSocketId: 0,
16+
require,
17+
restartAgain: false,
18+
restartPromise: null,
19+
sockets: {},
1420

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

2531
return this.exec('npm install', { cwd: options.outputPath })
2632
.then(() => {
27-
const middleware = require('fastboot-express-middleware')(options.outputPath);
28-
const express = require('express');
33+
const middleware = this.require('fastboot-express-middleware')(options.outputPath);
34+
const express = this.require('express');
2935
const app = express();
3036

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

38-
this.httpServer = http.createServer(app);
44+
this.httpServer = this.http.createServer(app);
3945

4046
// Track open sockets for fast restart
4147
this.httpServer.on('connection', (socket) => {

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
"loader.js": "^4.0.1",
5151
"mocha": "^2.2.4",
5252
"request": "^2.55.0",
53+
"sinon": "^1.17.4",
5354
"symlink-or-copy": "^1.0.1",
5455
"temp": "^0.8.3",
5556
"walk-sync": "^0.2.5"

test/lib-commands-fastboot-test.js

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
'use strict';
22

3-
const http = require('http');
43
const CoreObject = require('core-object');
5-
const RSVP = require('rsvp');
6-
const expect = require('chai').expect;
7-
const FastbootCommand = CoreObject.extend(require('../lib/commands/fastboot'));
84
const camelize = require('ember-cli-string-utils').camelize;
95
const defaults = require('lodash.defaults');
6+
const expect = require('chai').expect;
7+
const FastbootCommand = CoreObject.extend(require('../lib/commands/fastboot'));
8+
const RSVP = require('rsvp');
109

1110
function CommandOptions(options) {
1211
const defaultOptions = {};
Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
1+
'use strict';
2+
3+
const CoreObject = require('core-object');
4+
const camelize = require('ember-cli-string-utils').camelize;
5+
const defaults = require('lodash.defaults');
6+
const EventEmitter = require('events').EventEmitter;
7+
const expect = require('chai').expect;
8+
const FastbootCommand = CoreObject.extend(require('../lib/commands/fastboot'));
9+
const FastbootServerTask = require('../lib/tasks/fastboot-server');
10+
const http = require('http');
11+
const RSVP = require('rsvp');
12+
const sinon = require('sinon');
13+
14+
function CommandOptions(options) {
15+
const defaultOptions = {};
16+
new FastbootCommand().availableOptions.forEach(o => {
17+
defaultOptions[camelize(o.name)] = o.default;
18+
});
19+
return defaults(options || {}, defaultOptions);
20+
};
21+
function MockServer() {
22+
EventEmitter.apply(this, arguments);
23+
this.listen = () => {};
24+
}
25+
MockServer.prototype = Object.create(EventEmitter.prototype);
26+
const mockUI = { writeLine() {} };
27+
28+
describe('fastboot server task', function() {
29+
let options, task;
30+
31+
beforeEach(function() {
32+
this.sinon = sinon.sandbox.create();
33+
task = new FastbootServerTask({
34+
ui: mockUI,
35+
});
36+
options = new CommandOptions();
37+
});
38+
39+
afterEach(function() {
40+
this.sinon.restore();
41+
});
42+
43+
describe('run', function() {
44+
it('calls restart on SIGHUP', function() {
45+
const restartStub = this.sinon.stub(task, 'restart');
46+
task.run(options);
47+
process.emit('SIGHUP');
48+
expect(restartStub.called).to.be.ok;
49+
});
50+
});
51+
52+
describe('restart', function() {
53+
let restartSpy, stopStub, clearRequireCacheStub, startStub;
54+
55+
beforeEach(function() {
56+
restartSpy = this.sinon.spy(task, 'restart');
57+
stopStub = this.sinon.stub(task, 'stop').returns(RSVP.resolve());
58+
clearRequireCacheStub = this.sinon.stub(task, 'clearRequireCache');
59+
startStub = this.sinon.stub(task, 'start');
60+
});
61+
62+
it('calls stop, clearRequireCache, and start', function() {
63+
return task.restart(options).then(() => {
64+
expect(restartSpy.callCount).to.equal(1);
65+
expect(stopStub.callCount).to.equal(1);
66+
expect(clearRequireCacheStub.callCount).to.equal(1);
67+
expect(startStub.callCount).to.equal(1);
68+
});
69+
});
70+
71+
it('can restart multiple times', function() {
72+
const restartPromise = task.restart(options);
73+
return restartPromise
74+
.then(() => task.restart(options))
75+
.then(() => {
76+
expect(restartSpy.callCount).to.equal(2);
77+
expect(stopStub.callCount).to.equal(2);
78+
expect(clearRequireCacheStub.callCount).to.equal(2);
79+
expect(startStub.callCount).to.equal(2);
80+
});
81+
});
82+
83+
// when outputReady while server is starting
84+
// (e.g. app file change during server npm install)
85+
// - wait on start, then reload
86+
// when outputReady multiple times during startup
87+
// (e.g. fast app build, slow server npm install)
88+
// - call reload only once
89+
it('restarts again just once for all calls during startup', function() {
90+
const restartPromise = task.restart(options);
91+
expect(task.restartPromise).to.equal(restartPromise);
92+
expect(task.restartAgain).to.equal(false);
93+
task.restart(options);
94+
task.restart(options);
95+
expect(task.restartPromise).to.equal(restartPromise);
96+
expect(task.restartAgain).to.equal(true);
97+
return restartPromise
98+
.then(() => {
99+
expect(task.restartPromise).to.not.equal(restartPromise);
100+
expect(task.restartAgain).to.equal(false);
101+
return task.restartPromise;
102+
})
103+
.then(() => {
104+
expect(task.restartPromise).to.equal(null);
105+
expect(task.restartAgain).to.equal(false);
106+
expect(restartSpy.callCount).to.equal(4);
107+
expect(stopStub.callCount).to.equal(2);
108+
expect(clearRequireCacheStub.callCount).to.equal(2);
109+
expect(startStub.callCount).to.equal(2);
110+
});
111+
});
112+
113+
it('can restart again after immediate restart completes', function() {
114+
const restartPromise = task.restart(options);
115+
expect(task.restartPromise).to.equal(restartPromise);
116+
expect(task.restartAgain).to.equal(false);
117+
task.restart(options);
118+
task.restart(options);
119+
expect(task.restartPromise).to.equal(restartPromise);
120+
expect(task.restartAgain).to.equal(true);
121+
return restartPromise
122+
.then(() => {
123+
expect(task.restartPromise).to.not.equal(restartPromise);
124+
expect(task.restartAgain).to.equal(false);
125+
return task.restartPromise;
126+
})
127+
.then(() => {
128+
expect(task.restartPromise).to.equal(null);
129+
expect(task.restartAgain).to.equal(false);
130+
return task.restart(options);
131+
})
132+
.then(() => {
133+
expect(task.restartPromise).to.equal(null);
134+
expect(task.restartAgain).to.equal(false);
135+
expect(restartSpy.callCount).to.equal(5);
136+
expect(stopStub.callCount).to.equal(3);
137+
expect(clearRequireCacheStub.callCount).to.equal(3);
138+
expect(startStub.callCount).to.equal(3);
139+
});
140+
});
141+
});
142+
143+
describe('start', function() {
144+
let createServerStub, execStub, mockServer, requireSpy, useStub;
145+
const mockApp = { get() {}, use() {} };
146+
const mockExpress = () => mockApp;
147+
const mockExpressStatic = {};
148+
mockExpress.static = () => mockExpressStatic;
149+
const mockRequire = (which) => {
150+
if (which === 'express') { return mockExpress; }
151+
if (which === 'fastboot-express-middleware') return () => {};
152+
};
153+
154+
beforeEach(function() {
155+
mockServer = new MockServer();
156+
createServerStub = this.sinon.stub(task.http, 'createServer').returns(mockServer);
157+
execStub = this.sinon.stub(task, 'exec').returns(RSVP.resolve());
158+
task.require = mockRequire;
159+
requireSpy = this.sinon.spy(task, 'require');
160+
useStub = this.sinon.stub(mockApp, 'use');
161+
});
162+
163+
it('runs npm install in server root', function() {
164+
return task.start(options)
165+
.then(() => {
166+
expect(execStub.calledWith('npm install', { cwd: 'dist' })).to.equal(true);
167+
});
168+
});
169+
170+
it('requires server dependencies', function() {
171+
return task.start(options)
172+
.then(() => {
173+
expect(requireSpy.calledWith('fastboot-express-middleware')).to.equal(true);
174+
expect(requireSpy.calledWith('express')).to.equal(true);
175+
});
176+
});
177+
178+
it('uses express.static when serve-assets=true', function() {
179+
options = new CommandOptions({ serveAssets: true });
180+
return task.start(options)
181+
.then(() => {
182+
expect(useStub.calledWith(mockExpressStatic)).to.equal(true);
183+
});
184+
});
185+
186+
it('tracks open sockets using connection and close events', function() {
187+
return task.start(options)
188+
.then(() => {
189+
expect(Object.keys(task.sockets).length).to.equal(0);
190+
expect(task.nextSocketId).to.equal(0);
191+
let socket = new EventEmitter();
192+
mockServer.emit('connection', socket);
193+
expect(Object.keys(task.sockets).length).to.equal(1);
194+
expect(task.sockets[0]).to.equal(socket);
195+
expect(task.nextSocketId).to.equal(1);
196+
socket.emit('close');
197+
expect(Object.keys(task.sockets).length).to.equal(0);
198+
expect(task.sockets[0]).to.equal(undefined);
199+
mockServer.emit('connection', socket);
200+
expect(Object.keys(task.sockets).length).to.equal(1);
201+
expect(task.sockets[1]).to.equal(socket);
202+
expect(task.nextSocketId).to.equal(2);
203+
})
204+
});
205+
});
206+
207+
describe('stop', function() {
208+
it('is safe to call before start', function() {
209+
let stopPromise;
210+
const callStop = () => { stopPromise = task.stop(options); };
211+
expect(callStop).to.not.throw();
212+
return stopPromise;
213+
});
214+
215+
// Sequence of calls
216+
// 1. server.close (stop opening sockets and set close callback)
217+
// 2. sockets[i].destroy
218+
// 3. close callback
219+
it('destroys open sockets after calling server.close', function() {
220+
let closeCallback;
221+
const destroyPromise = new RSVP.Promise(resolve => { });
222+
const mockServer = { close(cb) { closeCallback = cb; } };
223+
const closeSpy = this.sinon.spy(mockServer, 'close');
224+
const mockSocket = {
225+
destroy() {
226+
expect(closeSpy.called).to.equal(true);
227+
if (closeCallback) closeCallback();
228+
}
229+
};
230+
const destroySpy = this.sinon.spy(mockSocket, 'destroy');
231+
task.httpServer = mockServer;
232+
task.sockets[0] = mockSocket;
233+
task.stop(options)
234+
.then(() => {
235+
expect(destroySpy.called).to.equal(true);
236+
expect(task.httpServer).to.equal(null);
237+
});
238+
});
239+
});
240+
});

0 commit comments

Comments
 (0)