2014-11-21 01:46:21 +00:00
|
|
|
/*jshint -W030 */
|
|
|
|
|
Scope app middleware to a list of paths
Add a new argument to `app.middleware` allowing developers
to restrict the middleware to a list of paths or regular expresions.
Modify `app.middlewareFromConfig` to pass `config.paths` as the second
arg of `app.middleware`.
Examples:
// A string path (interpreted via path-to-regexp)
app.middleware('auth', '/admin', ldapAuth);
// A regular expression
app.middleware('initial', /^\/~(admin|root)/, rejectWith404);
// A list of scopes
app.middleware('routes', ['/api', /^\/assets/.*\.json$/], foo);
// From config
app.middlewareFromConfig(
handlerFactory,
{
phase: 'initial',
paths: ['/scope', /^\/(a|b)/]
});
2014-11-19 11:03:47 +00:00
|
|
|
var async = require('async');
|
2013-12-18 03:15:48 +00:00
|
|
|
var path = require('path');
|
2014-11-11 16:51:50 +00:00
|
|
|
|
Middleware phases - initial implementation
Modify the app and router implementation, so that the middleware is
executed in order defined by phases.
Predefined phases:
'initial', 'session', 'auth', 'parse', 'routes', 'files', 'final'
Methods defined via `app.use`, `app.route` and friends are executed
as the first thing in 'routes' phase.
API usage:
app.middleware('initial', compression());
app.middleware('initial:before', serveFavicon());
app.middleware('files:after', loopback.urlNotFound());
app.middleware('final:after', errorHandler());
Middleware flavours:
// regular handler
function handler(req, res, next) {
// do stuff
next();
}
// error handler
function errorHandler(err, req, res, next) {
// handle error and/or call next
next(err);
}
2014-11-05 19:07:58 +00:00
|
|
|
var http = require('http');
|
2014-12-10 08:36:45 +00:00
|
|
|
var express = require('express');
|
2014-05-03 04:19:14 +00:00
|
|
|
var loopback = require('../');
|
2014-06-05 07:45:09 +00:00
|
|
|
var PersistedModel = loopback.PersistedModel;
|
2013-12-18 03:15:48 +00:00
|
|
|
|
2014-06-03 08:39:54 +00:00
|
|
|
var describe = require('./util/describe');
|
|
|
|
var it = require('./util/it');
|
|
|
|
|
2013-06-07 19:57:51 +00:00
|
|
|
describe('app', function() {
|
Middleware phases - initial implementation
Modify the app and router implementation, so that the middleware is
executed in order defined by phases.
Predefined phases:
'initial', 'session', 'auth', 'parse', 'routes', 'files', 'final'
Methods defined via `app.use`, `app.route` and friends are executed
as the first thing in 'routes' phase.
API usage:
app.middleware('initial', compression());
app.middleware('initial:before', serveFavicon());
app.middleware('files:after', loopback.urlNotFound());
app.middleware('final:after', errorHandler());
Middleware flavours:
// regular handler
function handler(req, res, next) {
// do stuff
next();
}
// error handler
function errorHandler(err, req, res, next) {
// handle error and/or call next
next(err);
}
2014-11-05 19:07:58 +00:00
|
|
|
describe.onServer('.middleware(phase, handler)', function() {
|
|
|
|
var app;
|
|
|
|
var steps;
|
|
|
|
|
|
|
|
beforeEach(function setup() {
|
|
|
|
app = loopback();
|
|
|
|
steps = [];
|
|
|
|
});
|
|
|
|
|
|
|
|
it('runs middleware in phases', function(done) {
|
|
|
|
var PHASES = [
|
|
|
|
'initial', 'session', 'auth', 'parse',
|
|
|
|
'routes', 'files', 'final'
|
|
|
|
];
|
|
|
|
|
|
|
|
PHASES.forEach(function(name) {
|
|
|
|
app.middleware(name, namedHandler(name));
|
|
|
|
});
|
|
|
|
app.use(namedHandler('main'));
|
|
|
|
|
2014-11-11 16:51:50 +00:00
|
|
|
executeMiddlewareHandlers(app, function(err) {
|
Middleware phases - initial implementation
Modify the app and router implementation, so that the middleware is
executed in order defined by phases.
Predefined phases:
'initial', 'session', 'auth', 'parse', 'routes', 'files', 'final'
Methods defined via `app.use`, `app.route` and friends are executed
as the first thing in 'routes' phase.
API usage:
app.middleware('initial', compression());
app.middleware('initial:before', serveFavicon());
app.middleware('files:after', loopback.urlNotFound());
app.middleware('final:after', errorHandler());
Middleware flavours:
// regular handler
function handler(req, res, next) {
// do stuff
next();
}
// error handler
function errorHandler(err, req, res, next) {
// handle error and/or call next
next(err);
}
2014-11-05 19:07:58 +00:00
|
|
|
if (err) return done(err);
|
|
|
|
expect(steps).to.eql([
|
|
|
|
'initial', 'session', 'auth', 'parse',
|
|
|
|
'main', 'routes', 'files', 'final'
|
|
|
|
]);
|
|
|
|
done();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2014-12-10 08:36:45 +00:00
|
|
|
it('preserves order of handlers in the same phase', function(done) {
|
|
|
|
app.middleware('initial', namedHandler('first'));
|
|
|
|
app.middleware('initial', namedHandler('second'));
|
|
|
|
|
|
|
|
executeMiddlewareHandlers(app, function(err) {
|
|
|
|
if (err) return done(err);
|
|
|
|
expect(steps).to.eql(['first', 'second']);
|
|
|
|
done();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2014-11-21 01:46:21 +00:00
|
|
|
it('supports `before:` and `after:` prefixes', function(done) {
|
Middleware phases - initial implementation
Modify the app and router implementation, so that the middleware is
executed in order defined by phases.
Predefined phases:
'initial', 'session', 'auth', 'parse', 'routes', 'files', 'final'
Methods defined via `app.use`, `app.route` and friends are executed
as the first thing in 'routes' phase.
API usage:
app.middleware('initial', compression());
app.middleware('initial:before', serveFavicon());
app.middleware('files:after', loopback.urlNotFound());
app.middleware('final:after', errorHandler());
Middleware flavours:
// regular handler
function handler(req, res, next) {
// do stuff
next();
}
// error handler
function errorHandler(err, req, res, next) {
// handle error and/or call next
next(err);
}
2014-11-05 19:07:58 +00:00
|
|
|
app.middleware('routes:before', namedHandler('routes:before'));
|
|
|
|
app.middleware('routes:after', namedHandler('routes:after'));
|
|
|
|
app.use(namedHandler('main'));
|
|
|
|
|
2014-11-11 16:51:50 +00:00
|
|
|
executeMiddlewareHandlers(app, function(err) {
|
Middleware phases - initial implementation
Modify the app and router implementation, so that the middleware is
executed in order defined by phases.
Predefined phases:
'initial', 'session', 'auth', 'parse', 'routes', 'files', 'final'
Methods defined via `app.use`, `app.route` and friends are executed
as the first thing in 'routes' phase.
API usage:
app.middleware('initial', compression());
app.middleware('initial:before', serveFavicon());
app.middleware('files:after', loopback.urlNotFound());
app.middleware('final:after', errorHandler());
Middleware flavours:
// regular handler
function handler(req, res, next) {
// do stuff
next();
}
// error handler
function errorHandler(err, req, res, next) {
// handle error and/or call next
next(err);
}
2014-11-05 19:07:58 +00:00
|
|
|
if (err) return done(err);
|
|
|
|
expect(steps).to.eql(['routes:before', 'main', 'routes:after']);
|
|
|
|
done();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2015-02-25 17:47:18 +00:00
|
|
|
it('allows extra handlers on express stack during app.use', function(done) {
|
|
|
|
function handlerThatAddsHandler(name) {
|
|
|
|
app.use(namedHandler('extra-handler'));
|
|
|
|
return namedHandler(name);
|
|
|
|
}
|
|
|
|
|
|
|
|
var myHandler;
|
|
|
|
app.middleware('routes:before',
|
|
|
|
myHandler = handlerThatAddsHandler('my-handler'));
|
|
|
|
var found = app._findLayerByHandler(myHandler);
|
|
|
|
expect(found).to.be.object;
|
|
|
|
expect(myHandler).to.equal(found.handle);
|
|
|
|
expect(found).have.property('phase', 'routes:before');
|
|
|
|
executeMiddlewareHandlers(app, function(err) {
|
|
|
|
if (err) return done(err);
|
|
|
|
expect(steps).to.eql(['my-handler', 'extra-handler']);
|
|
|
|
done();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
it('allows handlers to be wrapped as __NR_handler on express stack',
|
|
|
|
function(done) {
|
|
|
|
var myHandler = namedHandler('my-handler');
|
|
|
|
var wrappedHandler = function(req, res, next) {
|
|
|
|
myHandler(req, res, next);
|
|
|
|
};
|
|
|
|
wrappedHandler['__NR_handler'] = myHandler;
|
|
|
|
app.middleware('routes:before', wrappedHandler);
|
|
|
|
var found = app._findLayerByHandler(myHandler);
|
|
|
|
expect(found).to.be.object;
|
|
|
|
expect(found).have.property('phase', 'routes:before');
|
|
|
|
executeMiddlewareHandlers(app, function(err) {
|
|
|
|
if (err) return done(err);
|
|
|
|
expect(steps).to.eql(['my-handler']);
|
|
|
|
done();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
it('allows handlers to be wrapped as a property on express stack',
|
|
|
|
function(done) {
|
|
|
|
var myHandler = namedHandler('my-handler');
|
|
|
|
var wrappedHandler = function(req, res, next) {
|
|
|
|
myHandler(req, res, next);
|
|
|
|
};
|
|
|
|
wrappedHandler['__handler'] = myHandler;
|
|
|
|
app.middleware('routes:before', wrappedHandler);
|
|
|
|
var found = app._findLayerByHandler(myHandler);
|
|
|
|
expect(found).to.be.object;
|
|
|
|
expect(found).have.property('phase', 'routes:before');
|
|
|
|
executeMiddlewareHandlers(app, function(err) {
|
|
|
|
if (err) return done(err);
|
|
|
|
expect(steps).to.eql(['my-handler']);
|
|
|
|
done();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
Middleware phases - initial implementation
Modify the app and router implementation, so that the middleware is
executed in order defined by phases.
Predefined phases:
'initial', 'session', 'auth', 'parse', 'routes', 'files', 'final'
Methods defined via `app.use`, `app.route` and friends are executed
as the first thing in 'routes' phase.
API usage:
app.middleware('initial', compression());
app.middleware('initial:before', serveFavicon());
app.middleware('files:after', loopback.urlNotFound());
app.middleware('final:after', errorHandler());
Middleware flavours:
// regular handler
function handler(req, res, next) {
// do stuff
next();
}
// error handler
function errorHandler(err, req, res, next) {
// handle error and/or call next
next(err);
}
2014-11-05 19:07:58 +00:00
|
|
|
it('injects error from previous phases into the router', function(done) {
|
|
|
|
var expectedError = new Error('expected error');
|
|
|
|
|
|
|
|
app.middleware('initial', function(req, res, next) {
|
|
|
|
steps.push('initial');
|
|
|
|
next(expectedError);
|
|
|
|
});
|
|
|
|
|
|
|
|
// legacy solution for error handling
|
|
|
|
app.use(function errorHandler(err, req, res, next) {
|
|
|
|
expect(err).to.equal(expectedError);
|
|
|
|
steps.push('error');
|
|
|
|
next();
|
|
|
|
});
|
|
|
|
|
2014-11-11 16:51:50 +00:00
|
|
|
executeMiddlewareHandlers(app, function(err) {
|
Middleware phases - initial implementation
Modify the app and router implementation, so that the middleware is
executed in order defined by phases.
Predefined phases:
'initial', 'session', 'auth', 'parse', 'routes', 'files', 'final'
Methods defined via `app.use`, `app.route` and friends are executed
as the first thing in 'routes' phase.
API usage:
app.middleware('initial', compression());
app.middleware('initial:before', serveFavicon());
app.middleware('files:after', loopback.urlNotFound());
app.middleware('final:after', errorHandler());
Middleware flavours:
// regular handler
function handler(req, res, next) {
// do stuff
next();
}
// error handler
function errorHandler(err, req, res, next) {
// handle error and/or call next
next(err);
}
2014-11-05 19:07:58 +00:00
|
|
|
if (err) return done(err);
|
|
|
|
expect(steps).to.eql(['initial', 'error']);
|
|
|
|
done();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
it('passes unhandled error to callback', function(done) {
|
|
|
|
var expectedError = new Error('expected error');
|
|
|
|
|
|
|
|
app.middleware('initial', function(req, res, next) {
|
|
|
|
next(expectedError);
|
|
|
|
});
|
|
|
|
|
2014-11-11 16:51:50 +00:00
|
|
|
executeMiddlewareHandlers(app, function(err) {
|
Middleware phases - initial implementation
Modify the app and router implementation, so that the middleware is
executed in order defined by phases.
Predefined phases:
'initial', 'session', 'auth', 'parse', 'routes', 'files', 'final'
Methods defined via `app.use`, `app.route` and friends are executed
as the first thing in 'routes' phase.
API usage:
app.middleware('initial', compression());
app.middleware('initial:before', serveFavicon());
app.middleware('files:after', loopback.urlNotFound());
app.middleware('final:after', errorHandler());
Middleware flavours:
// regular handler
function handler(req, res, next) {
// do stuff
next();
}
// error handler
function errorHandler(err, req, res, next) {
// handle error and/or call next
next(err);
}
2014-11-05 19:07:58 +00:00
|
|
|
expect(err).to.equal(expectedError);
|
|
|
|
done();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2014-11-12 09:09:20 +00:00
|
|
|
it('passes errors to error handlers in the same phase', function(done) {
|
|
|
|
var expectedError = new Error('this should be handled by middleware');
|
|
|
|
var handledError;
|
|
|
|
|
|
|
|
app.middleware('initial', function(req, res, next) {
|
|
|
|
// continue in the next tick, this verifies that the next
|
|
|
|
// handler waits until the previous one is done
|
|
|
|
process.nextTick(function() {
|
|
|
|
next(expectedError);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
app.middleware('initial', function(err, req, res, next) {
|
|
|
|
handledError = err;
|
|
|
|
next();
|
|
|
|
});
|
|
|
|
|
|
|
|
executeMiddlewareHandlers(app, function(err) {
|
|
|
|
if (err) return done(err);
|
|
|
|
expect(handledError).to.equal(expectedError);
|
|
|
|
done();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
Scope app middleware to a list of paths
Add a new argument to `app.middleware` allowing developers
to restrict the middleware to a list of paths or regular expresions.
Modify `app.middlewareFromConfig` to pass `config.paths` as the second
arg of `app.middleware`.
Examples:
// A string path (interpreted via path-to-regexp)
app.middleware('auth', '/admin', ldapAuth);
// A regular expression
app.middleware('initial', /^\/~(admin|root)/, rejectWith404);
// A list of scopes
app.middleware('routes', ['/api', /^\/assets/.*\.json$/], foo);
// From config
app.middlewareFromConfig(
handlerFactory,
{
phase: 'initial',
paths: ['/scope', /^\/(a|b)/]
});
2014-11-19 11:03:47 +00:00
|
|
|
it('scopes middleware to a string path', function(done) {
|
|
|
|
app.middleware('initial', '/scope', pathSavingHandler());
|
|
|
|
|
|
|
|
async.eachSeries(
|
|
|
|
['/', '/scope', '/scope/item', '/other'],
|
|
|
|
function(url, next) { executeMiddlewareHandlers(app, url, next); },
|
|
|
|
function(err) {
|
|
|
|
if (err) return done(err);
|
|
|
|
expect(steps).to.eql(['/scope', '/scope/item']);
|
|
|
|
done();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
it('scopes middleware to a regex path', function(done) {
|
|
|
|
app.middleware('initial', /^\/(a|b)/, pathSavingHandler());
|
|
|
|
|
|
|
|
async.eachSeries(
|
|
|
|
['/', '/a', '/b', '/c'],
|
|
|
|
function(url, next) { executeMiddlewareHandlers(app, url, next); },
|
|
|
|
function(err) {
|
|
|
|
if (err) return done(err);
|
|
|
|
expect(steps).to.eql(['/a', '/b']);
|
|
|
|
done();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
it('scopes middleware to a list of scopes', function(done) {
|
|
|
|
app.middleware('initial', ['/scope', /^\/(a|b)/], pathSavingHandler());
|
|
|
|
|
|
|
|
async.eachSeries(
|
|
|
|
['/', '/a', '/b', '/c', '/scope', '/other'],
|
|
|
|
function(url, next) { executeMiddlewareHandlers(app, url, next); },
|
|
|
|
function(err) {
|
|
|
|
if (err) return done(err);
|
|
|
|
expect(steps).to.eql(['/a', '/b', '/scope']);
|
|
|
|
done();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2014-12-10 08:36:45 +00:00
|
|
|
it('sets req.url to a sub-path', function(done) {
|
|
|
|
app.middleware('initial', ['/scope'], function(req, res, next) {
|
|
|
|
steps.push(req.url);
|
|
|
|
next();
|
|
|
|
});
|
|
|
|
|
|
|
|
executeMiddlewareHandlers(app, '/scope/id', function(err) {
|
|
|
|
if (err) return done(err);
|
|
|
|
expect(steps).to.eql(['/id']);
|
|
|
|
done();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
it('exposes express helpers on req and res objects', function(done) {
|
|
|
|
var req;
|
|
|
|
var res;
|
|
|
|
|
|
|
|
app.middleware('initial', function(rq, rs, next) {
|
|
|
|
req = rq;
|
|
|
|
res = rs;
|
|
|
|
next();
|
|
|
|
});
|
|
|
|
|
|
|
|
executeMiddlewareHandlers(app, function(err) {
|
|
|
|
if (err) return done(err);
|
|
|
|
expect(getObjectAndPrototypeKeys(req), 'request').to.include.members([
|
|
|
|
'accepts',
|
|
|
|
'get',
|
|
|
|
'param',
|
|
|
|
'params',
|
|
|
|
'query',
|
|
|
|
'res'
|
|
|
|
]);
|
|
|
|
|
|
|
|
expect(getObjectAndPrototypeKeys(res), 'response').to.include.members([
|
|
|
|
'cookie',
|
|
|
|
'download',
|
|
|
|
'json',
|
|
|
|
'jsonp',
|
|
|
|
'redirect',
|
|
|
|
'req',
|
|
|
|
'send',
|
|
|
|
'sendFile',
|
|
|
|
'set'
|
|
|
|
]);
|
|
|
|
|
|
|
|
done();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
it('sets req.baseUrl and req.originalUrl', function(done) {
|
|
|
|
var reqProps;
|
|
|
|
app.middleware('initial', function(req, res, next) {
|
|
|
|
reqProps = { baseUrl: req.baseUrl, originalUrl: req.originalUrl };
|
|
|
|
next();
|
|
|
|
});
|
|
|
|
|
|
|
|
executeMiddlewareHandlers(app, '/test/url', function(err) {
|
|
|
|
if (err) return done(err);
|
|
|
|
expect(reqProps).to.eql({ baseUrl: '', originalUrl: '/test/url' });
|
|
|
|
done();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
it('preserves correct order of routes vs. middleware', function(done) {
|
|
|
|
// This test verifies that `app.route` triggers sort of layers
|
|
|
|
app.middleware('files', namedHandler('files'));
|
|
|
|
app.get('/test', namedHandler('route'));
|
|
|
|
|
|
|
|
executeMiddlewareHandlers(app, '/test', function(err) {
|
|
|
|
if (err) return done(err);
|
|
|
|
expect(steps).to.eql(['route', 'files']);
|
|
|
|
done();
|
|
|
|
});
|
|
|
|
});
|
2014-12-15 07:14:26 +00:00
|
|
|
|
|
|
|
it('preserves order of middleware in the same phase', function(done) {
|
|
|
|
// while we are discouraging developers from depending on
|
|
|
|
// the registration order of middleware in the same phase,
|
|
|
|
// we must preserve the order for compatibility with `app.use`
|
|
|
|
// and `app.route`.
|
|
|
|
|
|
|
|
// we need at least 9 elements to expose non-stability
|
|
|
|
// of the built-in sort function
|
|
|
|
var numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9];
|
|
|
|
numbers.forEach(function(n) {
|
|
|
|
app.middleware('routes', namedHandler(n));
|
|
|
|
});
|
|
|
|
|
|
|
|
executeMiddlewareHandlers(app, function(err) {
|
|
|
|
if (err) return done;
|
|
|
|
expect(steps).to.eql(numbers);
|
|
|
|
done();
|
|
|
|
});
|
|
|
|
});
|
2014-12-10 08:36:45 +00:00
|
|
|
|
|
|
|
it('correctly mounts express apps', function(done) {
|
|
|
|
var data;
|
|
|
|
var mountWasEmitted;
|
|
|
|
var subapp = express();
|
|
|
|
subapp.use(function(req, res, next) {
|
|
|
|
data = {
|
|
|
|
mountpath: req.app.mountpath,
|
|
|
|
parent: req.app.parent
|
|
|
|
};
|
|
|
|
next();
|
|
|
|
});
|
|
|
|
subapp.on('mount', function() { mountWasEmitted = true; });
|
|
|
|
|
|
|
|
app.middleware('routes', '/mountpath', subapp);
|
|
|
|
|
|
|
|
executeMiddlewareHandlers(app, '/mountpath/test', function(err) {
|
|
|
|
if (err) return done(err);
|
2015-03-05 00:30:03 +00:00
|
|
|
expect(mountWasEmitted, 'mountWasEmitted').to.be.true;
|
2014-12-10 08:36:45 +00:00
|
|
|
expect(data).to.eql({
|
|
|
|
mountpath: '/mountpath',
|
|
|
|
parent: app
|
|
|
|
});
|
|
|
|
done();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
it('restores req & res on return from mounted express app', function(done) {
|
|
|
|
// jshint proto:true
|
|
|
|
var expected = {};
|
|
|
|
var actual = {};
|
|
|
|
|
|
|
|
var subapp = express();
|
|
|
|
subapp.use(function verifyTestAssumptions(req, res, next) {
|
|
|
|
expect(req.__proto__).to.not.equal(expected.req);
|
|
|
|
expect(res.__proto__).to.not.equal(expected.res);
|
|
|
|
next();
|
|
|
|
});
|
|
|
|
|
|
|
|
app.middleware('initial', function saveOriginalValues(req, res, next) {
|
|
|
|
expected.req = req.__proto__;
|
|
|
|
expected.res = res.__proto__;
|
|
|
|
next();
|
|
|
|
});
|
|
|
|
app.middleware('routes', subapp);
|
|
|
|
app.middleware('final', function saveActualValues(req, res, next) {
|
|
|
|
actual.req = req.__proto__;
|
|
|
|
actual.res = res.__proto__;
|
|
|
|
next();
|
|
|
|
});
|
|
|
|
|
|
|
|
executeMiddlewareHandlers(app, function(err) {
|
|
|
|
if (err) return done(err);
|
|
|
|
expect(actual.req, 'req').to.equal(expected.req);
|
|
|
|
expect(actual.res, 'res').to.equal(expected.res);
|
|
|
|
done();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
Middleware phases - initial implementation
Modify the app and router implementation, so that the middleware is
executed in order defined by phases.
Predefined phases:
'initial', 'session', 'auth', 'parse', 'routes', 'files', 'final'
Methods defined via `app.use`, `app.route` and friends are executed
as the first thing in 'routes' phase.
API usage:
app.middleware('initial', compression());
app.middleware('initial:before', serveFavicon());
app.middleware('files:after', loopback.urlNotFound());
app.middleware('final:after', errorHandler());
Middleware flavours:
// regular handler
function handler(req, res, next) {
// do stuff
next();
}
// error handler
function errorHandler(err, req, res, next) {
// handle error and/or call next
next(err);
}
2014-11-05 19:07:58 +00:00
|
|
|
function namedHandler(name) {
|
|
|
|
return function(req, res, next) {
|
|
|
|
steps.push(name);
|
|
|
|
next();
|
|
|
|
};
|
|
|
|
}
|
Scope app middleware to a list of paths
Add a new argument to `app.middleware` allowing developers
to restrict the middleware to a list of paths or regular expresions.
Modify `app.middlewareFromConfig` to pass `config.paths` as the second
arg of `app.middleware`.
Examples:
// A string path (interpreted via path-to-regexp)
app.middleware('auth', '/admin', ldapAuth);
// A regular expression
app.middleware('initial', /^\/~(admin|root)/, rejectWith404);
// A list of scopes
app.middleware('routes', ['/api', /^\/assets/.*\.json$/], foo);
// From config
app.middlewareFromConfig(
handlerFactory,
{
phase: 'initial',
paths: ['/scope', /^\/(a|b)/]
});
2014-11-19 11:03:47 +00:00
|
|
|
|
|
|
|
function pathSavingHandler() {
|
|
|
|
return function(req, res, next) {
|
2014-12-10 08:36:45 +00:00
|
|
|
steps.push(req.originalUrl);
|
Scope app middleware to a list of paths
Add a new argument to `app.middleware` allowing developers
to restrict the middleware to a list of paths or regular expresions.
Modify `app.middlewareFromConfig` to pass `config.paths` as the second
arg of `app.middleware`.
Examples:
// A string path (interpreted via path-to-regexp)
app.middleware('auth', '/admin', ldapAuth);
// A regular expression
app.middleware('initial', /^\/~(admin|root)/, rejectWith404);
// A list of scopes
app.middleware('routes', ['/api', /^\/assets/.*\.json$/], foo);
// From config
app.middlewareFromConfig(
handlerFactory,
{
phase: 'initial',
paths: ['/scope', /^\/(a|b)/]
});
2014-11-19 11:03:47 +00:00
|
|
|
next();
|
|
|
|
};
|
|
|
|
}
|
2014-12-10 08:36:45 +00:00
|
|
|
|
|
|
|
function getObjectAndPrototypeKeys(obj) {
|
|
|
|
var result = [];
|
|
|
|
for (var k in obj) {
|
|
|
|
result.push(k);
|
|
|
|
}
|
|
|
|
result.sort();
|
|
|
|
return result;
|
|
|
|
}
|
2014-11-11 16:51:50 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
describe.onServer('.middlewareFromConfig', function() {
|
|
|
|
it('provides API for loading middleware from JSON config', function(done) {
|
|
|
|
var steps = [];
|
|
|
|
var expectedConfig = { key: 'value' };
|
|
|
|
|
|
|
|
var handlerFactory = function() {
|
|
|
|
var args = Array.prototype.slice.apply(arguments);
|
|
|
|
return function(req, res, next) {
|
|
|
|
steps.push(args);
|
|
|
|
next();
|
|
|
|
};
|
|
|
|
};
|
Middleware phases - initial implementation
Modify the app and router implementation, so that the middleware is
executed in order defined by phases.
Predefined phases:
'initial', 'session', 'auth', 'parse', 'routes', 'files', 'final'
Methods defined via `app.use`, `app.route` and friends are executed
as the first thing in 'routes' phase.
API usage:
app.middleware('initial', compression());
app.middleware('initial:before', serveFavicon());
app.middleware('files:after', loopback.urlNotFound());
app.middleware('final:after', errorHandler());
Middleware flavours:
// regular handler
function handler(req, res, next) {
// do stuff
next();
}
// error handler
function errorHandler(err, req, res, next) {
// handle error and/or call next
next(err);
}
2014-11-05 19:07:58 +00:00
|
|
|
|
2014-11-11 16:51:50 +00:00
|
|
|
// Config as an object (single arg)
|
|
|
|
app.middlewareFromConfig(handlerFactory, {
|
|
|
|
enabled: true,
|
|
|
|
phase: 'session',
|
2014-11-13 12:22:25 +00:00
|
|
|
params: expectedConfig
|
Middleware phases - initial implementation
Modify the app and router implementation, so that the middleware is
executed in order defined by phases.
Predefined phases:
'initial', 'session', 'auth', 'parse', 'routes', 'files', 'final'
Methods defined via `app.use`, `app.route` and friends are executed
as the first thing in 'routes' phase.
API usage:
app.middleware('initial', compression());
app.middleware('initial:before', serveFavicon());
app.middleware('files:after', loopback.urlNotFound());
app.middleware('final:after', errorHandler());
Middleware flavours:
// regular handler
function handler(req, res, next) {
// do stuff
next();
}
// error handler
function errorHandler(err, req, res, next) {
// handle error and/or call next
next(err);
}
2014-11-05 19:07:58 +00:00
|
|
|
});
|
|
|
|
|
2014-11-11 16:51:50 +00:00
|
|
|
// Config as a value (single arg)
|
|
|
|
app.middlewareFromConfig(handlerFactory, {
|
|
|
|
enabled: true,
|
|
|
|
phase: 'session:before',
|
2014-11-13 12:22:25 +00:00
|
|
|
params: 'before'
|
2014-11-11 16:51:50 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
// Config as a list of args
|
|
|
|
app.middlewareFromConfig(handlerFactory, {
|
|
|
|
enabled: true,
|
|
|
|
phase: 'session:after',
|
2014-11-13 12:22:25 +00:00
|
|
|
params: ['after', 2]
|
2014-11-11 16:51:50 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
// Disabled by configuration
|
|
|
|
app.middlewareFromConfig(handlerFactory, {
|
|
|
|
enabled: false,
|
|
|
|
phase: 'initial',
|
2014-11-13 12:22:25 +00:00
|
|
|
params: null
|
2014-11-11 16:51:50 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
executeMiddlewareHandlers(app, function(err) {
|
|
|
|
if (err) return done(err);
|
|
|
|
expect(steps).to.eql([
|
|
|
|
['before'],
|
|
|
|
[expectedConfig],
|
|
|
|
['after', 2]
|
|
|
|
]);
|
|
|
|
done();
|
|
|
|
});
|
|
|
|
});
|
Scope app middleware to a list of paths
Add a new argument to `app.middleware` allowing developers
to restrict the middleware to a list of paths or regular expresions.
Modify `app.middlewareFromConfig` to pass `config.paths` as the second
arg of `app.middleware`.
Examples:
// A string path (interpreted via path-to-regexp)
app.middleware('auth', '/admin', ldapAuth);
// A regular expression
app.middleware('initial', /^\/~(admin|root)/, rejectWith404);
// A list of scopes
app.middleware('routes', ['/api', /^\/assets/.*\.json$/], foo);
// From config
app.middlewareFromConfig(
handlerFactory,
{
phase: 'initial',
paths: ['/scope', /^\/(a|b)/]
});
2014-11-19 11:03:47 +00:00
|
|
|
|
|
|
|
it('scopes middleware to a list of scopes', function(done) {
|
|
|
|
var steps = [];
|
|
|
|
app.middlewareFromConfig(
|
|
|
|
function factory() {
|
|
|
|
return function(req, res, next) {
|
2014-12-10 08:36:45 +00:00
|
|
|
steps.push(req.originalUrl);
|
Scope app middleware to a list of paths
Add a new argument to `app.middleware` allowing developers
to restrict the middleware to a list of paths or regular expresions.
Modify `app.middlewareFromConfig` to pass `config.paths` as the second
arg of `app.middleware`.
Examples:
// A string path (interpreted via path-to-regexp)
app.middleware('auth', '/admin', ldapAuth);
// A regular expression
app.middleware('initial', /^\/~(admin|root)/, rejectWith404);
// A list of scopes
app.middleware('routes', ['/api', /^\/assets/.*\.json$/], foo);
// From config
app.middlewareFromConfig(
handlerFactory,
{
phase: 'initial',
paths: ['/scope', /^\/(a|b)/]
});
2014-11-19 11:03:47 +00:00
|
|
|
next();
|
|
|
|
};
|
|
|
|
},
|
|
|
|
{
|
|
|
|
phase: 'initial',
|
|
|
|
paths: ['/scope', /^\/(a|b)/]
|
|
|
|
});
|
|
|
|
|
|
|
|
async.eachSeries(
|
|
|
|
['/', '/a', '/b', '/c', '/scope', '/other'],
|
|
|
|
function(url, next) { executeMiddlewareHandlers(app, url, next); },
|
|
|
|
function(err) {
|
|
|
|
if (err) return done(err);
|
|
|
|
expect(steps).to.eql(['/a', '/b', '/scope']);
|
|
|
|
done();
|
|
|
|
});
|
|
|
|
});
|
Middleware phases - initial implementation
Modify the app and router implementation, so that the middleware is
executed in order defined by phases.
Predefined phases:
'initial', 'session', 'auth', 'parse', 'routes', 'files', 'final'
Methods defined via `app.use`, `app.route` and friends are executed
as the first thing in 'routes' phase.
API usage:
app.middleware('initial', compression());
app.middleware('initial:before', serveFavicon());
app.middleware('files:after', loopback.urlNotFound());
app.middleware('final:after', errorHandler());
Middleware flavours:
// regular handler
function handler(req, res, next) {
// do stuff
next();
}
// error handler
function errorHandler(err, req, res, next) {
// handle error and/or call next
next(err);
}
2014-11-05 19:07:58 +00:00
|
|
|
});
|
2013-06-06 00:11:21 +00:00
|
|
|
|
Implement `app.defineMiddlewarePhases`
Implement method for registering (new) middleware phases.
- If all names are new, then the phases are added just before
the "routes" phase.
- Otherwise the provided list of names is merged with the existing
phases in such way that the order of phases is preserved.
Example
// built-in phases:
// initial, session, auth, parse, routes, files, final
app.defineMiddlewarePhases('custom');
// new list of phases
// initial, session, auth, parse,
// custom,
// routes, files, final
app.defineMiddlewarePhases([
'initial', 'postinit', 'preauth', 'routes', 'subapps'
]);
// new list of phases
// initial,
// postinit, preauth,
// session, auth, parse, custom,
// routes,
// subapps,
// files, final
2014-11-11 16:13:44 +00:00
|
|
|
describe.onServer('.defineMiddlewarePhases(nameOrArray)', function() {
|
|
|
|
var app;
|
|
|
|
beforeEach(function() {
|
|
|
|
app = loopback();
|
|
|
|
});
|
|
|
|
|
2014-11-21 01:46:21 +00:00
|
|
|
it('adds the phase just before `routes` by default', function(done) {
|
Implement `app.defineMiddlewarePhases`
Implement method for registering (new) middleware phases.
- If all names are new, then the phases are added just before
the "routes" phase.
- Otherwise the provided list of names is merged with the existing
phases in such way that the order of phases is preserved.
Example
// built-in phases:
// initial, session, auth, parse, routes, files, final
app.defineMiddlewarePhases('custom');
// new list of phases
// initial, session, auth, parse,
// custom,
// routes, files, final
app.defineMiddlewarePhases([
'initial', 'postinit', 'preauth', 'routes', 'subapps'
]);
// new list of phases
// initial,
// postinit, preauth,
// session, auth, parse, custom,
// routes,
// subapps,
// files, final
2014-11-11 16:13:44 +00:00
|
|
|
app.defineMiddlewarePhases('custom');
|
|
|
|
verifyMiddlewarePhases(['custom', 'routes'], done);
|
|
|
|
});
|
|
|
|
|
Simplify `app.defineMiddlewarePhases`
Refactor the implementation to use the new method `phaseList.zipMerge`.
This is commit is changing the behaviour in the case when
the first new phase does not exist in the current list.
Before the change, all new phases were added just before the "routes"
phase.
After this change, new phases are added to the head of the list,
until an existing phase is encountered, at which point the regular
merge algorithm kicks in.
Example:
app.defineMiddlewarePhases(['first', 'routes', 'subapps']);
Before the change: code throws an error - 'routes' already exists.
After the change: phases are merged with the following result:
'first', 'initial', ..., 'routes', 'subapps', ...
2014-11-12 07:59:56 +00:00
|
|
|
it('merges phases adding to the start of the list', function(done) {
|
|
|
|
app.defineMiddlewarePhases(['first', 'routes', 'subapps']);
|
|
|
|
verifyMiddlewarePhases([
|
|
|
|
'first',
|
|
|
|
'initial', // this was the original first phase
|
|
|
|
'routes',
|
|
|
|
'subapps'
|
|
|
|
], done);
|
Implement `app.defineMiddlewarePhases`
Implement method for registering (new) middleware phases.
- If all names are new, then the phases are added just before
the "routes" phase.
- Otherwise the provided list of names is merged with the existing
phases in such way that the order of phases is preserved.
Example
// built-in phases:
// initial, session, auth, parse, routes, files, final
app.defineMiddlewarePhases('custom');
// new list of phases
// initial, session, auth, parse,
// custom,
// routes, files, final
app.defineMiddlewarePhases([
'initial', 'postinit', 'preauth', 'routes', 'subapps'
]);
// new list of phases
// initial,
// postinit, preauth,
// session, auth, parse, custom,
// routes,
// subapps,
// files, final
2014-11-11 16:13:44 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
it('merges phases preserving the order', function(done) {
|
|
|
|
app.defineMiddlewarePhases([
|
|
|
|
'initial',
|
|
|
|
'postinit', 'preauth', // add
|
|
|
|
'auth', 'routes',
|
|
|
|
'subapps', // add
|
|
|
|
'final',
|
|
|
|
'last' // add
|
|
|
|
]);
|
|
|
|
verifyMiddlewarePhases([
|
|
|
|
'initial',
|
|
|
|
'postinit', 'preauth', // new
|
|
|
|
'auth', 'routes',
|
|
|
|
'subapps', // new
|
|
|
|
'files', 'final',
|
|
|
|
'last' // new
|
|
|
|
], done);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('throws helpful error on ordering conflict', function() {
|
|
|
|
app.defineMiddlewarePhases(['first', 'second']);
|
|
|
|
expect(function() { app.defineMiddlewarePhases(['second', 'first']); })
|
2014-12-10 08:36:45 +00:00
|
|
|
.to.throw(/Ordering conflict.*first.*second/);
|
Implement `app.defineMiddlewarePhases`
Implement method for registering (new) middleware phases.
- If all names are new, then the phases are added just before
the "routes" phase.
- Otherwise the provided list of names is merged with the existing
phases in such way that the order of phases is preserved.
Example
// built-in phases:
// initial, session, auth, parse, routes, files, final
app.defineMiddlewarePhases('custom');
// new list of phases
// initial, session, auth, parse,
// custom,
// routes, files, final
app.defineMiddlewarePhases([
'initial', 'postinit', 'preauth', 'routes', 'subapps'
]);
// new list of phases
// initial,
// postinit, preauth,
// session, auth, parse, custom,
// routes,
// subapps,
// files, final
2014-11-11 16:13:44 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
function verifyMiddlewarePhases(names, done) {
|
|
|
|
var steps = [];
|
|
|
|
names.forEach(function(it) {
|
|
|
|
app.middleware(it, function(req, res, next) {
|
|
|
|
steps.push(it);
|
|
|
|
next();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
executeMiddlewareHandlers(app, function(err) {
|
|
|
|
if (err) return done(err);
|
|
|
|
expect(steps).to.eql(names);
|
|
|
|
done();
|
|
|
|
});
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2013-06-07 19:57:51 +00:00
|
|
|
describe('app.model(Model)', function() {
|
2014-11-21 02:35:36 +00:00
|
|
|
var app;
|
|
|
|
var db;
|
2014-01-22 10:22:23 +00:00
|
|
|
beforeEach(function() {
|
|
|
|
app = loopback();
|
|
|
|
db = loopback.createDataSource({connector: loopback.Memory});
|
|
|
|
});
|
|
|
|
|
2014-11-21 01:46:21 +00:00
|
|
|
it('Expose a `Model` to remote clients', function() {
|
2014-06-05 07:45:09 +00:00
|
|
|
var Color = PersistedModel.extend('color', {name: String});
|
2013-06-07 19:57:51 +00:00
|
|
|
app.model(Color);
|
2014-05-03 04:19:14 +00:00
|
|
|
Color.attachTo(db);
|
2014-01-22 10:22:23 +00:00
|
|
|
|
|
|
|
expect(app.models()).to.eql([Color]);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('uses singlar name as app.remoteObjects() key', function() {
|
2014-06-05 07:45:09 +00:00
|
|
|
var Color = PersistedModel.extend('color', {name: String});
|
2014-01-22 10:22:23 +00:00
|
|
|
app.model(Color);
|
2014-05-03 04:19:14 +00:00
|
|
|
Color.attachTo(db);
|
2014-01-22 10:22:23 +00:00
|
|
|
expect(app.remoteObjects()).to.eql({ color: Color });
|
|
|
|
});
|
|
|
|
|
|
|
|
it('uses singular name as shared class name', function() {
|
2014-06-05 07:45:09 +00:00
|
|
|
var Color = PersistedModel.extend('color', {name: String});
|
2014-01-22 10:22:23 +00:00
|
|
|
app.model(Color);
|
2014-05-03 04:19:14 +00:00
|
|
|
Color.attachTo(db);
|
2014-11-21 01:46:21 +00:00
|
|
|
var classes = app.remotes().classes().map(function(c) {return c.name;});
|
2014-05-19 22:56:26 +00:00
|
|
|
expect(classes).to.contain('color');
|
2014-01-22 10:22:23 +00:00
|
|
|
});
|
|
|
|
|
2014-06-09 23:31:33 +00:00
|
|
|
it('registers existing models to app.models', function() {
|
|
|
|
var Color = db.createModel('color', {name: String});
|
|
|
|
app.model(Color);
|
2014-06-10 06:53:01 +00:00
|
|
|
expect(Color.app).to.be.equal(app);
|
|
|
|
expect(Color.shared).to.equal(true);
|
|
|
|
expect(app.models.color).to.equal(Color);
|
|
|
|
expect(app.models.Color).to.equal(Color);
|
2014-06-09 23:31:33 +00:00
|
|
|
});
|
|
|
|
|
2014-11-21 01:46:21 +00:00
|
|
|
it('emits a `modelRemoted` event', function() {
|
2014-07-25 00:00:27 +00:00
|
|
|
var Color = PersistedModel.extend('color', {name: String});
|
|
|
|
Color.shared = true;
|
|
|
|
var remotedClass;
|
|
|
|
app.on('modelRemoted', function(sharedClass) {
|
|
|
|
remotedClass = sharedClass;
|
|
|
|
});
|
|
|
|
app.model(Color);
|
|
|
|
expect(remotedClass).to.exist;
|
|
|
|
expect(remotedClass).to.eql(Color.sharedClass);
|
|
|
|
});
|
|
|
|
|
2014-06-03 08:39:54 +00:00
|
|
|
it.onServer('updates REST API when a new model is added', function(done) {
|
2014-02-19 19:40:05 +00:00
|
|
|
app.use(loopback.rest());
|
|
|
|
request(app).get('/colors').expect(404, function(err, res) {
|
|
|
|
if (err) return done(err);
|
2014-06-05 07:45:09 +00:00
|
|
|
var Color = PersistedModel.extend('color', {name: String});
|
2014-02-19 19:40:05 +00:00
|
|
|
app.model(Color);
|
2014-05-03 04:19:14 +00:00
|
|
|
Color.attachTo(db);
|
2014-02-19 19:40:05 +00:00
|
|
|
request(app).get('/colors').expect(200, done);
|
|
|
|
});
|
2014-02-18 20:40:35 +00:00
|
|
|
});
|
2014-08-20 23:05:45 +00:00
|
|
|
|
2014-08-26 15:58:59 +00:00
|
|
|
it('accepts null dataSource', function() {
|
|
|
|
app.model('MyTestModel', { dataSource: null });
|
|
|
|
});
|
|
|
|
|
2014-08-20 23:05:45 +00:00
|
|
|
it('should not require dataSource', function() {
|
|
|
|
app.model('MyTestModel', {});
|
|
|
|
});
|
2014-08-26 15:58:59 +00:00
|
|
|
|
2013-06-06 00:11:21 +00:00
|
|
|
});
|
2013-06-07 19:57:51 +00:00
|
|
|
|
2014-11-21 02:35:36 +00:00
|
|
|
describe('app.model(name, config)', function() {
|
2014-05-25 14:27:45 +00:00
|
|
|
var app;
|
|
|
|
|
|
|
|
beforeEach(function() {
|
|
|
|
app = loopback();
|
2014-06-25 11:52:20 +00:00
|
|
|
app.dataSource('db', {
|
|
|
|
connector: 'memory'
|
2013-10-29 21:12:23 +00:00
|
|
|
});
|
2014-05-25 14:27:45 +00:00
|
|
|
});
|
2013-10-29 21:12:23 +00:00
|
|
|
|
2014-11-21 02:35:36 +00:00
|
|
|
it('Sugar for defining a fully built model', function() {
|
2013-10-29 21:12:23 +00:00
|
|
|
app.model('foo', {
|
|
|
|
dataSource: 'db'
|
|
|
|
});
|
|
|
|
|
|
|
|
var Foo = app.models.foo;
|
2014-05-25 14:27:45 +00:00
|
|
|
var f = new Foo();
|
2013-10-29 21:12:23 +00:00
|
|
|
|
|
|
|
assert(f instanceof loopback.Model);
|
|
|
|
});
|
2014-05-25 14:27:45 +00:00
|
|
|
|
|
|
|
it('interprets extra first-level keys as options', function() {
|
|
|
|
app.model('foo', {
|
|
|
|
dataSource: 'db',
|
|
|
|
base: 'User'
|
|
|
|
});
|
|
|
|
|
|
|
|
expect(app.models.foo.definition.settings.base).to.equal('User');
|
|
|
|
});
|
|
|
|
|
|
|
|
it('prefers config.options.key over config.key', function() {
|
|
|
|
app.model('foo', {
|
|
|
|
dataSource: 'db',
|
|
|
|
base: 'User',
|
|
|
|
options: {
|
|
|
|
base: 'Application'
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
expect(app.models.foo.definition.settings.base).to.equal('Application');
|
|
|
|
});
|
2014-06-10 06:53:01 +00:00
|
|
|
|
|
|
|
it('honors config.public options', function() {
|
|
|
|
app.model('foo', {
|
|
|
|
dataSource: 'db',
|
|
|
|
public: false
|
|
|
|
});
|
|
|
|
expect(app.models.foo.app).to.equal(app);
|
|
|
|
expect(app.models.foo.shared).to.equal(false);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('defaults config.public to be true', function() {
|
|
|
|
app.model('foo', {
|
|
|
|
dataSource: 'db'
|
|
|
|
});
|
|
|
|
expect(app.models.foo.app).to.equal(app);
|
|
|
|
expect(app.models.foo.shared).to.equal(true);
|
|
|
|
});
|
|
|
|
|
2013-12-12 03:31:16 +00:00
|
|
|
});
|
2013-10-29 21:12:23 +00:00
|
|
|
|
Add createModelFromConfig and configureModel()
Add new API allowing developers to split the model definition and
configuration into two steps:
1. Build models from JSON config, export them for re-use:
```js
var Customer = loopback.createModelFromConfig({
name: 'Customer',
base: 'User',
properties: {
address: 'string'
}
});
```
2. Attach existing models to a dataSource and a loopback app,
modify certain model aspects like relations:
```js
loopback.configureModel(Customer, {
dataSource: db,
relations: { /* ... */ }
});
```
Rework `app.model` to use `loopback.configureModel` under the hood.
Here is the new usage:
```js
var Customer = require('./models').Customer;
app.model(Customer, {
dataSource: 'db',
relations: { /* ... */ }
});
```
In order to preserve backwards compatibility with loopback 1.x,
`app.model(name, config)` calls both `createModelFromConfig`
and `configureModel`.
2014-06-05 15:41:12 +00:00
|
|
|
describe('app.model(ModelCtor, config)', function() {
|
|
|
|
it('attaches the model to a datasource', function() {
|
|
|
|
app.dataSource('db', { connector: 'memory' });
|
|
|
|
var TestModel = loopback.Model.extend('TestModel');
|
|
|
|
// TestModel was most likely already defined in a different test,
|
|
|
|
// thus TestModel.dataSource may be already set
|
|
|
|
delete TestModel.dataSource;
|
|
|
|
|
|
|
|
app.model(TestModel, { dataSource: 'db' });
|
|
|
|
|
|
|
|
expect(app.models.TestModel.dataSource).to.equal(app.dataSources.db);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2014-02-04 19:28:19 +00:00
|
|
|
describe('app.models', function() {
|
|
|
|
it('is unique per app instance', function() {
|
2014-05-27 12:33:42 +00:00
|
|
|
app.dataSource('db', { connector: 'memory' });
|
2014-02-04 19:28:19 +00:00
|
|
|
var Color = app.model('Color', { dataSource: 'db' });
|
|
|
|
expect(app.models.Color).to.equal(Color);
|
|
|
|
var anotherApp = loopback();
|
|
|
|
expect(anotherApp.models.Color).to.equal(undefined);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2014-05-27 12:33:42 +00:00
|
|
|
describe('app.dataSources', function() {
|
|
|
|
it('is unique per app instance', function() {
|
|
|
|
app.dataSource('ds', { connector: 'memory' });
|
|
|
|
expect(app.datasources.ds).to.not.equal(undefined);
|
|
|
|
var anotherApp = loopback();
|
|
|
|
expect(anotherApp.datasources.ds).to.equal(undefined);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2014-05-28 13:02:55 +00:00
|
|
|
describe('app.dataSource', function() {
|
|
|
|
it('looks up the connector in `app.connectors`', function() {
|
|
|
|
app.connector('custom', loopback.Memory);
|
|
|
|
app.dataSource('custom', { connector: 'custom' });
|
|
|
|
expect(app.dataSources.custom.name).to.equal(loopback.Memory.name);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2014-06-03 08:39:54 +00:00
|
|
|
describe.onServer('listen()', function() {
|
2014-01-08 14:20:17 +00:00
|
|
|
it('starts http server', function(done) {
|
|
|
|
var app = loopback();
|
|
|
|
app.set('port', 0);
|
2014-07-26 21:37:13 +00:00
|
|
|
app.get('/', function(req, res) { res.status(200).send('OK'); });
|
2014-01-08 14:20:17 +00:00
|
|
|
|
|
|
|
var server = app.listen();
|
|
|
|
|
|
|
|
expect(server).to.be.an.instanceOf(require('http').Server);
|
|
|
|
|
|
|
|
request(server)
|
|
|
|
.get('/')
|
|
|
|
.expect(200, done);
|
|
|
|
});
|
|
|
|
|
2014-11-21 01:46:21 +00:00
|
|
|
it('updates port on `listening` event', function(done) {
|
2014-01-08 14:20:17 +00:00
|
|
|
var app = loopback();
|
|
|
|
app.set('port', 0);
|
|
|
|
|
|
|
|
app.listen(function() {
|
|
|
|
expect(app.get('port'), 'port').to.not.equal(0);
|
|
|
|
done();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2014-11-21 01:46:21 +00:00
|
|
|
it('updates `url` on `listening` event', function(done) {
|
2014-07-01 12:27:02 +00:00
|
|
|
var app = loopback();
|
|
|
|
app.set('port', 0);
|
|
|
|
app.set('host', undefined);
|
|
|
|
|
|
|
|
app.listen(function() {
|
2014-10-17 07:58:02 +00:00
|
|
|
var host = process.platform === 'win32' ? 'localhost' : app.get('host');
|
|
|
|
var expectedUrl = 'http://' + host + ':' + app.get('port') + '/';
|
|
|
|
expect(app.get('url'), 'url').to.equal(expectedUrl);
|
2014-07-01 12:27:02 +00:00
|
|
|
done();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2014-01-08 14:20:17 +00:00
|
|
|
it('forwards to http.Server.listen on more than one arg', function(done) {
|
|
|
|
var app = loopback();
|
|
|
|
app.set('port', 1);
|
|
|
|
app.listen(0, '127.0.0.1', function() {
|
|
|
|
expect(app.get('port'), 'port').to.not.equal(0).and.not.equal(1);
|
|
|
|
expect(this.address().address).to.equal('127.0.0.1');
|
|
|
|
done();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
it('forwards to http.Server.listen when the single arg is not a function',
|
|
|
|
function(done) {
|
|
|
|
var app = loopback();
|
|
|
|
app.set('port', 1);
|
|
|
|
app.listen(0).on('listening', function() {
|
|
|
|
expect(app.get('port'), 'port') .to.not.equal(0).and.not.equal(1);
|
|
|
|
done();
|
|
|
|
});
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
|
|
|
it('uses app config when no parameter is supplied', function(done) {
|
|
|
|
var app = loopback();
|
|
|
|
// Http listens on all interfaces by default
|
|
|
|
// Custom host serves as an indicator whether
|
|
|
|
// the value was used by app.listen
|
|
|
|
app.set('host', '127.0.0.1');
|
|
|
|
app.listen()
|
|
|
|
.on('listening', function() {
|
|
|
|
expect(this.address().address).to.equal('127.0.0.1');
|
|
|
|
done();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2014-06-03 08:39:54 +00:00
|
|
|
describe.onServer('enableAuth', function() {
|
2014-02-05 17:46:22 +00:00
|
|
|
it('should set app.isAuthEnabled to true', function() {
|
|
|
|
expect(app.isAuthEnabled).to.not.equal(true);
|
|
|
|
app.enableAuth();
|
|
|
|
expect(app.isAuthEnabled).to.equal(true);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2014-11-21 02:35:36 +00:00
|
|
|
describe.onServer('app.get(\'/\', loopback.status())', function() {
|
|
|
|
it('should return the status of the application', function(done) {
|
2013-11-19 20:35:29 +00:00
|
|
|
var app = loopback();
|
|
|
|
app.get('/', loopback.status());
|
|
|
|
request(app)
|
|
|
|
.get('/')
|
|
|
|
.expect(200)
|
|
|
|
.end(function(err, res) {
|
2014-11-21 02:35:36 +00:00
|
|
|
if (err) return done(err);
|
2013-11-19 20:35:29 +00:00
|
|
|
|
|
|
|
assert.equal(typeof res.body, 'object');
|
|
|
|
assert(res.body.started);
|
2014-02-19 23:14:31 +00:00
|
|
|
// The number can be 0
|
|
|
|
assert(res.body.uptime !== undefined);
|
2013-11-19 20:35:29 +00:00
|
|
|
|
|
|
|
var elapsed = Date.now() - Number(new Date(res.body.started));
|
|
|
|
|
|
|
|
// elapsed should be a positive number...
|
2014-10-27 22:22:17 +00:00
|
|
|
assert(elapsed >= 0);
|
2013-11-19 20:35:29 +00:00
|
|
|
|
|
|
|
// less than 100 milliseconds
|
|
|
|
assert(elapsed < 100);
|
|
|
|
|
|
|
|
done();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
2014-05-28 13:02:55 +00:00
|
|
|
|
|
|
|
describe('app.connectors', function() {
|
|
|
|
it('is unique per app instance', function() {
|
|
|
|
app.connectors.foo = 'bar';
|
|
|
|
var anotherApp = loopback();
|
|
|
|
expect(anotherApp.connectors.foo).to.equal(undefined);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('includes Remote connector', function() {
|
|
|
|
expect(app.connectors.remote).to.equal(loopback.Remote);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('includes Memory connector', function() {
|
|
|
|
expect(app.connectors.memory).to.equal(loopback.Memory);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('app.connector', function() {
|
2015-03-27 15:59:11 +00:00
|
|
|
// any connector will do
|
2014-05-28 13:02:55 +00:00
|
|
|
it('adds the connector to the registry', function() {
|
|
|
|
app.connector('foo-bar', loopback.Memory);
|
|
|
|
expect(app.connectors['foo-bar']).to.equal(loopback.Memory);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('adds a classified alias', function() {
|
|
|
|
app.connector('foo-bar', loopback.Memory);
|
|
|
|
expect(app.connectors.FooBar).to.equal(loopback.Memory);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('adds a camelized alias', function() {
|
|
|
|
app.connector('FOO-BAR', loopback.Memory);
|
|
|
|
expect(app.connectors.FOOBAR).to.equal(loopback.Memory);
|
|
|
|
});
|
|
|
|
});
|
2014-06-03 08:39:54 +00:00
|
|
|
|
|
|
|
describe('app.settings', function() {
|
|
|
|
it('can be altered via `app.set(key, value)`', function() {
|
|
|
|
app.set('write-key', 'write-value');
|
|
|
|
expect(app.settings).to.have.property('write-key', 'write-value');
|
|
|
|
});
|
|
|
|
|
|
|
|
it('can be read via `app.get(key)`', function() {
|
|
|
|
app.settings['read-key'] = 'read-value';
|
|
|
|
expect(app.get('read-key')).to.equal('read-value');
|
|
|
|
});
|
|
|
|
|
|
|
|
it('is unique per app instance', function() {
|
|
|
|
var app1 = loopback();
|
|
|
|
var app2 = loopback();
|
|
|
|
|
|
|
|
expect(app1.settings).to.not.equal(app2.settings);
|
|
|
|
|
|
|
|
app1.set('key', 'value');
|
|
|
|
expect(app2.get('key'), 'app2 value').to.equal(undefined);
|
|
|
|
});
|
|
|
|
});
|
2014-06-14 07:40:57 +00:00
|
|
|
|
2014-06-13 08:27:23 +00:00
|
|
|
it('exposes loopback as a property', function() {
|
|
|
|
var app = loopback();
|
|
|
|
expect(app.loopback).to.equal(loopback);
|
|
|
|
});
|
2014-10-11 14:36:29 +00:00
|
|
|
|
|
|
|
describe('normalizeHttpPath option', function() {
|
2014-11-21 02:35:36 +00:00
|
|
|
var app;
|
|
|
|
var db;
|
2014-10-11 14:36:29 +00:00
|
|
|
beforeEach(function() {
|
|
|
|
app = loopback();
|
|
|
|
db = loopback.createDataSource({ connector: loopback.Memory });
|
|
|
|
});
|
|
|
|
|
|
|
|
it.onServer('normalizes the http path', function(done) {
|
|
|
|
var UserAccount = PersistedModel.extend(
|
|
|
|
'UserAccount',
|
|
|
|
{ name: String },
|
|
|
|
{
|
|
|
|
remoting: { normalizeHttpPath: true }
|
|
|
|
});
|
|
|
|
app.model(UserAccount);
|
|
|
|
UserAccount.attachTo(db);
|
|
|
|
|
|
|
|
app.use(loopback.rest());
|
|
|
|
request(app).get('/user-accounts').expect(200, done);
|
|
|
|
});
|
|
|
|
});
|
2013-10-29 21:12:23 +00:00
|
|
|
});
|
2014-11-11 16:51:50 +00:00
|
|
|
|
Scope app middleware to a list of paths
Add a new argument to `app.middleware` allowing developers
to restrict the middleware to a list of paths or regular expresions.
Modify `app.middlewareFromConfig` to pass `config.paths` as the second
arg of `app.middleware`.
Examples:
// A string path (interpreted via path-to-regexp)
app.middleware('auth', '/admin', ldapAuth);
// A regular expression
app.middleware('initial', /^\/~(admin|root)/, rejectWith404);
// A list of scopes
app.middleware('routes', ['/api', /^\/assets/.*\.json$/], foo);
// From config
app.middlewareFromConfig(
handlerFactory,
{
phase: 'initial',
paths: ['/scope', /^\/(a|b)/]
});
2014-11-19 11:03:47 +00:00
|
|
|
function executeMiddlewareHandlers(app, urlPath, callback) {
|
2014-11-11 16:51:50 +00:00
|
|
|
var server = http.createServer(function(req, res) {
|
|
|
|
app.handle(req, res, callback);
|
|
|
|
});
|
|
|
|
|
Scope app middleware to a list of paths
Add a new argument to `app.middleware` allowing developers
to restrict the middleware to a list of paths or regular expresions.
Modify `app.middlewareFromConfig` to pass `config.paths` as the second
arg of `app.middleware`.
Examples:
// A string path (interpreted via path-to-regexp)
app.middleware('auth', '/admin', ldapAuth);
// A regular expression
app.middleware('initial', /^\/~(admin|root)/, rejectWith404);
// A list of scopes
app.middleware('routes', ['/api', /^\/assets/.*\.json$/], foo);
// From config
app.middlewareFromConfig(
handlerFactory,
{
phase: 'initial',
paths: ['/scope', /^\/(a|b)/]
});
2014-11-19 11:03:47 +00:00
|
|
|
if (callback === undefined && typeof urlPath === 'function') {
|
|
|
|
callback = urlPath;
|
|
|
|
urlPath = '/test/url';
|
|
|
|
}
|
|
|
|
|
2014-11-11 16:51:50 +00:00
|
|
|
request(server)
|
Scope app middleware to a list of paths
Add a new argument to `app.middleware` allowing developers
to restrict the middleware to a list of paths or regular expresions.
Modify `app.middlewareFromConfig` to pass `config.paths` as the second
arg of `app.middleware`.
Examples:
// A string path (interpreted via path-to-regexp)
app.middleware('auth', '/admin', ldapAuth);
// A regular expression
app.middleware('initial', /^\/~(admin|root)/, rejectWith404);
// A list of scopes
app.middleware('routes', ['/api', /^\/assets/.*\.json$/], foo);
// From config
app.middlewareFromConfig(
handlerFactory,
{
phase: 'initial',
paths: ['/scope', /^\/(a|b)/]
});
2014-11-19 11:03:47 +00:00
|
|
|
.get(urlPath)
|
2014-11-11 16:51:50 +00:00
|
|
|
.end(function(err) {
|
|
|
|
if (err) return callback(err);
|
|
|
|
});
|
|
|
|
}
|