ember-cli-ajh
Version:
Command line tool for developing ambitious ember.js apps
961 lines (865 loc) • 28.1 kB
JavaScript
;
var expect = require('chai').expect;
var ExpressServer = require('../../../../lib/tasks/server/express-server');
var Promise = require('../../../../lib/ext/promise');
var MockUI = require('../../../helpers/mock-ui');
var MockProject = require('../../../helpers/mock-project');
var MockWatcher = require('../../../helpers/mock-watcher');
var MockServerWatcher = require('../../../helpers/mock-server-watcher');
var ProxyServer = require('../../../helpers/proxy-server');
var chalk = require('chalk');
var request = require('supertest');
var net = require('net');
var EOL = require('os').EOL;
var nock = require('nock');
var express = require('express');
describe('express-server', function() {
var subject, ui, project, proxy, nockProxy;
nock.enableNetConnect();
beforeEach(function() {
this.timeout(10000);
ui = new MockUI();
project = new MockProject();
proxy = new ProxyServer();
subject = new ExpressServer({
ui: ui,
project: project,
watcher: new MockWatcher(),
serverWatcher: new MockServerWatcher(),
serverRestartDelayTime: 100,
serverRoot: './server',
proxyMiddleware: function() {
return proxy.handler.bind(proxy);
},
environment: 'development'
});
});
afterEach(function() {
try {
subject.httpServer.close();
} catch(err) { }
try {
proxy.httpServer.close();
} catch(err) { }
});
describe('displayHost', function() {
it('should use the specified host if specified', function() {
expect(subject.displayHost('1.2.3.4')).to.equal('1.2.3.4');
});
it('should use the use localhost if host is not specified', function() {
expect(subject.displayHost(undefined)).to.equal('localhost');
});
});
describe('processAppMiddlewares', function() {
it('has a good error message if a file exists, but does not export a function', function() {
subject.project = {
has: function() { return true; },
require: function() { return {}; }
};
expect(function() {
subject.processAppMiddlewares();
}).to.throw(TypeError, 'ember-cli expected ./server/index.js to be the entry for your mock or proxy server');
});
it('returns values returned by server/index', function(){
subject.project = {
has: function() { return true; },
require: function() {
return function(){ return 'foo'; };
}
};
expect(subject.processAppMiddlewares()).to.equal('foo');
});
});
describe('output', function() {
this.timeout(40000);
it('with ssl', function() {
return subject.start({
host: undefined,
port: '1337',
ssl: true,
sslCert: 'tests/fixtures/ssl/server.crt',
sslKey: 'tests/fixtures/ssl/server.key',
baseURL: '/'
}).then(function() {
var output = ui.output.trim().split(EOL);
expect(output[0]).to.equal('Serving on https://localhost:1337/');
});
});
it('with proxy', function() {
return subject.start({
proxy: 'http://localhost:3001/',
host: undefined,
port: '1337',
baseURL: '/'
}).then(function() {
var output = ui.output.trim().split(EOL);
expect(output[1]).to.equal('Serving on http://localhost:1337/');
expect(output[0]).to.equal('Proxying to http://localhost:3001/');
expect(output.length).to.equal(2, 'expected only two lines of output');
});
});
it('without proxy', function() {
return subject.start({
host: undefined,
port: '1337',
baseURL: '/'
}).then(function() {
var output = ui.output.trim().split(EOL);
expect(output[0]).to.equal('Serving on http://localhost:1337/');
expect(output.length).to.equal(1, 'expected only one line of output');
});
});
it('with baseURL', function() {
return subject.start({
host: undefined,
port: '1337',
baseURL: '/foo'
}).then(function() {
var output = ui.output.trim().split(EOL);
expect(output[0]).to.equal('Serving on http://localhost:1337/foo/');
expect(output.length).to.equal(1, 'expected only one line of output');
});
});
it('address in use', function() {
var preexistingServer = net.createServer();
preexistingServer.listen(1337);
return subject.start({
host: undefined,
port: '1337'
})
.then(function() {
expect(false, 'should have rejected');
})
.catch(function(reason) {
expect(reason.message).to.equal('Could not serve on http://localhost:1337. It is either in use or you do not have permission.');
})
.finally(function() {
preexistingServer.close();
});
});
});
describe('behaviour', function() {
it('starts with ssl if ssl option is passed', function() {
return subject.start({
host: 'localhost',
port: '1337',
ssl: true,
sslCert: 'tests/fixtures/ssl/server.crt',
sslKey: 'tests/fixtures/ssl/server.key',
baseURL: '/'
})
.then(function() {
return new Promise(function(resolve, reject) {
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
request('https://localhost:1337', {strictSSL: false}).
get('/').expect(200, function(err, value) {
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '1';
if(err) { reject(err); }
else { resolve(value); }
});
});
});
}),
it('app middlewares are processed before the proxy', function(done) {
var expected = '/foo was hit';
project.require = function() {
return function(app) {
app.use('/foo', function(req,res) {
res.send(expected);
});
};
};
subject.start({
proxy: 'http://localhost:3001/',
host: undefined,
port: '1337',
baseURL: '/'
})
.then(function() {
request(subject.app)
.get('/foo')
.set('accept', 'application/json, */*')
.expect(function(res) {
expect(res.text).to.equal(expected);
})
.end(function(err) {
if (err) {
return done(err);
}
expect(proxy.called).to.equal(false);
done();
});
});
});
it('works with a regular express app', function(done) {
var expected = '/foo was hit';
project.require = function() {
var app = express();
app.use('/foo', function(req,res) {
res.send(expected);
});
return app;
};
subject.start({
proxy: 'http://localhost:3001/',
host: undefined,
port: '1337',
baseURL: '/'
})
.then(function() {
request(subject.app)
.get('/foo')
.set('accept', 'application/json, */*')
.expect(function(res) {
expect(res.text).to.equal(expected);
})
.end(function(err) {
if (err) {
return done(err);
}
expect(proxy.called).to.equal(false);
done();
});
});
});
describe('with proxy', function() {
beforeEach(function() {
return subject.start({
proxy: 'http://localhost:3001/',
host: undefined,
port: '1337',
baseURL: '/'
});
});
function bypassTest(app, url, done, responseCallback) {
request(app)
.get(url)
.set('accept', 'text/html')
.end(function(err, response) {
if (err) {
return done(err);
}
expect(proxy.called).to.equal(false);
if (responseCallback) { responseCallback(response); }
done();
});
}
it('bypasses proxy for /', function(done) {
bypassTest(subject.app, '/', done);
});
it('bypasses proxy for files that exist', function(done) {
bypassTest(subject.app, '/test-file.txt', done, function(response) {
expect(response.text.trim()).to.equal('some contents');
});
});
function apiTest(app, method, url, done) {
var req = request(app);
return req[method].call(req, url)
.set('accept', 'text/json')
.end(function(err) {
if (err) {
return done(err);
}
expect(proxy.called, 'proxy receives the request');
expect(proxy.lastReq.method).to.equal(method.toUpperCase());
expect(proxy.lastReq.url).to.equal(url);
done();
});
}
it('proxies GET', function(done) {
apiTest(subject.app, 'get', '/api/get', done);
});
it('proxies PUT', function(done) {
apiTest(subject.app, 'put', '/api/put', done);
});
it('proxies POST', function(done) {
apiTest(subject.app, 'post', '/api/post', done);
});
it('proxies DELETE', function(done) {
apiTest(subject.app, 'delete', '/api/delete', done);
});
// test for #1263
it('proxies when accept contains */*', function(done) {
request(subject.app)
.get('/api/get')
.set('accept', 'application/json, */*')
.end(function(err) {
if (err) {
return done(err);
}
expect(proxy.called, 'proxy receives the request');
done();
});
});
});
describe('proxy with subdomain', function() {
beforeEach(function() {
nockProxy = {
called: null,
method: null,
url: null
};
return subject.start({
proxy: 'http://api.lvh.me',
host: undefined,
port: '1337',
baseURL: '/'
});
});
function apiTest(app, method, url, done) {
var req = request(app);
return req[method].call(req, url)
.set('accept', 'text/json')
.end(function(err) {
if (err) {
return done(err);
}
expect(nockProxy.called, 'proxy receives the request');
expect(nockProxy.method).to.equal(method.toUpperCase());
expect(nockProxy.url).to.equal(url);
done();
});
}
it('proxies GET', function(done) {
nock('http://api.lvh.me', {
reqheaders: {
'host': 'api.lvh.me'
}
}).get('/api/get')
.reply(200, function() {
nockProxy.called = true;
nockProxy.method = 'GET';
nockProxy.url = '/api/get';
return '';
});
apiTest(subject.app, 'get', '/api/get', done);
});
it('proxies PUT', function(done) {
nock('http://api.lvh.me', {
reqheaders: {
'host': 'api.lvh.me'
}
}).put('/api/put')
.reply(204, function() {
nockProxy.called = true;
nockProxy.method = 'PUT';
nockProxy.url = '/api/put';
return '';
});
apiTest(subject.app, 'put', '/api/put', done);
});
it('proxies POST', function(done) {
nock('http://api.lvh.me', {
reqheaders: {
'host': 'api.lvh.me'
}
}).post('/api/post')
.reply(201, function() {
nockProxy.called = true;
nockProxy.method = 'POST';
nockProxy.url = '/api/post';
return '';
});
apiTest(subject.app, 'post', '/api/post', done);
});
it('proxies DELETE', function(done) {
nock('http://api.lvh.me', {
reqheaders: {
'host': 'api.lvh.me'
}
}).delete('/api/delete')
.reply(204, function() {
nockProxy.called = true;
nockProxy.method = 'DELETE';
nockProxy.url = '/api/delete';
return '';
});
apiTest(subject.app, 'delete', '/api/delete', done);
});
// test for #1263
it('proxies when accept contains */*', function(done) {
nock('http://api.lvh.me')
.get('/api/get')
.reply(200, function() {
nockProxy.called = true;
nockProxy.method = 'GET';
nockProxy.url = '/api/get';
return '';
});
request(subject.app)
.get('/api/get')
.set('accept', 'application/json, */*')
.end(function(err) {
if (err) {
return done(err);
}
expect(nockProxy.called, 'proxy receives the request');
done();
});
});
});
describe('without proxy', function() {
function startServer(baseURL) {
return subject.start({
host: undefined,
port: '1337',
baseURL: baseURL || '/'
});
}
it('serves index.html when file not found with auto/history location', function(done) {
return startServer()
.then(function() {
request(subject.app)
.get('/someurl.withperiod')
.set('accept', 'text/html')
.expect(200)
.expect('Content-Type', /html/)
.end(function(err) {
if (err) {
return done(err);
}
done();
});
});
});
it('GET /tests serves tests/index.html for mime of */* (hash location)', function(done) {
project._config = {
baseURL: '/',
locationType: 'hash'
};
return startServer()
.then(function() {
request(subject.app)
.get('/tests')
.set('accept', '*/*')
.expect(200)
.expect('Content-Type', /html/)
.end(function(err) {
if (err) {
return done(err);
}
done();
});
});
});
it('GET /tests serves tests/index.html for mime of */* (auto location)', function(done) {
return startServer()
.then(function() {
request(subject.app)
.get('/tests')
.set('accept', '*/*')
.expect(200)
.expect('Content-Type', /html/)
.end(function(err) {
if (err) {
return done(err);
}
done();
});
});
});
it('GET /tests/whatever serves tests/index.html when file not found', function(done) {
return startServer()
.then(function() {
request(subject.app)
.get('/tests/whatever')
.set('accept', 'text/html')
.expect(200)
.expect('Content-Type', /html/)
.end(function(err) {
if (err) {
return done(err);
}
done();
});
});
});
it('GET /tests/an-existing-file.tla serves tests/an-existing-file.tla if it is found', function(done) {
return startServer()
.then(function() {
request(subject.app)
.get('/tests/test-file.txt')
.set('accept', 'text/html')
.expect(200)
.expect(/some contents/)
.expect('Content-Type', /text/)
.end(function(err) {
if (err) {
return done(err);
}
done();
});
});
});
it('serves index.html when file not found (with baseURL) with auto/history location', function(done) {
return startServer('/foo')
.then(function() {
request(subject.app)
.get('/foo/someurl')
.set('accept', 'text/html')
.expect(200)
.expect('Content-Type', /html/)
.end(function(err) {
if (err) {
return done(err);
}
done();
});
});
});
it('serves index.html when file not found (with baseURL) with custom history location', function(done) {
project._config = {
baseURL: '/',
locationType: 'blahr',
historySupportMiddleware: true
};
return startServer('/foo')
.then(function() {
request(subject.app)
.get('/foo/someurl')
.set('accept', 'text/html')
.expect(200)
.expect('Content-Type', /html/)
.end(function(err) {
if (err) {
return done(err);
}
done();
});
});
});
it('returns a 404 when file not found with hash location', function(done) {
project._config = {
baseURL: '/',
locationType: 'hash'
};
return startServer()
.then(function() {
request(subject.app)
.get('/someurl.withperiod')
.set('accept', 'text/html')
.expect(404)
.end(done);
});
});
it('files that exist in broccoli directory are served up', function(done) {
return startServer()
.then(function() {
request(subject.app)
.get('/test-file.txt')
.end(function(err, response) {
expect(response.text.trim()).to.equal('some contents');
done();
});
});
});
it('serves static asset up from build output without a period in name', function(done) {
return startServer()
.then(function() {
request(subject.app)
.get('/someurl-without-period')
.expect(200)
.end(function(err, response) {
if (err) {
return done(err);
}
expect(response.text.trim()).to.equal('some other content');
done();
});
});
});
it('serves static asset up from build output without a period in name (with baseURL)', function(done) {
return startServer('/foo')
.then(function() {
request(subject.app)
.get('/foo/someurl-without-period')
.expect(200)
.end(function(err, response) {
if (err) {
return done(err);
}
expect(response.text.trim()).to.equal('some other content');
done();
});
});
});
});
describe('addons', function() {
var calls;
beforeEach(function() {
calls = 0;
subject.processAddonMiddlewares = function() {
calls++;
};
});
it('calls processAddonMiddlewares upon start', function() {
return subject.start({
host: undefined,
port: '1337'
}).then(function() {
expect(calls).to.equal(1);
});
});
});
describe('addon middleware', function() {
var firstCalls;
var secondCalls;
beforeEach(function() {
firstCalls = 0;
secondCalls = 0;
project.initializeAddons = function() { };
project.addons = [{
serverMiddleware: function() {
firstCalls++;
}
}, {
serverMiddleware: function() {
secondCalls++;
}
}, {
doesntGoBoom: null
}];
});
it('calls serverMiddleware on the addons on start', function() {
return subject.start({
host: undefined,
port: '1337'
}).then(function() {
expect(firstCalls).to.equal(1);
expect(secondCalls).to.equal(1);
});
});
it('calls serverMiddleware on the addons on restart', function() {
return subject.start({
host: undefined,
port: '1337'
}).then(function() {
subject.changedFiles = ['bar.js'];
return subject.restartHttpServer();
}).then(function() {
expect(firstCalls).to.equal(2);
expect(secondCalls).to.equal(2);
});
});
});
describe('addon middleware is async', function(){
var order = [];
beforeEach(function() {
project.initializeAddons = function() { };
project.addons = [
{
serverMiddleware: function () {
order.push('first');
}
},
{
serverMiddleware: function() {
return new Promise(function(resolve) {
setTimeout(function(){
order.push('second');
resolve();
}, 50);
});
}
}, {
serverMiddleware: function() {
order.push('third');
}
}
];
});
it('waits for async middleware to complete before the next middleware', function(){
return subject.start({
host: undefined,
port: '1337'
}).then(function() {
expect(order[0]).to.equal('first');
expect(order[1]).to.equal('second');
expect(order[2]).to.equal('third');
});
});
});
describe('addon middleware bubble errors', function(){
beforeEach(function() {
project.initializeAddons = function() { };
project.addons = [{
serverMiddleware: function() {
return Promise.reject('addon middleware fail');
}
}
];
});
it('up to server start', function(){
return subject.start({
host: undefined,
port: '1337'
})
.catch(function(reason){
expect(reason).to.equal('addon middleware fail');
});
});
});
describe('app middleware', function() {
var passedOptions;
var calls;
beforeEach(function() {
passedOptions = null;
calls = 0;
subject.processAppMiddlewares = function(options) {
passedOptions = options;
calls++;
};
});
it('calls processAppMiddlewares upon start', function() {
var realOptions = {
host: undefined,
port: '1337'
};
return subject.start(realOptions).then(function() {
expect(passedOptions === realOptions).to.equal(true);
expect(calls).to.equal(1);
});
});
it('calls processAppMiddlewares upon restart', function() {
var realOptions = {
host: undefined,
port: '1337'
};
var originalApp;
return subject.start(realOptions)
.then(function() {
originalApp = subject.app;
subject.changedFiles = ['bar.js'];
return subject.restartHttpServer();
})
.then(function() {
expect(subject.app);
expect(originalApp).to.not.equal(subject.app);
expect(passedOptions === realOptions).to.equal(true);
expect(calls).to.equal(2);
});
});
it('includes httpServer instance in options', function() {
var passedOptions;
subject.processAppMiddlewares = function(options) {
passedOptions = options;
};
var realOptions = {
host: undefined,
port: '1337'
};
return subject.start(realOptions).then(function() {
expect(!!passedOptions.httpServer.listen);
});
});
});
describe('serverWatcherDidChange', function() {
it('is called on file change', function() {
var calls = 0;
subject.serverWatcherDidChange = function() {
calls++;
};
return subject.start({
host: undefined,
port: '1337'
}).then(function() {
subject.serverWatcher.emit('change', 'foo.txt');
expect(calls).to.equal(1);
});
});
it('schedules a server restart', function() {
var calls = 0;
subject.scheduleServerRestart = function() {
calls++;
};
return subject.start({
host: undefined,
port: '1337'
}).then(function() {
subject.serverWatcher.emit('change', 'foo.txt');
subject.serverWatcher.emit('change', 'bar.txt');
expect(calls).to.equal(2);
});
});
});
describe('scheduleServerRestart', function() {
it('schedules exactly one call of restartHttpServer', function(done) {
var calls = 0;
subject.restartHttpServer = function() {
calls++;
};
subject.scheduleServerRestart();
expect(calls).to.equal(0);
setTimeout(function() {
expect(calls).to.equal(0);
subject.scheduleServerRestart();
}, 50);
setTimeout(function() {
expect(calls).to.equal(1);
done();
}, 175);
});
});
describe('restartHttpServer', function() {
it('restarts the server', function() {
var originalHttpServer;
var originalApp;
return subject.start({
host: undefined,
port: '1337'
}).then(function() {
ui.output = '';
originalHttpServer = subject.httpServer;
originalApp = subject.app;
subject.changedFiles = ['bar.js'];
return subject.restartHttpServer();
}).then(function() {
expect(ui.output).to.equal(EOL + chalk.green('Server restarted.') + EOL + EOL);
expect(subject.httpServer, 'HTTP server exists');
expect(subject.httpServer).to.not.equal(originalHttpServer, 'HTTP server has changed');
expect(!!subject.app).to.equal(true, 'App exists');
expect(subject.app).to.not.equal(originalApp, 'App has changed');
});
});
it('restarts the server again if one or more files change during a previous restart', function() {
var originalHttpServer;
var originalApp;
return subject.start({
host: undefined,
port: '1337'
}).then(function() {
originalHttpServer = subject.httpServer;
originalApp = subject.app;
subject.serverRestartPromise = new Promise(function(resolve) {
setTimeout(function () {
subject.serverRestartPromise = null;
resolve();
}, 20);
});
subject.changedFiles = ['bar.js'];
return subject.restartHttpServer();
}).then(function() {
expect(!!subject.httpServer).to.equal(true, 'HTTP server exists');
expect(subject.httpServer).to.not.equal(originalHttpServer, 'HTTP server has changed');
expect(!!subject.app).to.equal(true, 'App exists');
expect(subject.app).to.not.equal(originalApp, 'App has changed');
});
});
it('emits the restart event', function() {
var calls = 0;
subject.on('restart', function() {
calls++;
});
return subject.start({
host: undefined,
port: '1337'
}).then(function() {
subject.changedFiles = ['bar.js'];
return subject.restartHttpServer();
}).then(function() {
expect(calls).to.equal(1);
});
});
});
});
});