Browse Source

resolve merge

pull/7/head
Peter Johnson 10 years ago
parent
commit
5234852396
  1. 55
      README.md
  2. 35
      app.js
  3. 13
      controller/index.js
  4. 43
      controller/suggest.js
  5. 63
      docs/404.md
  6. 52
      docs/cors.md
  7. 82
      docs/index.md
  8. 50
      docs/jsonp.md
  9. 139
      docs/suggest/success.md
  10. 14
      express.js
  11. 23
      index.js
  12. 8
      middleware/404.js
  13. 9
      middleware/500.js
  14. 10
      middleware/cors.js
  15. 12
      middleware/headers.js
  16. 24
      middleware/jsonp.js
  17. 19
      middleware/toobusy.js
  18. 6
      package.json
  19. 2
      query/suggest.js
  20. 23
      sanitiser/suggest.js
  21. 11
      src/backend.js
  22. 33
      src/responder.js
  23. 5
      test/ciao.json
  24. 17
      test/ciao/404.coffee
  25. 9
      test/ciao/cors.coffee
  26. 10
      test/ciao/index.coffee
  27. 10
      test/ciao/jsonp.coffee
  28. 15
      test/ciao/suggest/success.coffee
  29. 6
      test/unit/controller/index.js
  30. 64
      test/unit/controller/suggest.js
  31. 28
      test/unit/mock/backend.js
  32. 10
      test/unit/mock/query.js
  33. 23
      test/unit/query/indeces.js
  34. 29
      test/unit/query/suggest.js
  35. 6
      test/unit/run.js
  36. 203
      test/unit/sanitiser/suggest.js

55
README.md

