initial commit

This commit is contained in:
Camel Aissani 2016-06-26 12:10:37 +02:00
parent 176b251ae8
commit 6e1f4fc779
14 changed files with 1749 additions and 0 deletions

3
.babelrc Executable file
View File

@ -0,0 +1,3 @@
{
"presets": ["es2015"]
}

3
.eslintrc Normal file
View File

@ -0,0 +1,3 @@
{
"parser": "babel-eslint"
}

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
node_modules
coverage

9
.travis.yml Normal file
View File

@ -0,0 +1,9 @@
language: node_js
node_js:
- "4.2"
before_script:
- npm install coveralls
after_script:
- cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js

160
lib/application.js Executable file
View File

@ -0,0 +1,160 @@
import Router, {Route} from './router';
import Middleware from './middleware';
import Requester, {HTTP_METHODS} from './requester';
export default class Application {
constructor() {
this.routers = [];
this.requester = new Requester();
this.lastVisited = null;
this.settingsDef = {
'http-requester': (requester) => {this.requester = requester}
};
}
////////////////////////////////////////////////////////
// Setting
set(name, value) {
const settingFn = this.settingsDef[name];
if (!settingFn) {
throw new ReferenceError(`unsupported setting ${name}`);
}
settingFn(value);
}
////////////////////////////////////////////////////////
// Routes
route(uri) {
const router = new Router(uri);
this.routers.push(router);
return router;
}
use(...args) {
if (args.length === 0) {
throw new TypeError(`use method takes at least a middleware or a router`);
}
let baseUri, middleware, router, which;
if (args.length === 1) {
[which,] = args;
} else {
[baseUri, which,] = args;
}
if (!(which instanceof Middleware) && (typeof which !== 'function') && !(which instanceof Router)) {
throw new TypeError(`use method takes at least a middleware or a router`);
}
if (which instanceof Router) {
router = which;
router.baseUri = baseUri;
} else {
middleware = which;
router = new Router(baseUri);
for (const method of Object.keys(HTTP_METHODS)) {
router[method.toLowerCase()](middleware);
}
}
this.routers.push(router);
}
///////////////////////////////////////////////////////
// Ajax request
_fetch({method, uri, headers, data}, resolve, reject) {
if (this.lastVisited) {
for (const router of this.routers) {
const routes = router.getRoutes(this.lastVisited.uri, this.lastVisited.method);
for (const route of routes) {
if (route.middleware.exited) {
route.middleware.exited(this.lastVisited);
}
}
}
}
const currentRoutes = [];
for (const router of this.routers) {
const routes = router.getRoutes(uri, method);
currentRoutes.push(...routes);
}
for (const route of currentRoutes) {
if (route.middleware.entered) {
route.middleware.entered({method, uri, headers, data});
}
}
this.requester.fetch({uri, method}, (request, response) => {
this.lastVisited = {method, uri, headers, data};
for (const route of currentRoutes) {
if (route.middleware.updated) {
route.middleware.updated(request, response);
} else {
route.middleware(request, response);
}
}
if (resolve) {
resolve(request, response);
}
}, (request, response) => {
for (const route of currentRoutes) {
if (route.middleware.failed) {
route.middleware.failed(request, response);
}
}
if (reject) {
reject(request, response);
}
});
}
}
Object.keys(HTTP_METHODS).reduce((reqProto, method) => {
// Middleware methods
const middlewareMethodeName = method.toLowerCase();
reqProto[middlewareMethodeName] = function(...args) {
if (args.length === 0) {
throw new TypeError(`${middlewareMethodeName} method takes at least a middleware`);
}
let baseUri, middleware, which;
if (args.length === 1) {
[which,] = args;
} else {
[baseUri, which,] = args;
}
if (!(which instanceof Middleware) && (typeof which !== 'function')) {
throw new TypeError(`${middlewareMethodeName} method takes at least a middleware`);
}
const router = new Router();
middleware = which;
router[middlewareMethodeName](baseUri, middleware);
this.routers.push(router);
}
// HTTP methods
const transformer = HTTP_METHODS[method];
const httpMethodName = 'http'+method.charAt(0).toUpperCase() + method.slice(1).toLowerCase();
reqProto[httpMethodName] = function(request, resolve, reject) {
let {uri, headers, data} = request;
if (!uri) {
uri = request;
}
return this._fetch({
uri,
method,
headers,
data,
}, resolve, reject);
}
return reqProto;
}, Application.prototype);

12
lib/frontexpress.js Executable file
View File

@ -0,0 +1,12 @@
import Application from './application';
import Router from './router';
import Middleware from './middleware';
function frontexpress() {
return new Application();
}
frontexpress.Router = Router;
frontexpress.Middleware = Middleware;
export default frontexpress;

19
lib/middleware.js Executable file
View File

@ -0,0 +1,19 @@
import Application from './application';
export default class Middleware {
constructor(name='') {
this.name = name;
}
entered(request) {
}
exited(request) {
}
updated(request, response) {
}
failed(request, response) {
}
}

132
lib/requester.js Executable file
View File

@ -0,0 +1,132 @@
export const HTTP_METHODS = {
'GET': {
uri({uri, headers, data}) {
if (!data) {
return uri;
}
return Object.keys(data).reduce((gUri, d, index) => {
if (index === 0) {
gUri += '?';
} else {
gUri += '&';
}
gUri += `${d}=${data[d]}`;
return gUri;
}, uri);
},
data({uri, headers, data}) {
return undefined;
}
},
'POST': {
headers({uri, headers, data}) {
const postHeaders = {};
postHeaders['Content-type'] = 'application/x-www-form-urlencoded';
if (headers) {
Object.keys(headers).reduce((phds, headKey) => {
phds[headKey] = headers[headKey];
return phds;
}, postHeaders);
}
return postHeaders;
},
data({uri, headers, data}) {
if (!data) {
return data;
}
return Object.keys(data).reduce((newData, d, index) => {
if (index !== 0) {
newData += '&';
}
newData += `${d}=${data[d]}`;
return newData;
}, '');
}
},
'PUT': {
//TODO
},
'DELETE': {
//TODO
}
// non exhaustive list
};
export default class Requester {
fetch({uri, method, headers, data}, resolve, reject) {
const transformer = HTTP_METHODS[method];
uri = transformer.uri ? transformer.uri({uri, headers, data}) : uri;
headers = transformer.headers ? transformer.headers({uri, headers, data}) : headers;
data = transformer.data ? transformer.data({uri, headers, data}) : data;
const success = (responseText) => {
resolve(
{uri, method, headers, data},
{status: 200, statusText: 'OK', responseText}
);
};
const fail = ({status, statusText, errorThrown}) => {
const errors = this._analyzeErrors({status, statusText, errorThrown});
reject(
{uri, method, headers, data},
{status, statusText, errorThrown, errors}
);
};
const xmlhttp = new XMLHttpRequest();
xmlhttp.onreadystatechange = () => {
if (xmlhttp.readyState === 4) {
if (xmlhttp.status === 200) {
success(xmlhttp.responseText);
} else {
fail({status: xmlhttp.status, statusText: xmlhttp.statusText});
}
}
};
try {
xmlhttp.open(method, uri, true);
if (headers) {
for (const header of Object.keys(headers)) {
xmlhttp.setRequestHeader(header, headers[header]);
}
}
if (data) {
xmlhttp.send(data);
} else {
xmlhttp.send();
}
} catch (errorThrown) {
fail({errorThrown});
}
}
_analyzeErrors(response) {
let error = '';
if (response.status === 0) {
error = 'Server access problem. Check your network connection';
} else if (response.status === 401) {
error = 'Your session has expired, Please reconnect. [code: 401]';
} else if (response.status === 404) {
error = 'Page not found on server. [code: 404]';
} else if (response.status === 500) {
error = 'Internal server error. [code: 500]';
} else if (response.errorThrown) {
if (response.errorThrown.name === 'SyntaxError') {
error = 'Problem during data decoding [JSON]';
} else if (response.errorThrown.name === 'TimeoutError') {
error = 'Server is taking too long to reply';
} else if (response.errorThrown.name === 'AbortError') {
error = 'Request cancelled on server';
} else if (response.errorThrown.name === 'NetworkError') {
error = 'A network error occurred';
} else {
error = `${response.errorThrown.name} ${response.errorThrown.message}`;
}
} else {
error = `Unknown error. ${response.statusText?response.statusText:''}`;
}
return error;
}
}

108
lib/router.js Executable file
View File

@ -0,0 +1,108 @@
import {HTTP_METHODS} from './requester';
import Middleware from './middleware';
class Route {
constructor(router, uriPart, method, middleware) {
this.router = router;
this.uriPart = uriPart;
this.method = method;
this.middleware = middleware;
}
get uri() {
if (this.uriPart instanceof RegExp) {
return this.uriPart;
}
if (this.router.baseUri instanceof RegExp) {
return this.router.baseUri;
}
if (this.router.baseUri && this.uriPart) {
return (this.router.baseUri.trim() + this.uriPart.trim()).replace(/\/{2,}/, '/');
}
if (this.router.baseUri) {
return this.router.baseUri.trim();
}
return this.uriPart;
}
}
export default class Router {
constructor(baseUri) {
if (baseUri) {
this.baseUri = baseUri;
}
this.routes = [];
}
_add(route) {
this.routes.push(route);
return this;
}
getRoutes(uri, method) {
return this.routes.filter((route) => {
if (route.method !== method) {
return false;
}
if (route.uri instanceof RegExp) {
return uri.match(route.uri);
}
if (!route.uri) {
return true;
}
return route.uri === uri;
});
}
all(...args) {
if (args.length === 0) {
throw new TypeError(`use all method takes at least a middleware`);
}
let middleware;
if (args.length === 1) {
[middleware,] = args;
} else {
[, middleware,] = args;
}
if (!(middleware instanceof Middleware) && (typeof middleware !== 'function') ) {
throw new TypeError(`use all method takes at least a middleware`);
}
for (const method of Object.keys(HTTP_METHODS)) {
this[method.toLowerCase()](...args);
}
return this;
}
}
for (const method of Object.keys(HTTP_METHODS)) {
const methodName = method.toLowerCase();
Router.prototype[methodName] = function(...args) {
if (args.length === 0) {
throw new TypeError(`use ${methodName} method takes at least a middleware`);
}
let uri, middleware;
if (args.length === 1) {
[middleware,] = args;
} else {
[uri, middleware,] = args;
}
if (!(middleware instanceof Middleware) && (typeof middleware !== 'function') ) {
throw new TypeError(`use ${methodName} method takes at least a middleware`);
}
this._add(new Route(this, uri, method, middleware));
return this;
}
}

49
package.json Executable file
View File