@ -0,0 +1,55 @@
# API
Pelias RESTful API
## Documentation
[API Documentation](https://github.com/pelias/api/tree/master/docs)
## Install Dependencies
```bash
$ npm install
```
## Contributing
Please fork and pull request against upstream master on a feature branch.
Pretty please; provide unit tests and script fixtures in the `test` directory.
### Start Server
```bash
$ npm start
```
### Running Unit Tests
```bash
$ npm run unit
```
### Running Functional Tests
```bash
$ npm run ciao
```
### Running All Tests
```bash
$ npm test
```
### Generate API Documentation
```bash
$ npm run docs
```
### Continuous Integration
Travis tests every release against node version `0.10`
[![Build Status](https://travis-ci.org/pelias/api.png?branch=master)](https://travis-ci.org/pelias/api)

35
app.js

@ -0,0 +1,35 @@
var app = require('express')();
/** ----------------------- middleware ----------------------- **/
app.use( require('./middleware/toobusy') ); // should be first
app.use( require('./middleware/headers') );
app.use( require('./middleware/cors') );
app.use( require('./middleware/jsonp') );
/** ----------------------- sanitisers ----------------------- **/
var sanitisers = {};
sanitisers.suggest = require('./sanitiser/suggest');
/** ----------------------- controllers ----------------------- **/
var controllers = {};
controllers.index = require('./controller/index');
controllers.suggest = require('./controller/suggest');
/** ----------------------- routes ----------------------- **/
// api root
app.get( '/', controllers.index() );
// suggest API
app.get( '/suggest', sanitisers.suggest.middleware, controllers.suggest() );
/** ----------------------- error middleware ----------------------- **/
app.use( require('./middleware/404') );
app.use( require('./middleware/500') );
module.exports = app;

13
controller/index.js

@ -1,13 +1,22 @@
var pkg = require('../package'); var pkg = require('../package');
function controller( req, res ){ function setup(){
function controller( req, res, next ){
// stats
res.json({ res.json({
name: pkg.name, name: pkg.name,
version: { version: {
number: pkg.version number: pkg.version
} }
}); });
}
return controller;
} }
module.exports = controller; module.exports = setup;

43
controller/suggest.js

@ -1,32 +1,41 @@
var logger = require('../src/logger'), function setup( backend, query ){
responder = require('../src/responder'),
query = require('../query/suggest'),
backend = require('../src/backend');
module.exports = function( req, res, next ){ // allow overriding of dependencies
backend = backend || require('../src/backend');
query = query || require('../query/suggest');
var reply = { function controller( req, res, next ){
date: new Date().getTime(),
body: []
};
// backend command
var cmd = { var cmd = {
index: 'pelias', index: 'pelias',
body: query( req.clean ) // generate query from clean params body: query( req.clean )
}; };
// Proxy request to ES backend & map response to a valid FeatureCollection // query backend
backend().client.suggest( cmd, function( err, data ){ backend().client.suggest( cmd, function( err, data ){
if( err ){ return responder.error( req, res, next, err ); } var docs = [];
if( data && data.pelias && data.pelias.length ){
// map options to reply body // handle backend errors
reply.body = data['pelias'][0].options; if( err ){ return next( err ); }
// map response to a valid FeatureCollection
if( data && Array.isArray( data.pelias ) && data.pelias.length ){
docs = data['pelias'][0].options || [];
} }
return responder.cors( req, res, reply ); // respond
return res.status(200).json({
date: new Date().getTime(),
body: docs
}); });
});
}
return controller;
}
}; module.exports = setup;

63
docs/404.md

@ -0,0 +1,63 @@
# invalid path
*Generated: Fri Sep 12 2014 20:51:44 GMT+0100 (BST)*
## Request
```javascript
{
"protocol": "http:",
"host": "localhost",
"method": "GET",
"port": 3100,
"path": "/notexist"
}
```
## Response
```javascript
Status: 404
{
"x-powered-by": "mapzen",
"charset": "utf8",
"access-control-allow-origin": "*",
"access-control-allow-methods": "GET",
"access-control-allow-headers": "X-Requested-With,content-type",
"access-control-allow-credentials": "true",
"server": "Pelias/0.0.0",
"cache-control": "public,max-age=300",
"content-type": "application/json; charset=utf-8",
"content-length": "35",
"etag": "W/\"23-dfdfa185\"",
"date": "Fri, 12 Sep 2014 19:51:44 GMT",
"connection": "close"
}
```
```javascript
{
"error": "not found: invalid path"
}
```
## Tests
### ✓ cache-control header correctly set
```javascript
response.should.have.header 'Cache-Control','public,max-age=300'
```
### ✓ content-type header correctly set
```javascript
response.should.have.header 'Content-Type','application/json; charset=utf-8'
```
### ✓ not found
```javascript
response.statusCode.should.equal 404
```
### ✓ should respond in json with server info
```javascript
should.exist json
should.exist json.error
json.error.should.equal 'not found: invalid path'
```

52
docs/cors.md

@ -0,0 +1,52 @@
# cross-origin resource sharing
*Generated: Fri Sep 12 2014 20:51:44 GMT+0100 (BST)*
## Request
```javascript
{
"protocol": "http:",
"host": "localhost",
"method": "GET",
"port": 3100,
"path": "/"
}
```
## Response
```javascript
Status: 200
{
"x-powered-by": "mapzen",
"charset": "utf8",
"access-control-allow-origin": "*",
"access-control-allow-methods": "GET",
"access-control-allow-headers": "X-Requested-With,content-type",
"access-control-allow-credentials": "true",
"server": "Pelias/0.0.0",
"cache-control": "public,max-age=60",
"content-type": "application/json; charset=utf-8",
"content-length": "50",
"etag": "W/\"32-85536434\"",
"date": "Fri, 12 Sep 2014 19:51:44 GMT",
"connection": "close"
}
```
```javascript
{
"name": "pelias-api",
"version": {
"number": "0.0.0"
}
}
```
## Tests
### ✓ access control headers correctly set
```javascript
response.should.have.header 'Access-Control-Allow-Origin','*'
response.should.have.header 'Access-Control-Allow-Methods','GET'
response.should.have.header 'Access-Control-Allow-Headers','X-Requested-With,content-type'
response.should.have.header 'Access-Control-Allow-Credentials','true'
```

82
docs/index.md

@ -0,0 +1,82 @@
# api root
*Generated: Fri Sep 12 2014 20:51:45 GMT+0100 (BST)*
## Request
```javascript
{
"protocol": "http:",
"host": "localhost",
"method": "GET",
"port": 3100,
"path": "/"
}
```
## Response
```javascript
Status: 200
{
"x-powered-by": "mapzen",
"charset": "utf8",
"access-control-allow-origin": "*",
"access-control-allow-methods": "GET",
"access-control-allow-headers": "X-Requested-With,content-type",
"access-control-allow-credentials": "true",
"server": "Pelias/0.0.0",
"cache-control": "public,max-age=60",
"content-type": "application/json; charset=utf-8",
"content-length": "50",
"etag": "W/\"32-85536434\"",
"date": "Fri, 12 Sep 2014 19:51:44 GMT",
"connection": "close"
}
```
```javascript
{
"name": "pelias-api",
"version": {
"number": "0.0.0"
}
}
```
## Tests
### ✓ content-type header correctly set
```javascript
response.should.have.header 'Content-Type','application/json; charset=utf-8'
```
### ✓ endpoint available
```javascript
response.statusCode.should.equal 200
```
### ✓ cache-control header correctly set
```javascript
response.should.have.header 'Cache-Control','public,max-age=60'
```
### ✓ charset header correctly set
```javascript
response.should.have.header 'Charset','utf8'
```
### ✓ server header correctly set
```javascript
response.should.have.header 'Server'
response.headers.server.should.match /Pelias\/\d{1,2}\.\d{1,2}\.\d{1,2}/
```
### ✓ vanity header correctly set
```javascript
response.should.have.header 'X-Powered-By','mapzen'
```
### ✓ should respond in json with server info
```javascript
should.exist json
should.exist json.name
should.exist json.version
```

50
docs/jsonp.md

@ -0,0 +1,50 @@
# jsonp
*Generated: Fri Sep 12 2014 20:51:45 GMT+0100 (BST)*
## Request
```javascript
{
"protocol": "http:",
"host": "localhost",
"method": "GET",
"port": 3100,
"path": "/?callback=test"
}
```
## Response
```javascript
Status: 200
{
"x-powered-by": "mapzen",
"charset": "utf8",
"access-control-allow-origin": "*",
"access-control-allow-methods": "GET",
"access-control-allow-headers": "X-Requested-With,content-type",
"access-control-allow-credentials": "true",
"server": "Pelias/0.0.0",
"cache-control": "public,max-age=60",
"content-type": "application/javascript; charset=utf-8",
"content-length": "57",
"etag": "W/\"39-b8a2aba1\"",
"date": "Fri, 12 Sep 2014 19:51:44 GMT",
"connection": "close"
}
```
```html
test({"name":"pelias-api","version":{"number":"0.0.0"}});
```
## Tests
### ✓ should respond with jsonp
```javascript
should.exist response.body
response.body.substr(0,5).should.equal 'test(';
```
### ✓ content-type header correctly set
```javascript
response.should.have.header 'Content-Type','application/javascript; charset=utf-8'
```

139
docs/suggest/success.md

@ -0,0 +1,139 @@
# valid suggest query
*Generated: Fri Sep 12 2014 20:51:45 GMT+0100 (BST)*
## Request
```javascript
{
"protocol": "http:",
"host": "localhost",
"method": "GET",
"port": 3100,
"path": "/suggest?input=a&lat=0&lon=0"
}
```
## Response
```javascript
Status: 200
{
"x-powered-by": "mapzen",
"charset": "utf8",
"access-control-allow-origin": "*",
"access-control-allow-methods": "GET",
"access-control-allow-headers": "X-Requested-With,content-type",
"access-control-allow-credentials": "true",
"server": "Pelias/0.0.0",
"cache-control": "public,max-age=60",
"content-type": "application/json; charset=utf-8",
"content-length": "1248",
"etag": "W/\"o9NALcf9i0O3JoLO7pfqog==\"",
"date": "Fri, 12 Sep 2014 19:51:44 GMT",
"connection": "close"
}
```
```javascript
{
"date": 1410551504928,
"body": [
{
"text": "ACRELÂNDIA, Brazil",
"score": 1,
"payload": {
"id": "admin2/708:adm2:br:bra:acrel__ndia",
"geo": "-66.908143,-9.954353"
}
},
{
"text": "ALTA FLORESTA, Brazil",
"score": 1,
"payload": {
"id": "admin2/2986:adm2:br:bra:alta_floresta",
"geo": "-56.404593,-10.042071"
}
},
{
"text": "ALTO ALEGRE, Brazil",
"score": 1,
"payload": {
"id": "admin2/4611:adm2:br:bra:alto_alegre",
"geo": "-62.627879,3.103540"
}
},
{
"text": "ALTO PARAÍSO, Brazil",
"score": 1,
"payload": {
"id": "admin2/4584:adm2:br:bra:alto_para__so",
"geo": "-63.418743,-9.697774"
}
},
{
"text": "ALVARÃES, Brazil",
"score": 1,
"payload": {
"id": "admin2/832:adm2:br:bra:alvar__es",
"geo": "-65.296384,-3.674615"
}
},
{
"text": "AMAJARI, Brazil",
"score": 1,
"payload": {
"id": "admin2/4610:adm2:br:bra:amajari",
"geo": "-62.710104,3.724864"
}
},
{
"text": "AMAZONAS, Brazil",
"score": 1,
"payload": {
"id": "admin1/3232:adm1:br:bra:amazonas",
"geo": "-64.949558,-3.785708"
}
},
{
"text": "ANAMÃ, Brazil",
"score": 1,
"payload": {
"id": "admin2/834:adm2:br:bra:anam__",
"geo": "-61.683670,-3.473836"
}
},
{
"text": "ANORI, Brazil",
"score": 1,
"payload": {
"id": "admin2/835:adm2:br:bra:anori",
"geo": "-62.182138,-4.154809"
}
},
{
"text": "APIACÁS, Brazil",
"score": 1,
"payload": {
"id": "admin2/2992:adm2:br:bra:apiac__s",
"geo": "-57.803447,-8.583036"
}
}
]
}
```
## Tests
### ✓ valid response
```javascript
now = new Date().getTime()
should.exist json
should.not.exist json.error
should.exist json.date
json.date.should.be.within now-1000, now+1000
should.exist json.body
json.body.should.be.instanceof Array
```
### ✓ 200 ok
```javascript
response.statusCode.should.equal 200
```

14
express.js

@ -1,14 +0,0 @@
var express = require('express'),
app = express();
// middleware modules
// app.use( require('cookie-parser')() );
// enable client-side caching of 60s by default
app.use(function(req, res, next){
res.header('Cache-Control','public,max-age=60');
next();
});
module.exports = app;

23
index.js

@ -1,10 +1,17 @@
var app = require('./express'); var cluster = require('cluster'),
app = require('./app'),
multicore = false,
port = ( process.env.PORT || 3100 );
// api root /** cluster webserver across all cores **/
app.get( '/', require('./controller/index') ); if( multicore ){
// @todo: not finished yet
// suggest API // cluster(app)
app.get( '/suggest', require('./sanitiser/suggest'), require('./controller/suggest') ); // .use(cluster.stats())
// .listen( process.env.PORT || 3100 );
app.listen( process.env.PORT || 3100 ); }
else {
console.log( 'listening on ' + port );
app.listen( process.env.PORT || 3100 );
}

8
middleware/404.js

@ -0,0 +1,8 @@
// handle not found errors
function middleware(req, res) {
res.header('Cache-Control','public,max-age=300'); // 5 minute cache
res.status(404).json({ error: 'not found: invalid path' });
}
module.exports = middleware;

9
middleware/500.js

@ -0,0 +1,9 @@
// handle application errors
function middleware(err, req, res, next) {
res.header('Cache-Control','no-cache');
if( res.statusCode < 400 ){ res.status(500); }
res.json({ error: err });
}
module.exports = middleware;

10
middleware/cors.js

@ -0,0 +1,10 @@
function middleware(req, res, next){
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Methods', 'GET');
res.header('Access-Control-Allow-Headers', 'X-Requested-With,content-type');
res.header('Access-Control-Allow-Credentials', true);
next();
}
module.exports = middleware;

12
middleware/headers.js

@ -0,0 +1,12 @@
var pkg = require('../package');
function middleware(req, res, next){
res.header('Charset','utf8');
res.header('Cache-Control','public,max-age=60');
res.header('Server', 'Pelias/'+pkg.version);
res.header('X-Powered-By', 'mapzen');
next();
}
module.exports = middleware;

24
middleware/jsonp.js

@ -0,0 +1,24 @@
function middleware(req, res, next){
// store old json function
var json = res.json.bind(res);
// replace with jsonp aware function
res.json = function( data ){
// jsonp
if( req.query && req.query.callback ){
res.header('Content-type','application/javascript');
return res.send( req.query.callback + '('+ JSON.stringify( data ) + ');' );
}
// regular json
res.header('Content-type','application/json');
return json( data );
};
next();
}
module.exports = middleware;

19
middleware/toobusy.js

@ -0,0 +1,19 @@
// middleware which blocks requests when the eventloop is too busy
var toobusy = require('toobusy');
function middleware(req, res, next){
if( toobusy() ){
res.status(503); // Service Unavailable
return next('Server Overwhelmed');
}
return next();
}
// calling .shutdown allows your process to exit normally
process.on('SIGINT', function() {
toobusy.shutdown();
process.exit();
});
module.exports = middleware;

6
package.json

@ -11,7 +11,8 @@
"test": "npm run unit && npm run ciao", "test": "npm run unit && npm run ciao",
"unit": "node test/unit/run.js | tap-spec", "unit": "node test/unit/run.js | tap-spec",
"ciao": "node node_modules/ciao/bin/ciao -c test/ciao.json test/ciao", "ciao": "node node_modules/ciao/bin/ciao -c test/ciao.json test/ciao",
"audit": "npm shrinkwrap; node node_modules/nsp/bin/nspCLI.js audit-shrinkwrap; rm npm-shrinkwrap.json;" "audit": "npm shrinkwrap; node node_modules/nsp/bin/nspCLI.js audit-shrinkwrap; rm npm-shrinkwrap.json;",
"docs": "rm -r docs; cd test/ciao; node ../../node_modules/ciao/bin/ciao -c ../ciao.json . -d ../../docs"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
@ -33,7 +34,8 @@
"dependencies": { "dependencies": {
"express": "^4.8.8", "express": "^4.8.8",
"geopipes-elasticsearch-backend": "0.0.7", "geopipes-elasticsearch-backend": "0.0.7",
"pelias-esclient": "0.0.25" "pelias-esclient": "0.0.25",
"toobusy": "^0.2.4"
}, },
"devDependencies": { "devDependencies": {
"ciao": "^0.3.4", "ciao": "^0.3.4",

2
query/suggest.js

@ -23,7 +23,7 @@ function generate( params ){
} }
}; };
logger.log( 'cmd', JSON.stringify( cmd, null, 2 ) ); // logger.log( 'cmd', JSON.stringify( cmd, null, 2 ) );
return cmd; return cmd;
} }