@ -0,0 +1,49 @@
{
"name": "frontexpress",
"version": "1.0.0",
"description": "Minimalist front end router framework a la express",
"main": "dist/frontexpress.js",
"scripts": {
"build": "webpack",
"dev": "webpack-dev-server index.js --watch",
"test": "mocha --compilers js:babel-core/register",
"cover": "babel-node node_modules/.bin/babel-istanbul cover node_modules/.bin/_mocha"
},
"author": "Camel Aissani <camel.aissani@gmail.com> (https://nuageprive.fr)",
"license": "MIT",
"repository": "camelaissani/frontexpress",
"keywords": [
"front",
"framework",
"web",
"router",
"middleware",
"rest",
"restful",
"app",
"api",
"express",
"frontexpress"
],
"devDependencies": {
"babel-cli": "^6.9.0",
"babel-core": "^6.9.0",
"babel-eslint": "^6.0.4",
"babel-istanbul": "^0.8.0",
"babel-loader": "^6.2.4",
"babel-preset-es2015": "^6.9.0",
"babel-register": "^6.9.0",
"chai": "^3.5.0",
"eslint": "^2.11.0",
"eslint-loader": "^1.3.0",
"fake-xml-http-request": "^1.4.0",
"istanbul": "^0.4.3",
"jsdom": "^9.2.0",
"mocha": "^2.5.3",
"mocha-jsdom": "^1.1.0",
"rewire": "^2.5.1",
"sinon": "^1.17.4",
"webpack": "^1.13.1",
"webpack-dev-server": "^1.14.1"
}
}

564
test/application-test.js Executable file
View File

@ -0,0 +1,564 @@
/*eslint-env mocha*/
import {assert} from 'chai';
import sinon from 'sinon';
import frontexpress from '../lib/frontexpress';
import Requester from '../lib/requester';
describe('Application', () => {
let requester;
describe('generated methods', () => {
it('checks http methods are exposed', ()=> {
const app = frontexpress();
assert(typeof app.httpGet === 'function');
assert(typeof app.httpPut === 'function');
assert(typeof app.httpPost === 'function');
assert(typeof app.httpDelete === 'function');
});
it('checks middleware methods are exposed', ()=> {
const app = frontexpress();
assert(typeof app.get === 'function');
assert(typeof app.put === 'function');
assert(typeof app.post === 'function');
assert(typeof app.delete === 'function');
});
});
describe('set method', () => {
it('unsupported setting', () => {
const app = frontexpress();
try {
app.set('blabla', 'value');
} catch (ex) {
assert(ex instanceof ReferenceError);
}
});
it('supported setting', () => {
const requester = new Requester();
const app = frontexpress();
app.set('http-requester', requester);
assert(app.requester === requester);
});
});
describe('use method', () => {
beforeEach(()=>{
requester = new Requester();
sinon.stub(requester, 'fetch', ({uri, method, headers, data}, resolve, reject) => {
resolve(
{uri, method, headers, data},
{status: 200, statusText: 'OK', responseText:''}
);
});
});
it('no arguments', () => {
const app = frontexpress();
app.set('http-requester', requester);
assert.throws(app.use, TypeError);
});
it('bad arguments', () => {
const app = frontexpress();
app.set('http-requester', requester);
try {
app.use('eee');
} catch (ex) {
assert(ex instanceof TypeError);
}
});
it('middleware as function on path /', (done) => {
const spy = sinon.spy();
const app = frontexpress();
app.set('http-requester', requester);
app.use((request, response) => {spy()});
app.httpGet('/', (request, response) => {
assert(spy.callCount === 1);
app.httpPost('/route1', (request, response) => {
assert(spy.callCount === 2);
done();
});
});
});
it('middleware as function on path /route1', (done) => {
const spy = sinon.spy();
const app = frontexpress();
app.set('http-requester', requester);
app.use('/route1', (request, response) => {spy()});
app.httpGet('/route1', (request, response) => {
assert(spy.callCount === 1);
app.httpPost('/route1', (request, response) => {
assert(spy.callCount === 2);
done();
});
});
});
it('middleware as object on path /', (done) => {
const middleware = new frontexpress.Middleware('on path /');
const spy = sinon.spy(middleware, 'updated');
const app = frontexpress();
app.set('http-requester', requester);
app.use(middleware);
app.httpGet('/', (request, response) => {
assert(spy.callCount === 1);
app.httpPost('/route1', (request, response) => {
assert(spy.callCount === 2);
done();
});
});
});
it('middleware as object on path /route1', (done) => {
const middleware = new frontexpress.Middleware('on path /route1');
const spy = sinon.spy(middleware, 'updated');
const app = frontexpress();
app.set('http-requester', requester);
app.use('/route1', middleware);
app.httpGet('/route1', (request, response) => {
assert(spy.callCount === 1);
app.httpPost('/route1', (request, response) => {
assert(spy.callCount === 2);
done();
});
});
});
it('router on path /', (done) => {
const spy = sinon.spy();
const router = new frontexpress.Router();
router
.get((request, response) => {spy()})
.post((request, response) => {spy()});
const app = frontexpress();
app.set('http-requester', requester);
app.use(router);
app.httpGet('/', (request, response) => {
assert(spy.callCount === 1);
app.httpPost('/route1', (request, response) => {
assert(spy.callCount === 2);
done();
});
});
});
it('router on path /route1', (done) => {
const spy = sinon.spy();
const router = new frontexpress.Router();
router
.get((request, response) => {spy()})
.post((request, response) => {spy()});
const app = frontexpress();
app.set('http-requester', requester);
app.use('/route1', router);
app.httpGet('/route1', (request, response) => {
assert(spy.callCount === 1);
app.httpPost('/route1', (request, response) => {
assert(spy.callCount === 2);
done();
});
});
});
it('router with base uri', (done)=> {
const middleware = new frontexpress.Middleware('get middleware');
const spy = sinon.spy(middleware, 'updated');
const app = frontexpress();
app.set('http-requester', requester);
const router = new frontexpress.Router();
router.get('/subroute1', middleware);
app.use('/route1', router);
app.httpGet('/route1/subroute1', (request, response) => {
assert(spy.calledOnce);
done();
},
(request, response) => {
done(response);
});
});
});
describe('get method', () => {
beforeEach(()=>{
requester = new Requester();
sinon.stub(requester, 'fetch', ({uri, method, headers, data}, resolve, reject) => {
resolve(
{uri, method, headers, data},
{status: 200, statusText: 'OK', responseText:''}
);
});
});
it('no arguments', () => {
const app = frontexpress();
app.set('http-requester', requester);
assert.throws(app.get, TypeError);
});
it('bad arguments', () => {
const app = frontexpress();
app.set('http-requester', requester);
try {
app.get('eee');
} catch (ex) {
assert(ex instanceof TypeError);
}
try {
app.get(new frontexpress.Router());
} catch (ex) {
assert(ex instanceof TypeError);
}
try {
app.get('/route1', new frontexpress.Router());
} catch (ex) {
assert(ex instanceof TypeError);
}
});
it('middleware as function on path /', (done) => {
const spy = sinon.spy();
const app = frontexpress();
app.set('http-requester', requester);
app.get((request, response) => {spy()});
app.httpGet('/', (request, response) => {
assert(spy.callCount === 1);
app.httpPost('/route1', (request, response) => {
assert(spy.callCount === 1);
done();
});
});
});
it('middleware as function on path /route1', (done) => {
const spy = sinon.spy();
const app = frontexpress();
app.set('http-requester', requester);
app.get('/route1', (request, response) => {spy()});
app.httpGet('/route1', (request, response) => {
assert(spy.callCount === 1);
app.httpPost('/route1', (request, response) => {
assert(spy.callCount === 1);
done();
});
});
});
it('middleware as object on path /', (done) => {
const middleware = new frontexpress.Middleware('on path /');
const spy = sinon.spy(middleware, 'updated');
const app = frontexpress();
app.set('http-requester', requester);
app.get(middleware);
app.httpGet('/', (request, response) => {
assert(spy.callCount === 1);
app.httpPost('/route1', (request, response) => {
assert(spy.callCount === 1);
done();
});
});
});
it('middleware as object on path /route1', (done) => {
const middleware = new frontexpress.Middleware('on path /route1');
const spy = sinon.spy(middleware, 'updated');
const app = frontexpress();
app.set('http-requester', requester);
app.get(middleware);
app.httpGet('/route1', (request, response) => {
assert(spy.callCount === 1);
app.httpPost('/route1', (request, response) => {
assert(spy.callCount === 1);
done();
});
});
});
});
describe('route method', () => {
beforeEach(()=>{
requester = new Requester();
sinon.stub(requester, 'fetch', ({uri, method, headers, data}, resolve, reject) => {
resolve(
{uri, method, headers, data},
{status: 200, statusText: 'OK', responseText:''}
);
});
});
it('on root path not specified', (done) => {
const spy = sinon.spy();
const app = frontexpress();
app.set('http-requester', requester);
app.route().get((request, response) => {spy()});
app.httpGet('/', (request, response) => {
assert(spy.calledOnce);
app.httpPost('/', (request, response) => {
assert(spy.calledOnce);
done();
});
});
});
it('on root path /', (done) => {
const spy = sinon.spy();
const app = frontexpress();
app.set('http-requester', requester);
app.route('/').get((request, response) => {spy()});
app.httpGet('/', (request, response) => {
assert(spy.calledOnce);
app.httpPost('/', (request, response) => {
assert(spy.calledOnce);
done();
});
});
});
it('on root path /route1', (done) => {
const spy = sinon.spy();
const app = frontexpress();
app.set('http-requester', requester);
app.route('/route1').get((request, response) => {spy()});
app.httpGet('/route1', (request, response) => {
assert(spy.calledOnce);
app.httpPost('/route1', (request, response) => {
assert(spy.calledOnce);
done();
});
});
});
it('on root path undefined with sub path /subroute1', (done) => {
const spy = sinon.spy();
const app = frontexpress();
app.set('http-requester', requester);
app.route().get('/subroute1', (request, response) => {spy()});
app.httpGet('/subroute1', (request, response) => {
assert(spy.calledOnce);
app.httpPost('/subroute1', (request, response) => {
assert(spy.calledOnce);
done();
});
});
});
it('on root path / with sub path /subroute1', (done) => {
const spy = sinon.spy();
const app = frontexpress();
app.set('http-requester', requester);
app.route('/').get('/subroute1', (request, response) => {spy()});
app.httpGet('/subroute1', (request, response) => {
assert(spy.calledOnce);
app.httpPost('/subroute1', (request, response) => {
assert(spy.calledOnce);
done();
});
});
});
it('on root path /route1 with sub path /subroute1', (done) => {
const spy = sinon.spy();
const app = frontexpress();
app.set('http-requester', requester);
app.route('/route1').get('/subroute1', (request, response) => {spy()});
app.httpGet('/route1/subroute1', (request, response) => {
assert(spy.calledOnce);
app.httpPost('/route1/subroute1', (request, response) => {
assert(spy.calledOnce);
done();
});
});
});
});
describe('middleware object lifecycle', () => {
beforeEach(()=>{
requester = new Requester();
sinon.stub(requester, 'fetch', ({uri, method, headers, data}, resolve, reject) => {
resolve(
{uri, method, headers, data},
{status: 200, statusText: 'OK', responseText:''}
);
});
});
it('http GET request', (done) => {
const app = frontexpress();
app.set('http-requester', requester);
const getMiddleware = new frontexpress.Middleware('get middleware');
const spy_get_entered = sinon.spy(getMiddleware, 'entered');
const spy_get_updated = sinon.spy(getMiddleware, 'updated');
const spy_get_exited = sinon.spy(getMiddleware, 'exited');
app.route('/route1').get(getMiddleware);
app.httpGet({uri:'/route1', data: {p1: 'a', p2: 'b', p3: 'c'}}, (request, response) => {
assert(spy_get_entered.callCount === 1);
assert(spy_get_updated.callCount === 1);
assert(spy_get_exited.callCount === 0);
assert(spy_get_entered.calledBefore(spy_get_updated));
done();
},
(request, response) => {
done(response);
});
});
it('http GET followed by http POST requests', (done) => {
const app = frontexpress();
app.set('http-requester', requester);
const allMiddleware = new frontexpress.Middleware('all middleware');
const spy_all_entered = sinon.spy(allMiddleware, 'entered');
const spy_all_updated = sinon.spy(allMiddleware, 'updated');
const spy_all_exited = sinon.spy(allMiddleware, 'exited');
const getMiddleware = new frontexpress.Middleware('get middleware');
const spy_get_entered = sinon.spy(getMiddleware, 'entered');
const spy_get_updated = sinon.spy(getMiddleware, 'updated');
const spy_get_exited = sinon.spy(getMiddleware, 'exited');
const postMiddleware = new frontexpress.Middleware('post middleware');
const spy_post_entered = sinon.spy(postMiddleware, 'entered');
const spy_post_updated = sinon.spy(postMiddleware, 'updated');
const spy_post_exited = sinon.spy(postMiddleware, 'exited');
app.route('/route1')
.all(allMiddleware)
.get(getMiddleware)
.post(postMiddleware);
app.httpGet('/route1', (request, response) => {
assert(spy_all_entered.callCount === 1);
assert(spy_all_updated.callCount === 1);
assert(spy_all_exited.callCount === 0);
assert(spy_all_entered.calledBefore(spy_get_entered));
assert(spy_all_entered.calledBefore(spy_post_entered));
assert(spy_get_entered.callCount === 1);
assert(spy_get_updated.callCount === 1);
assert(spy_get_exited.callCount === 0);
assert(spy_get_entered.calledBefore(spy_post_entered));
assert(spy_post_entered.callCount === 0);
assert(spy_post_updated.callCount === 0);
assert(spy_post_exited.callCount === 0);
spy_all_entered.reset();
spy_all_updated.reset();
spy_all_exited.reset();
spy_get_entered.reset();
spy_get_updated.reset();
spy_get_exited.reset();
spy_post_entered.reset();
spy_post_updated.reset();
spy_post_exited.reset();
app.httpPost('/route1', (request, response) => {
assert(spy_all_entered.callCount === 1);
assert(spy_all_updated.callCount === 1);
assert(spy_all_exited.callCount === 1);
assert(spy_all_exited.calledBefore(spy_all_entered));
assert(spy_all_exited.calledBefore(spy_all_updated));
assert(spy_get_entered.callCount === 0);
assert(spy_get_updated.callCount === 0);
assert(spy_get_exited.callCount === 1);
assert(spy_post_entered.callCount === 1);
assert(spy_post_updated.callCount === 1);
assert(spy_post_exited.callCount === 0);
done();
}, (request, response) => {
done('should fail');
});
}, (request, response) => {
done('should fail');
});
});
it('request returning error', (done) => {
requester = new Requester();
sinon.stub(requester, 'fetch', ({uri, method, headers, data}, resolve, reject) => {
reject(
{uri, method, headers, data},
{status: 404, statusText: 'page not found'}
);
});
const app = frontexpress();
app.set('http-requester', requester);
const getMiddleware = new frontexpress.Middleware('get middleware');
const spy_get_failed = sinon.spy(getMiddleware, 'failed');
app.route('/route1').get(getMiddleware);
app.httpGet('/route1', (request, response) => {
done('should fail');
},
(request, response) => {
assert(spy_get_failed.callCount === 1);
done();
});
});
});
});