23
sanitiser/suggest.js

@ -14,14 +14,14 @@ function sanitize( params, cb ){
// input text // input text
if('string' !== typeof params.input || !params.input.length){ if('string' !== typeof params.input || !params.input.length){
return cb( 'invalid input text length, must be >0' ); return cb( 'invalid param \'input\': text length, must be >0' );
} }
clean.input = params.input; clean.input = params.input;
// total results // total results
var size = parseInt( params.size, 10 ); var size = parseInt( params.size, 10 );
if( !isNaN( size ) ){ if( !isNaN( size ) ){
clean.size = Math.min( size, 40 ); // max clean.size = Math.min( Math.max( size, 1 ), 40 ); // max
} else { } else {
clean.size = 10; // default clean.size = 10; // default
} }
@ -33,7 +33,7 @@ function sanitize( params, cb ){
}); });
for( var x=0; x<layers.length; x++ ){ for( var x=0; x<layers.length; x++ ){
if( -1 === indeces.indexOf( layers[x] ) ){ if( -1 === indeces.indexOf( layers[x] ) ){
return cb( 'invalid layer, must be one or more of ' + layers.join(',') ); return cb( 'invalid param \'layer\': must be one or more of ' + indeces.join(',') );
} }
} }
clean.layers = layers; clean.layers = layers;
@ -45,21 +45,21 @@ function sanitize( params, cb ){
// lat // lat
var lat = parseFloat( params.lat, 10 ); var lat = parseFloat( params.lat, 10 );
if( isNaN( lat ) || lat < 0 || lat > 90 ){ if( isNaN( lat ) || lat < 0 || lat > 90 ){
return cb( 'invalid lat, must be >0 and <90' ); return cb( 'invalid param \'lat\': must be >0 and <90' );
} }
clean.lat = lat; clean.lat = lat;
// lon // lon
var lon = parseFloat( params.lon, 10 ); var lon = parseFloat( params.lon, 10 );
if( isNaN( lon ) || lon < -180 || lon > 180 ){ if( isNaN( lon ) || lon < -180 || lon > 180 ){
return cb( 'invalid lon, must be >-180 and <180' ); return cb( 'invalid param \'lon\': must be >-180 and <180' );
} }
clean.lon = lon; clean.lon = lon;
// zoom level // zoom level
var zoom = parseInt( params.zoom, 10 ); var zoom = parseInt( params.zoom, 10 );
if( !isNaN( zoom ) ){ if( !isNaN( zoom ) ){
clean.zoom = Math.min( zoom, 18 ); // max clean.zoom = Math.min( Math.max( zoom, 1 ), 18 ); // max
} else { } else {
clean.zoom = 10; // default clean.zoom = 10; // default
} }
@ -68,9 +68,16 @@ function sanitize( params, cb ){
} }
module.exports = function( req, res, next ){ // export function
module.exports = sanitize;
// middleware
module.exports.middleware = function( req, res, next ){
sanitize( req.query, function( err, clean ){ sanitize( req.query, function( err, clean ){
if( err ){ next( err ); } if( err ){
res.status(400); // 400 Bad Request
return next(err);
}
req.clean = clean; req.clean = clean;
next(); next();
}); });

11
src/backend.js

@ -1,14 +1,7 @@
var Backend = require('geopipes-elasticsearch-backend'), var Backend = require('geopipes-elasticsearch-backend'),
backends = {}, client = require('pelias-esclient')(),
client; backends = {};
// set env specific client
if( process.env.NODE_ENV === 'test' ){
client = require('./pelias-mockclient');
} else {
client = require('pelias-esclient')();
}
function getBackend( index, type ){ function getBackend( index, type ){
var key = ( index + ':' + type ); var key = ( index + ':' + type );

33
src/responder.js

@ -1,33 +0,0 @@
// send a reply that is capable of JSON, CORS and JSONP
function cors( req, res, obj ){
res.header('Charset','utf8');
res.header('Cache-Control','public,max-age=60');
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Methods', 'GET');
res.header('Access-Control-Allow-Headers', 'X-Requested-With,content-type');
res.header('Access-Control-Allow-Credentials', true);
res.header('X-Powered-By', 'pelias');
// jsonp
if( req.query && req.query.callback ){
res.header('Content-type','application/javascript');
return res.send( req.query.callback + '('+ JSON.stringify( obj ) + ');' );
}
// regular json
res.header('Content-type','application/json');
return res.json( obj );
}
// send an error
function error( req, res, next, err ){
console.error( 'application error:', err );
// mask error from user (contains paths)
return cors( req, res, { error: 'application error' } );
}
module.exports = {
cors: cors,
error: error
};

5
test/ciao.json

@ -2,10 +2,7 @@
"defaults": { "defaults": {
"protocol": "http", "protocol": "http",
"host": "localhost", "host": "localhost",
"port": 3100, "port": 3100
"headers": {
"User-Agent": "Ciao/Client 1.0"
}
}, },
"config": {} "config": {}
} }

17
test/ciao/404.coffee

@ -0,0 +1,17 @@
#> invalid path
path: '/notexist'
#? not found
response.statusCode.should.equal 404
#? content-type header correctly set
response.should.have.header 'Content-Type','application/json; charset=utf-8'
#? cache-control header correctly set
response.should.have.header 'Cache-Control','public,max-age=300'
#? should respond in json with server info
should.exist json
should.exist json.error
json.error.should.equal 'not found: invalid path'

9
test/ciao/cors.coffee

@ -0,0 +1,9 @@
#> cross-origin resource sharing
path: '/'
#? access control headers correctly set
response.should.have.header 'Access-Control-Allow-Origin','*'
response.should.have.header 'Access-Control-Allow-Methods','GET'
response.should.have.header 'Access-Control-Allow-Headers','X-Requested-With,content-type'
response.should.have.header 'Access-Control-Allow-Credentials','true'

10
test/ciao/index.coffee

@ -8,9 +8,19 @@ response.statusCode.should.equal 200
#? content-type header correctly set #? content-type header correctly set
response.should.have.header 'Content-Type','application/json; charset=utf-8' response.should.have.header 'Content-Type','application/json; charset=utf-8'
#? charset header correctly set
response.should.have.header 'Charset','utf8'
#? cache-control header correctly set #? cache-control header correctly set
response.should.have.header 'Cache-Control','public,max-age=60' response.should.have.header 'Cache-Control','public,max-age=60'
#? server header correctly set
response.should.have.header 'Server'
response.headers.server.should.match /Pelias\/\d{1,2}\.\d{1,2}\.\d{1,2}/
#? vanity header correctly set
response.should.have.header 'X-Powered-By','mapzen'
#? should respond in json with server info #? should respond in json with server info
should.exist json should.exist json
should.exist json.name should.exist json.name

10
test/ciao/jsonp.coffee

@ -0,0 +1,10 @@
#> jsonp
path: '/?callback=test'
#? content-type header correctly set
response.should.have.header 'Content-Type','application/javascript; charset=utf-8'
#? should respond with jsonp
should.exist response.body
response.body.substr(0,5).should.equal 'test(';

15
test/ciao/suggest/success.coffee

@ -0,0 +1,15 @@
#> valid suggest query
path: '/suggest?input=a&lat=0&lon=0'
#? 200 ok
response.statusCode.should.equal 200
#? valid response
now = new Date().getTime()
should.exist json
should.not.exist json.error
should.exist json.date
json.date.should.be.within now-1000, now+1000
should.exist json.body
json.body.should.be.instanceof Array

6
test/unit/controller/index.js

@ -1,17 +1,19 @@
var controller = require('../../../controller/index'); var setup = require('../../../controller/index');
module.exports.tests = {}; module.exports.tests = {};
module.exports.tests.interface = function(test, common) { module.exports.tests.interface = function(test, common) {
test('valid interface', function(t) { test('valid interface', function(t) {
t.equal(typeof controller, 'function', 'controller is a function'); t.equal(typeof setup, 'function', 'setup is a function');
t.equal(typeof setup(), 'function', 'setup returns a controller');
t.end(); t.end();
}); });
}; };
module.exports.tests.info = function(test, common) { module.exports.tests.info = function(test, common) {
test('returns server info', function(t) { test('returns server info', function(t) {
var controller = setup();
var res = { json: function( json ){ var res = { json: function( json ){
t.equal(typeof json, 'object', 'returns json'); t.equal(typeof json, 'object', 'returns json');
t.equal(typeof json.name, 'string', 'name'); t.equal(typeof json.name, 'string', 'name');

64
test/unit/controller/suggest.js

@ -0,0 +1,64 @@
var setup = require('../../../controller/suggest'),
mockBackend = require('../mock/backend'),
mockQuery = require('../mock/query');
module.exports.tests = {};
module.exports.tests.interface = function(test, common) {
test('valid interface', function(t) {
t.equal(typeof setup, 'function', 'setup is a function');
t.equal(typeof setup(), 'function', 'setup returns a controller');
t.end();
});
};
// functionally test controller (backend success)
module.exports.tests.functional_success = function(test, common) {
test('functional test', function(t) {
var backend = mockBackend( 'client/suggest/ok/1', function( cmd ){
t.deepEqual(cmd, { body: { a: 'b' }, index: 'pelias' }, 'correct backend command');
});
var controller = setup( backend, mockQuery() );
var res = {
status: function( code ){
t.equal(code, 200, 'status set');
return res;
},
json: function( json ){
t.equal(typeof json, 'object', 'returns json');
t.equal(typeof json.date, 'number', 'date set');
t.true(Array.isArray(json.body), 'body is array');
t.deepEqual(json.body, [ { value: 1 }, { value: 2 } ], 'values correctly mapped');
t.end();
}
};
controller( { clean: { a: 'b' } }, res );
});
};
// functionally test controller (backend failure)
module.exports.tests.functional_failure = function(test, common) {
test('functional test', function(t) {
var backend = mockBackend( 'client/suggest/fail/1', function( cmd ){
t.deepEqual(cmd, { body: { a: 'b' }, index: 'pelias' }, 'correct backend command');
});
var controller = setup( backend, mockQuery() );
var next = function( message ){
t.equal(message,'a backend error occurred','error passed to errorHandler');
t.end();
};
controller( { clean: { a: 'b' } }, undefined, next );
});
};
module.exports.all = function (tape, common) {
function test(name, testFunction) {
return tape('GET /suggest ' + name, testFunction);
}
for( var testCase in module.exports.tests ){
module.exports.tests[testCase](test, common);
}
};

28
test/unit/mock/backend.js

@ -0,0 +1,28 @@
var responses = {};
responses['client/suggest/ok/1'] = function( cmd, cb ){
return cb( undefined, suggestEnvelope([ { value: 1 }, { value: 2 } ]) );
};
responses['client/suggest/fail/1'] = function( cmd, cb ){
return cb( 'a backend error occurred' );
};
function setup( key, cmdCb ){
function backend( a, b ){
return {
client: {
suggest: function( cmd, cb ){
if( 'function' === typeof cmdCb ){ cmdCb( cmd ); }
return responses[key].apply( this, arguments );
}
}
};
}
return backend;
}
function suggestEnvelope( options ){
return { pelias: [{ options: options }]};
}
module.exports = setup;

10
test/unit/mock/query.js

@ -0,0 +1,10 @@
function setup(){
return query;
}
function query( clean ){
return clean;
}
module.exports = setup;

23
test/unit/query/indeces.js

@ -0,0 +1,23 @@
var indeces = require('../../../query/indeces');
module.exports.tests = {};
module.exports.tests.interface = function(test, common) {
test('valid interface', function(t) {
t.true(Array.isArray(indeces), 'valid array');
t.equal(indeces.length, 7, 'valid array');
t.end();
});
};
module.exports.all = function (tape, common) {
function test(name, testFunction) {
return tape('indeces ' + name, testFunction);
}
for( var testCase in module.exports.tests ){
module.exports.tests[testCase](test, common);
}
};

29
test/unit/query/suggest.js

@ -0,0 +1,29 @@
var query = require('../../../query/suggest');
module.exports.tests = {};
module.exports.tests.interface = function(test, common) {
test('valid interface', function(t) {
t.equal(typeof query, 'function', 'valid function');
t.end();
});
};
module.exports.tests.interface = function(test, common) {
test('valid interface', function(t) {
t.equal(typeof query, 'function', 'valid function');
t.end();
});
};
module.exports.all = function (tape, common) {
function test(name, testFunction) {
return tape('suggest query ' + name, testFunction);
}
for( var testCase in module.exports.tests ){
module.exports.tests[testCase](test, common);
}
};

6
test/unit/run.js

@ -3,7 +3,11 @@ var tape = require('tape');
var common = {}; var common = {};
var tests = [ var tests = [
require('./controller/index') require('./controller/index'),
require('./controller/suggest'),
require('./sanitiser/suggest'),
require('./query/indeces'),
require('./query/suggest')
]; ];
tests.map(function(t) { tests.map(function(t) {

203
test/unit/sanitiser/suggest.js

@ -0,0 +1,203 @@
var sanitize = require('../../../sanitiser/suggest'),
defaultError = 'invalid param \'input\': text length, must be >0',
defaultClean = { input: 'test', lat: 0, layers: [ 'geoname', 'osmnode', 'osmway', 'admin0', 'admin1', 'admin2', 'neighborhood' ], lon: 0, size: 10, zoom: 10 };
module.exports.tests = {};
module.exports.tests.interface = function(test, common) {
test('sanitize interface', function(t) {
t.equal(typeof sanitize, 'function', 'sanitize is a function');
t.equal(sanitize.length, 2, 'sanitize interface');
t.end();
});
test('middleware interface', function(t) {
t.equal(typeof sanitize.middleware, 'function', 'middleware is a function');
t.equal(sanitize.middleware.length, 3, 'sanitize is valid middleware');
t.end();
});
};
module.exports.tests.sanitize_input = function(test, common) {
var inputs = {
invalid: [ '', 100, null, undefined, new Date() ],
valid: [ 'a', 'aa', 'aaaaaaaa' ]
};
inputs.invalid.forEach( function( input ){
test('invalid input', function(t) {
sanitize({ input: input, lat: 0, lon: 0 }, function( err, clean ){
t.equal(err, 'invalid param \'input\': text length, must be >0', 'invalid input');
t.equal(clean, undefined, 'clean not set');
t.end();
});
});
});
inputs.valid.forEach( function( input ){
test('valid input', function(t) {
sanitize({ input: input, lat: 0, lon: 0 }, function( err, clean ){
var expected = JSON.parse(JSON.stringify( defaultClean ));
expected.input = input;
t.equal(err, undefined, 'no error');
t.deepEqual(clean, expected, 'clean set correctly');
t.end();
});
});
});
};
module.exports.tests.sanitize_lat = function(test, common) {
var lats = {
invalid: [ -1, -45, -90, 91, 120, 181 ],
valid: [ 0, 45, 90, -0, '0', '45', '90' ]
};
lats.invalid.forEach( function( lat ){
test('invalid lat', function(t) {
sanitize({ input: 'test', lat: lat, lon: 0 }, function( err, clean ){
t.equal(err, 'invalid param \'lat\': must be >0 and <90', 'invalid latitude');
t.equal(clean, undefined, 'clean not set');
t.end();
});
});
});
lats.valid.forEach( function( lat ){
test('valid lat', function(t) {
sanitize({ input: 'test', lat: lat, lon: 0 }, function( err, clean ){
var expected = JSON.parse(JSON.stringify( defaultClean ));
expected.lat = parseFloat( lat );
t.equal(err, undefined, 'no error');
t.deepEqual(clean, expected, 'clean set correctly');
t.end();
});
});
});
};
module.exports.tests.sanitize_lon = function(test, common) {
var lons = {
invalid: [ -360, -181, 181, 360 ],
valid: [ -180, -1, -0, 0, 45, 90, '-180', '0', '180' ]
};
lons.invalid.forEach( function( lon ){
test('invalid lon', function(t) {
sanitize({ input: 'test', lat: 0, lon: lon }, function( err, clean ){
t.equal(err, 'invalid param \'lon\': must be >-180 and <180', 'invalid longitude');
t.equal(clean, undefined, 'clean not set');
t.end();
});
});
});
lons.valid.forEach( function( lon ){
test('valid lon', function(t) {
sanitize({ input: 'test', lat: 0, lon: lon }, function( err, clean ){
var expected = JSON.parse(JSON.stringify( defaultClean ));
expected.lon = parseFloat( lon );
t.equal(err, undefined, 'no error');
t.deepEqual(clean, expected, 'clean set correctly');
t.end();
});
});
});
};
module.exports.tests.sanitize_zoom = function(test, common) {
test('invalid zoom value', function(t) {
sanitize({ zoom: 'a', input: 'test', lat: 0, lon: 0 }, function( err, clean ){
t.equal(clean.zoom, 10, 'default zoom set');
t.end();
});
});
test('below min zoom value', function(t) {
sanitize({ zoom: -100, input: 'test', lat: 0, lon: 0 }, function( err, clean ){
t.equal(clean.zoom, 1, 'min zoom set');
t.end();
});
});
test('above max zoom value', function(t) {
sanitize({ zoom: 9999, input: 'test', lat: 0, lon: 0 }, function( err, clean ){
t.equal(clean.zoom, 18, 'max zoom set');
t.end();
});
});
};
module.exports.tests.sanitize_size = function(test, common) {
test('invalid size value', function(t) {
sanitize({ size: 'a', input: 'test', lat: 0, lon: 0 }, function( err, clean ){
t.equal(clean.size, 10, 'default size set');
t.end();
});
});
test('below min size value', function(t) {
sanitize({ size: -100, input: 'test', lat: 0, lon: 0 }, function( err, clean ){
t.equal(clean.size, 1, 'min size set');
t.end();
});
});
test('above max size value', function(t) {
sanitize({ size: 9999, input: 'test', lat: 0, lon: 0 }, function( err, clean ){
t.equal(clean.size, 40, 'max size set');
t.end();
});
});
};
module.exports.tests.sanitize_layers = function(test, common) {
test('unspecified', function(t) {
sanitize({ layers: undefined, input: 'test', lat: 0, lon: 0 }, function( err, clean ){
t.deepEqual(clean.layers, defaultClean.layers, 'default layers set');
t.end();
});
});
test('invalid layer', function(t) {
sanitize({ layers: 'test_layer', input: 'test', lat: 0, lon: 0 }, function( err, clean ){
var msg = 'invalid param \'layer\': must be one or more of geoname,osmnode,osmway,admin0,admin1,admin2,neighborhood';
t.equal(err, msg, 'invalid layer requested');
t.end();
});
});
};
module.exports.tests.invalid_params = function(test, common) {
test('invalid input params', function(t) {
sanitize( undefined, function( err, clean ){
t.equal(err, defaultError, 'handle invalid params gracefully');
t.end();
});
});
};
module.exports.tests.middleware_failure = function(test, common) {
test('middleware failure', function(t) {
var res = { status: function( code ){
t.equal(code, 400, 'status set');
}};
var next = function( message ){
t.equal(message, defaultError);
t.end();
};
sanitize.middleware( {}, res, next );
});
};
module.exports.tests.middleware_success = function(test, common) {
test('middleware success', function(t) {
var req = { query: { input: 'test', lat: 0, lon: 0 }};
var next = function( message ){
t.equal(message, undefined, 'no error message set');
t.deepEqual(req.clean, defaultClean);
t.end();
};
sanitize.middleware( req, undefined, next );
});
};
module.exports.all = function (tape, common) {
function test(name, testFunction) {
return tape('SANTIZE /suggest ' + name, testFunction);
}
for( var testCase in module.exports.tests ){
module.exports.tests[testCase](test, common);
}
};
Loading…
Cancel
Save