389
test/requester-test.js Executable file
View File

@ -0,0 +1,389 @@
/*eslint-env mocha*/
/*global global*/
import {assert} from 'chai';
import sinon from 'sinon';
import jsdom from 'mocha-jsdom';
import FakeXMLHttpRequest from 'fake-xml-http-request';
import Requester from '../lib/requester';
function xHttpWillRespond(xhttp, readyState, status, statusText, responseText) {
const stub_send = sinon.stub(xhttp, 'send', function() {
this.readyState = readyState;
this.status = status;
this.statusText = statusText;
this.responseText = responseText;
this.onreadystatechange()
});
const stub_open = sinon.stub(xhttp, 'open', function() {});
const stub_setRequestHeader = sinon.stub(xhttp, 'setRequestHeader', function() {});
return {stub_open, stub_send, stub_setRequestHeader};
}
function xHttpWillThrow(xhttp, sendErrorName, openErrorName) {
const stub_send = sinon.stub(xhttp, 'send', function() {
if (sendErrorName) {
throw {name: sendErrorName};
}
});
const stub_open = sinon.stub(xhttp, 'open', function() {
if (openErrorName) {
throw {name: openErrorName};
}
});
return {stub_open, stub_send};
}
describe('Requester', () => {
let xhttp;
// Init DOM with a fake document
// <base> and uri (initial uri) allow to do pushState in jsdom
jsdom({
html:`
<html>
<head>
<base href="http://localhost:8080/"></base>
</head>
</html>
`,
url: 'http://localhost:8080/'
});
beforeEach(() => {
// Stub XMLHttpRequest
xhttp = new FakeXMLHttpRequest();
global.XMLHttpRequest = () => {
return xhttp;
};
});
describe('GET Requests', () => {
it('with data', (done) => {
const requester = new Requester();
const {stub_open, stub_send} = xHttpWillRespond(xhttp, 4, 200, '', '<p>content!</p>');
requester.fetch({method: 'GET', uri:'/route1', data:{p1: 'a', p2: 'b', p3: 'c'}},
(request, response) => {
assert(request.uri === '/route1?p1=a&p2=b&p3=c');
done();
},
(err) => {
done(err);
});
});
it('without data', (done) => {
const requester = new Requester();
const {stub_open, stub_send} = xHttpWillRespond(xhttp, 4, 200, '', '<p>content!</p>');
requester.fetch({method: 'GET', uri:'/route1'}, (request, response) => {
assert(stub_open.calledOnce);
assert(stub_send.calledOnce);
assert(stub_open.calledBefore(stub_send));
assert(request.uri === '/route1');
assert(request.method === 'GET');
assert(request.data === undefined);
assert(response.status === 200);
assert(response.statusText === 'OK');
assert(response.responseText === '<p>content!</p>');
assert(response.errorThrown === undefined);
assert(response.errors === undefined);
done();
},
(error) => {
done(error);
});
});
});
describe('POST Requests', () => {
it('with data', (done) => {
const requester = new Requester();
const {stub_open, stub_send, stub_setRequestHeader} = xHttpWillRespond(xhttp, 4, 200, '', '<p>content!</p>');
requester.fetch({method: 'POST', uri:'/route1', data:{p1: 'a', p2: 'b', p3: 'c'}},
(request, response) => {
assert(stub_open.calledOnce);
assert(stub_setRequestHeader.calledOnce);
assert(stub_send.calledOnce);
assert(stub_open.calledBefore(stub_send));
assert(stub_setRequestHeader.calledAfter(stub_open));
assert(stub_setRequestHeader.calledBefore(stub_send));
assert(request.uri === '/route1');
assert(request.headers['Content-type'] === 'application/x-www-form-urlencoded');
assert(request.data === 'p1=a&p2=b&p3=c');
done();
},
(err) => {
done(err);
});
});
it('without data', (done) => {
const requester = new Requester();
const {stub_open, stub_send, stub_setRequestHeader} = xHttpWillRespond(xhttp, 4, 200, '', '<p>content!</p>');
requester.fetch({method: 'POST', uri:'/route1'},
(request, response) => {
assert(stub_open.calledOnce);
assert(stub_setRequestHeader.calledOnce);
assert(stub_send.calledOnce);
assert(stub_open.calledBefore(stub_send));
assert(stub_setRequestHeader.calledAfter(stub_open));
assert(stub_setRequestHeader.calledBefore(stub_send));
assert(request.uri === '/route1');
assert(request.headers['Content-type'] === 'application/x-www-form-urlencoded');
assert(request.data === undefined);
done();
},
(err) => {
done(err);
});
});
it('with custom headers', (done) => {
const requester = new Requester();
const {stub_open, stub_send, stub_setRequestHeader} = xHttpWillRespond(xhttp, 4, 200, '', '<p>content!</p>');
requester.fetch({method: 'POST', uri:'/route1', headers: {'Accept-Charset': 'utf-8'}},
(request, response) => {
assert(stub_open.calledOnce);
assert(stub_setRequestHeader.calledTwice);
assert(stub_send.calledOnce);
assert(stub_open.calledBefore(stub_send));
assert(stub_setRequestHeader.calledAfter(stub_open));
assert(stub_setRequestHeader.calledBefore(stub_send));
assert(request.uri === '/route1');
assert(request.headers['Content-type'] === 'application/x-www-form-urlencoded');
assert(request.headers['Accept-Charset'] === 'utf-8');
assert(request.data === undefined);
done();
},
(err) => {
done(err);
});
});
});
describe('HTTP errors', () => {
it('request returns no network', (done) => {
const requester = new Requester();
const {stub_open, stub_send} = xHttpWillRespond(xhttp, 4, 0);
requester.fetch({method: 'GET', uri: '/route1'}, null,
(request, response) => {
assert(stub_open.calledOnce);
assert(stub_send.calledOnce);
assert(stub_open.calledBefore(stub_send));
assert(response.status === 0);
assert(response.statusText === undefined);
assert(response.responseText === undefined);
assert(response.errorThrown === undefined);
assert(response.errors.length !== 0);
done();
});
});
it('request returns 401', (done) => {
const requester = new Requester();
const {stub_open, stub_send} = xHttpWillRespond(xhttp, 4, 401, 'not authenticated', '');
requester.fetch({method: 'GET', uri:'/route1'}, null,
(request, response) => {
assert(stub_send.calledOnce);
assert(stub_open.calledOnce);
assert(response.status === 401);
assert(response.statusText === 'not authenticated');
assert(response.responseText === undefined);
assert(response.errorThrown === undefined);
assert(response.errors.length !== 0);
done();
});
});
it('request returns 404', (done) => {
const requester = new Requester();
const {stub_open, stub_send} = xHttpWillRespond(xhttp, 4, 404, 'page not found', '');
requester.fetch({method: 'GET', uri:'/route1'}, null,
(request, response) => {
assert(stub_send.calledOnce);
assert(stub_open.calledOnce);
assert(response.status === 404);
assert(response.statusText === 'page not found');
assert(response.responseText === undefined);
assert(response.errorThrown === undefined);
assert(response.errors.length !== 0);
done();
});
});
it('request returns 500', (done) => {
const requester = new Requester();
const {stub_open, stub_send} = xHttpWillRespond(xhttp, 4, 500, 'server error', '');
requester.fetch({method: 'GET', uri:'/route1'}, null,
(request, response) => {
assert(stub_send.calledOnce);
assert(stub_open.calledOnce);
assert(response.status === 500);
assert(response.statusText === 'server error');
assert(response.responseText === undefined);
assert(response.errorThrown === undefined);
assert(response.errors.length !== 0);
done();
});
});
it('request returns 501 (http status not managed)', (done) => {
const requester = new Requester();
const {stub_open, stub_send} = xHttpWillRespond(xhttp, 4, 501, 'Not Implemented', '');
requester.fetch({method: 'GET', uri:'/route1'}, null,
(request, response) => {
assert(stub_send.calledOnce);
assert(stub_open.calledOnce);
assert(response.status === 501);
assert(response.statusText === 'Not Implemented');
assert(response.responseText === undefined);
assert(response.errorThrown === undefined);
assert(response.errors.length !== 0);
done();
});
});
it('request returns syntax error', (done) => {
const requester = new Requester();
const {stub_open, stub_send} = xHttpWillThrow(xhttp, 'SyntaxError');
requester.fetch({method: 'GET', uri:'/route1'}, null,
(request, response) => {
assert(stub_send.calledOnce);
assert(stub_open.calledOnce);
assert(response.status === undefined);
assert(response.statusText === undefined);
assert(response.responseText === undefined);
assert(response.errorThrown.name === 'SyntaxError');
assert(response.errors.length !== 0);
done();
});
});
it('request returns timeout error', (done) => {
const requester = new Requester();
const {stub_open, stub_send} = xHttpWillThrow(xhttp, 'TimeoutError');
requester.fetch({method: 'GET', uri:'/route1'}, null,
(request, response) => {
assert(stub_send.calledOnce);
assert(stub_open.calledOnce);
assert(response.status === undefined);
assert(response.statusText === undefined);
assert(response.responseText === undefined);
assert(response.errorThrown.name === 'TimeoutError');
assert(response.errors.length !== 0);
done();
});
});
it('request returns abort error', (done) => {
const requester = new Requester();
const {stub_open, stub_send} = xHttpWillThrow(xhttp, 'AbortError');
requester.fetch({method: 'GET', uri:'/route1'}, null,
(request, response) => {
assert(stub_send.calledOnce);
assert(stub_open.calledOnce);
assert(response.status === undefined);
assert(response.statusText === undefined);
assert(response.responseText === undefined);
assert(response.errorThrown.name === 'AbortError');
assert(response.errors.length !== 0);
done();
});
});
it('request returns network error', (done) => {
const requester = new Requester();
const {stub_open, stub_send} = xHttpWillThrow(xhttp, 'NetworkError');
requester.fetch({method: 'GET', uri:'/route1'}, null,
(request, response) => {
assert(stub_send.calledOnce);
assert(stub_open.calledOnce);
assert(response.status === undefined);
assert(response.statusText === undefined);
assert(response.responseText === undefined);
assert(response.errorThrown.name === 'NetworkError');
assert(response.errors.length !== 0);
done();
});
});
it('request returns unknown error', (done) => {
const requester = new Requester();
const {stub_open, stub_send} = xHttpWillThrow(xhttp, 'BlaBlaError');
requester.fetch({method: 'GET', uri:'/route1'}, null,
(request, response) => {
assert(stub_send.calledOnce);
assert(stub_open.calledOnce);
assert(response.status === undefined);
assert(response.statusText === undefined);
assert(response.responseText === undefined);
assert(response.errorThrown.name === 'BlaBlaError');
assert(response.errors.length !== 0);
done();
});
});
});
});

255
test/router-test.js Executable file
View File

@ -0,0 +1,255 @@
/*eslint-env mocha*/
import {assert} from 'chai';
import sinon from 'sinon';
import frontexpress from '../lib/frontexpress';
import {HTTP_METHODS} from '../lib/requester';
describe('Router', () => {
describe('generated methods', () => {
it('checks http methods are exposed', ()=> {
assert(typeof frontexpress.Router.prototype.all === 'function');
assert(typeof frontexpress.Router.prototype.get === 'function');
assert(typeof frontexpress.Router.prototype.put === 'function');
assert(typeof frontexpress.Router.prototype.post === 'function');
assert(typeof frontexpress.Router.prototype.delete === 'function');
});
});
describe('getRoutes method', () => {
it('no root path and no path uri', ()=> {
const router = new frontexpress.Router();
const middleware = (request, response) => {};
router.get(middleware);
const r1 = router.getRoutes('/', 'GET');
assert(r1.length === 1);
assert(r1[0].uri === undefined);
assert(r1[0].method === 'GET');
assert(r1[0].middleware === middleware);
});
it('no root path and path /routeX', ()=> {
const router = new frontexpress.Router();
const middleware1 = (request, response) => {};
const middleware2 = (request, response) => {};
const middleware3 = (request, response) => {};
router
.get('/route1', middleware1)
.post('/route2', middleware2)
.all('/route3', middleware3);
const r1 = router.getRoutes('/route1', 'GET');
assert(r1.length === 1);
assert(r1[0].uri === '/route1');
assert(r1[0].method === 'GET');
assert(r1[0].middleware === middleware1);
const r2 = router.getRoutes('/route2', 'POST');
assert(r2.length === 1);
assert(r2[0].uri === '/route2');
assert(r2[0].method === 'POST');
assert(r2[0].middleware === middleware2);
let r3 = router.getRoutes('/route3', 'GET');
assert(r3.length === 1);
assert(r3[0].uri === '/route3');
assert(r3[0].method === 'GET');
assert(r3[0].middleware === middleware3);
r3 = router.getRoutes('/route3', 'POST');
assert(r3.length === 1);
assert(r3[0].uri === '/route3');
assert(r3[0].method === 'POST');
assert(r3[0].middleware === middleware3);
r3 = router.getRoutes('/route3', 'PUT');
assert(r3.length === 1);
assert(r3[0].uri === '/route3');
assert(r3[0].method === 'PUT');
assert(r3[0].middleware === middleware3);
r3 = router.getRoutes('/route3', 'DELETE');
assert(r3.length === 1);
assert(r3[0].uri === '/route3');
assert(r3[0].method === 'DELETE');
assert(r3[0].middleware === middleware3);
});
it('no root path and regexp uri', ()=> {
const router = new frontexpress.Router();
const middleware = new frontexpress.Middleware();
router.get(/^\/route1/, middleware);
const r = router.getRoutes('/route1', 'GET');
assert(r.length === 1);
assert(r[0].uri instanceof RegExp);
assert(r[0].uri.toString() === new RegExp('^\/route1').toString());
assert(r[0].method === 'GET');
assert(r[0].middleware === middleware);
});
it('with root path /route1 and path /subroute', () => {
const router = new frontexpress.Router('/route1');
router.get('/subroute', new frontexpress.Middleware());
const r = router.getRoutes('/route1/subroute', 'GET');
assert(r.length === 1);
assert(r[0].uri === '/route1/subroute');
});
it('with root path /route1 and no path uri', () => {
const router = new frontexpress.Router('/route1');
router.get(new frontexpress.Middleware());
const r = router.getRoutes('/route1', 'GET');
assert(r.length === 1);
assert(r[0].uri === '/route1');
});
it('duplicate / in route', () => {
const router = new frontexpress.Router('/route1/');
router.get('/subroute', new frontexpress.Middleware());
const r = router.getRoutes('/route1/subroute', 'GET');
assert(r.length === 1);
assert(r[0].uri === '/route1/subroute');
});
it('spaces in route', () => {
let router = new frontexpress.Router(' /route1 ');
router.get('/subroute ', new frontexpress.Middleware());
let r = router.getRoutes('/route1/subroute', 'GET');
assert(r.length === 1);
assert(r[0].uri === '/route1/subroute');
// ----
router = new frontexpress.Router(' /route1 ');
router.get(new frontexpress.Middleware());
r = router.getRoutes('/route1', 'GET');
assert(r.length === 1);
assert(r[0].uri === '/route1');
});
});
describe('all method', () => {
it('no arguments', () => {
const router = new frontexpress.Router('/route1');
assert.throws(router.all, TypeError);
});
it('bad argument', () => {
const router = new frontexpress.Router('/route1');
try {
router.all('ddd');
} catch(ex) {
assert(ex instanceof TypeError);
}
try {
router.all('ddd', 'eee');
} catch(ex) {
assert(ex instanceof TypeError);
}
try {
router.all('ddd', new frontexpress.Router());
} catch(ex) {
assert(ex instanceof TypeError);
}
});
it('only middleware as argument', () => {
const router = new frontexpress.Router('/route1');
const middleware = new frontexpress.Middleware();
const spied_methods = [];
for (const method of Object.keys(HTTP_METHODS)) {
spied_methods.push(sinon.spy(router, method.toLowerCase()));
}
router.all(middleware);
for (const spied_method of spied_methods) {
assert(spied_method.calledOnce);
}
});
it('with path /route1 and middleware as arguments', () => {
const router = new frontexpress.Router();
const middleware = new frontexpress.Middleware();
const spied_methods = [];
for (const method of Object.keys(HTTP_METHODS)) {
spied_methods.push(sinon.spy(router, method.toLowerCase()));
}
router.all('/route1', middleware);
for (const spied_method of spied_methods) {
assert(spied_method.calledOnce);
}
});
});
describe('one http (get) method', () => {
it('no arguments', () => {
const router = new frontexpress.Router('/route1');
assert.throws(router.get, TypeError);
});
it('bad argument', () => {
const router = new frontexpress.Router('/route1');
try {
router.get('ddd');
} catch(ex) {
assert(ex instanceof TypeError);
}
try {
router.get('ddd', 'eee');
} catch(ex) {
assert(ex instanceof TypeError);
}
try {
router.get('ddd', new frontexpress.Router());
} catch(ex) {
assert(ex instanceof TypeError);
}
});
it('only middleware as argument', () => {
const router = new frontexpress.Router('/');
const middleware = new frontexpress.Middleware();
router.get(middleware);
const r1 = router.getRoutes('/', 'GET');
assert(r1.length === 1);
assert(r1[0].uri === '/');
assert(r1[0].method === 'GET');
assert(r1[0].middleware === middleware);
});
it('with path /route1 and middleware as arguments', () => {
const router = new frontexpress.Router();
const middleware = new frontexpress.Middleware();
router.get('/route1', middleware);
const r1 = router.getRoutes('/route1', 'GET');
assert(r1.length === 1);
assert(r1[0].uri === '/route1');
assert(r1[0].method === 'GET');
assert(r1[0].middleware === middleware);
});
});
});

44
webpack.config.babel.js Executable file
View File

@ -0,0 +1,44 @@
import webpack from 'webpack';
const cmd = (arr) => { return process.argv.indexOf(arr) > -1; }
const production = cmd('-p') || cmd('--optimize-minimize');
const debug = cmd('-d') || cmd('--debug');
const options = {
uglifyjs: undefined
};
const webpackConfig = {
entry: "./lib/frontexpress.js",
output: {
path: __dirname,
filename: 'dist/frontexpress.js'
},
devtool: 'inline-source-map',
module: {
preLoaders: [
// Javascript
{ test: /\.js$/, loader: 'eslint', exclude: /node_modules/ }
],
loaders: [
{ test: /lib\/.+\.js$/, loader: 'babel', query: { presets: ['es2015'] } },
{ test: /\.css$/, loader: 'style!css' }
]
},
eslint: {
failOnWarning: false,
failOnError: true
},
plugins: []
};
if (production) {
webpackConfig.plugins.push(
// Prevents the inclusion of duplicate code
new webpack.optimize.DedupePlugin(),
// Add UglifyJs options to the compiler
new webpack.optimize.UglifyJsPlugin(options.uglifyjs)
);
}
export default webpackConfig;