diff --git a/app.js b/app.js index 987b5bcc..21b465f0 100644 --- a/app.js +++ b/app.js @@ -11,6 +11,7 @@ app.use( require('./middleware/jsonp') ); /** ----------------------- sanitisers ----------------------- **/ var sanitisers = {}; +sanitisers.doc = require('./sanitiser/doc'); sanitisers.suggest = require('./sanitiser/suggest'); sanitisers.search = sanitisers.suggest; sanitisers.reverse = require('./sanitiser/reverse'); @@ -19,7 +20,9 @@ sanitisers.reverse = require('./sanitiser/reverse'); var controllers = {}; controllers.index = require('./controller/index'); +controllers.doc = require('./controller/doc'); controllers.suggest = require('./controller/suggest'); +controllers.suggest_nearby = require('./controller/suggest_nearby'); controllers.search = require('./controller/search'); /** ----------------------- routes ----------------------- **/ @@ -27,8 +30,12 @@ controllers.search = require('./controller/search'); // api root app.get( '/', controllers.index() ); +// doc API +app.get( '/doc', sanitisers.doc.middleware, controllers.doc() ); + // suggest API app.get( '/suggest', sanitisers.suggest.middleware, controllers.suggest() ); +app.get( '/suggest/nearby', sanitisers.suggest.middleware, controllers.suggest_nearby() ); // search API app.get( '/search', sanitisers.search.middleware, controllers.search() ); diff --git a/controller/doc.js b/controller/doc.js new file mode 100644 index 00000000..c945aadb --- /dev/null +++ b/controller/doc.js @@ -0,0 +1,41 @@ + +var service = { mget: require('../service/mget') }; +var geojsonify = require('../helper/geojsonify').search; + +function setup( backend ){ + + // allow overriding of dependencies + backend = backend || require('../src/backend'); + + function controller( req, res, next ){ + + var query = req.clean.ids.map( function(id) { + return { + _index: 'pelias', + _type: id.type, + _id: id.id + }; + }); + + service.mget( backend, query, function( err, docs ){ + + // error handler + if( err ){ return next( err ); } + + // convert docs to geojson + var geojson = geojsonify( docs ); + + // response envelope + geojson.date = new Date().getTime(); + + // respond + return res.status(200).json( geojson ); + + }); + + } + + return controller; +} + +module.exports = setup; diff --git a/controller/search.js b/controller/search.js index fcb12831..36f6c3f1 100644 --- a/controller/search.js +++ b/controller/search.js @@ -1,4 +1,5 @@ +var service = { search: require('../service/search') }; var geojsonify = require('../helper/geojsonify').search; function setup( backend, query ){ @@ -15,20 +16,16 @@ function setup( backend, query ){ body: query( req.clean ) }; - // query backend - backend().client.search( cmd, function( err, data ){ + if (req.clean.layers) { + cmd.type = req.clean.layers; + } - var docs = []; + // query backend + service.search( backend, cmd, function( err, docs ){ - // handle backend errors + // error handler if( err ){ return next( err ); } - if( data && data.hits && data.hits.total && Array.isArray(data.hits.hits)){ - docs = data.hits.hits.map( function( hit ){ - return hit._source; - }); - } - // convert docs to geojson var geojson = geojsonify( docs ); diff --git a/controller/suggest.js b/controller/suggest.js index 36c3cc98..425b7415 100644 --- a/controller/suggest.js +++ b/controller/suggest.js @@ -1,5 +1,7 @@ +var service = { suggest: require('../service/suggest') }; var geojsonify = require('../helper/geojsonify').suggest; +var async = require('async'); function setup( backend, query ){ @@ -9,35 +11,110 @@ function setup( backend, query ){ function controller( req, res, next ){ - // backend command var cmd = { index: 'pelias', body: query( req.clean ) }; - // query backend - backend().client.suggest( cmd, function( err, data ){ + var SIZE = req.clean.size || 10; - var docs = []; + var query_backend = function(cmd, callback) { + // query backend + service.suggest( backend, cmd, function( err, docs ){ + + // error handler + if( err ){ return next( err ); } - // handle backend errors - if( err ){ return next( err ); } + callback(null, docs); + }); + }; - // map response to a valid FeatureCollection - if( data && Array.isArray( data.pelias ) && data.pelias.length ){ - docs = data['pelias'][0].options || []; - } + var dedup = function(combined) { + var unique_ids = []; + return combined.filter(function(item, pos) { + if (unique_ids.indexOf(item.payload.id) == -1) { + unique_ids.push(item.payload.id); + return true; + } + return false; + }); + }; + + var respond = function(data) { // convert docs to geojson - var geojson = geojsonify( docs ); + var geojson = geojsonify( data ); // response envelope geojson.date = new Date().getTime(); // respond return res.status(200).json( geojson ); - }); + }; + + if (req.clean.input) { + var async_query; + // admin only + req.admin = {}; + for (k in req.clean) { req.admin[k] = req.clean[k] } + req.admin.layers = ['admin0','admin1','admin2']; + + if (req.clean.input.length < 4 && isNaN(parseInt(req.clean.input, 10))) { + async_query = { + admin_3p: function(callback){ + cmd.body = query( req.admin, 3 ); + query_backend(cmd, callback); + }, + admin_1p: function(callback){ + cmd.body = query( req.admin, 1 ); + query_backend(cmd, callback); + }, + all_3p: function(callback) { + cmd.body = query( req.clean, 3 ); + query_backend(cmd, callback); + } + } + } else { + async_query = { + all_5p: function(callback){ + cmd.body = query( req.clean, 5); + query_backend(cmd, callback); + }, + all_3p: function(callback){ + cmd.body = query( req.clean, 3); + query_backend(cmd, callback); + }, + all_1p: function(callback){ + cmd.body = query( req.clean, 1 ); + query_backend(cmd, callback); + }, + admin_1p: function(callback){ + cmd.body = query( req.admin ); + query_backend(cmd, callback); + } + } + } + + async.parallel(async_query, function(err, results) { + // results is equal to: {a: docs, b: docs, c: docs} + var splice_length = parseInt((SIZE / Object.keys(results).length), 10); + var results_keys = Object.keys(async_query); + + var combined = []; + results_keys.forEach(function(key){ + combined = combined.concat(results[key].splice(0,splice_length)); + }); + + combined = dedup(combined); + respond(combined); + }); + } else { + query_backend(cmd, function(err, results) { + respond(results); + }); + } + } return controller; diff --git a/controller/suggest_nearby.js b/controller/suggest_nearby.js new file mode 100644 index 00000000..956363b8 --- /dev/null +++ b/controller/suggest_nearby.js @@ -0,0 +1,40 @@ + +var service = { suggest: require('../service/suggest') }; +var geojsonify = require('../helper/geojsonify').suggest; + +function setup( backend, query ){ + + // allow overriding of dependencies + backend = backend || require('../src/backend'); + query = query || require('../query/suggest'); + + function controller( req, res, next ){ + + // backend command + var cmd = { + index: 'pelias', + body: query( req.clean ) + }; + + // query backend + service.suggest( backend, cmd, function( err, docs ){ + + // error handler + if( err ){ return next( err ); } + + // convert docs to geojson + var geojson = geojsonify( docs ); + + // response envelope + geojson.date = new Date().getTime(); + + // respond + return res.status(200).json( geojson ); + }); + + } + + return controller; +} + +module.exports = setup; \ No newline at end of file diff --git a/docs/404.md b/docs/404.md index 79c6241a..917c5ad4 100644 --- a/docs/404.md +++ b/docs/404.md @@ -1,6 +1,6 @@ # invalid path -*Generated: Thu Sep 25 2014 19:25:20 GMT+0100 (BST)* +*Generated: Thu Nov 06 2014 11:44:19 GMT-0500 (EST)* ## Request ```javascript { @@ -27,7 +27,7 @@ Status: 404 "content-type": "application/json; charset=utf-8", "content-length": "35", "etag": "W/\"23-dfdfa185\"", - "date": "Thu, 25 Sep 2014 18:25:20 GMT", + "date": "Thu, 06 Nov 2014 16:44:19 GMT", "connection": "close" } ``` @@ -44,6 +44,11 @@ Status: 404 response.statusCode.should.equal 404 ``` +### ✓ content-type header correctly set +```javascript +response.should.have.header 'Content-Type','application/json; charset=utf-8' +``` + ### ✓ cache-control header correctly set ```javascript response.should.have.header 'Cache-Control','public,max-age=300' @@ -56,8 +61,3 @@ should.exist json.error json.error.should.equal 'not found: invalid path' ``` -### ✓ content-type header correctly set -```javascript -response.should.have.header 'Content-Type','application/json; charset=utf-8' -``` - diff --git a/docs/cors.md b/docs/cors.md index a186bc55..17ddda39 100644 --- a/docs/cors.md +++ b/docs/cors.md @@ -1,6 +1,6 @@ # cross-origin resource sharing -*Generated: Thu Sep 25 2014 19:25:20 GMT+0100 (BST)* +*Generated: Thu Nov 06 2014 11:44:19 GMT-0500 (EST)* ## Request ```javascript { @@ -27,7 +27,7 @@ Status: 200 "content-type": "application/json; charset=utf-8", "content-length": "50", "etag": "W/\"32-85536434\"", - "date": "Thu, 25 Sep 2014 18:25:20 GMT", + "date": "Thu, 06 Nov 2014 16:44:19 GMT", "connection": "close" } ``` diff --git a/docs/doc/msuccess.md b/docs/doc/msuccess.md new file mode 100644 index 00000000..1f510a42 --- /dev/null +++ b/docs/doc/msuccess.md @@ -0,0 +1,97 @@ +# valid doc query + +*Generated: Thu Nov 06 2014 11:44:19 GMT-0500 (EST)* +## Request +```javascript +{ + "protocol": "http:", + "host": "localhost", + "method": "GET", + "port": 3100, + "path": "/doc?id=geoname:4221195&id=geoname:4193595" +} +``` + +## Response +```javascript +Status: 200 +{ + "x-powered-by": "mapzen", + "charset": "utf8", + "cache-control": "public,max-age=60", + "server": "Pelias/0.0.0", + "access-control-allow-origin": "*", + "access-control-allow-methods": "GET", + "access-control-allow-headers": "X-Requested-With,content-type", + "access-control-allow-credentials": "true", + "content-type": "application/json; charset=utf-8", + "content-length": "555", + "etag": "W/\"22b-dd736629\"", + "date": "Thu, 06 Nov 2014 16:44:19 GMT", + "connection": "close" +} +``` +```javascript +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -82.9207, + 34.36094 + ] + }, + "properties": { + "name": "Sanders Grove Cemetery", + "admin0": "United States", + "admin1": "Georgia", + "admin2": "Hart County", + "text": "Sanders Grove Cemetery, Hart County, Georgia" + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -83.94213, + 33.32262 + ] + }, + "properties": { + "name": "Etheredge Cemetery", + "admin0": "United States", + "admin1": "Georgia", + "admin2": "Butts County", + "text": "Etheredge Cemetery, Butts County, Georgia" + } + } + ], + "date": 1415292259726 +} +``` + +## Tests + +### ✓ 200 ok +```javascript +response.statusCode.should.equal 200 +``` + +### ✓ valid geojson +```javascript +json.type.should.equal 'FeatureCollection' +json.features.should.be.instanceof Array +``` + +### ✓ valid response +```javascript +now = new Date().getTime() +should.exist json +should.not.exist json.error +json.date.should.be.within now-5000, now+5000 +``` + diff --git a/docs/doc/success.md b/docs/doc/success.md new file mode 100644 index 00000000..a6dd6c00 --- /dev/null +++ b/docs/doc/success.md @@ -0,0 +1,80 @@ +# valid doc query + +*Generated: Thu Nov 06 2014 11:44:20 GMT-0500 (EST)* +## Request +```javascript +{ + "protocol": "http:", + "host": "localhost", + "method": "GET", + "port": 3100, + "path": "/doc?id=geoname:4221195" +} +``` + +## Response +```javascript +Status: 200 +{ + "x-powered-by": "mapzen", + "charset": "utf8", + "cache-control": "public,max-age=60", + "server": "Pelias/0.0.0", + "access-control-allow-origin": "*", + "access-control-allow-methods": "GET", + "access-control-allow-headers": "X-Requested-With,content-type", + "access-control-allow-credentials": "true", + "content-type": "application/json; charset=utf-8", + "content-length": "311", + "etag": "W/\"137-1644173e\"", + "date": "Thu, 06 Nov 2014 16:44:20 GMT", + "connection": "close" +} +``` +```javascript +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -82.9207, + 34.36094 + ] + }, + "properties": { + "name": "Sanders Grove Cemetery", + "admin0": "United States", + "admin1": "Georgia", + "admin2": "Hart County", + "text": "Sanders Grove Cemetery, Hart County, Georgia" + } + } + ], + "date": 1415292260057 +} +``` + +## Tests + +### ✓ valid geojson +```javascript +json.type.should.equal 'FeatureCollection' +json.features.should.be.instanceof Array +``` + +### ✓ valid response +```javascript +now = new Date().getTime() +should.exist json +should.not.exist json.error +json.date.should.be.within now-5000, now+5000 +``` + +### ✓ 200 ok +```javascript +response.statusCode.should.equal 200 +``` + diff --git a/docs/index.md b/docs/index.md index 485f343b..cde683b5 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,6 +1,6 @@ # api root -*Generated: Thu Sep 25 2014 19:25:20 GMT+0100 (BST)* +*Generated: Thu Nov 06 2014 11:44:19 GMT-0500 (EST)* ## Request ```javascript { @@ -27,7 +27,7 @@ Status: 200 "content-type": "application/json; charset=utf-8", "content-length": "50", "etag": "W/\"32-85536434\"", - "date": "Thu, 25 Sep 2014 18:25:20 GMT", + "date": "Thu, 06 Nov 2014 16:44:19 GMT", "connection": "close" } ``` @@ -42,25 +42,20 @@ Status: 200 ## Tests -### ✓ vanity header correctly set -```javascript -response.should.have.header 'X-Powered-By','mapzen' -``` - -### ✓ server header correctly set +### ✓ charset header correctly set ```javascript -response.should.have.header 'Server' -response.headers.server.should.match /Pelias\/\d{1,2}\.\d{1,2}\.\d{1,2}/ +response.should.have.header 'Charset','utf8' ``` -### ✓ content-type header correctly set +### ✓ endpoint available ```javascript -response.should.have.header 'Content-Type','application/json; charset=utf-8' +response.statusCode.should.equal 200 ``` -### ✓ cache-control header correctly set +### ✓ server header correctly set ```javascript -response.should.have.header 'Cache-Control','public,max-age=60' +response.should.have.header 'Server' +response.headers.server.should.match /Pelias\/\d{1,2}\.\d{1,2}\.\d{1,2}/ ``` ### ✓ should respond in json with server info @@ -70,13 +65,18 @@ should.exist json.name should.exist json.version ``` -### ✓ endpoint available +### ✓ vanity header correctly set ```javascript -response.statusCode.should.equal 200 +response.should.have.header 'X-Powered-By','mapzen' ``` -### ✓ charset header correctly set +### ✓ content-type header correctly set ```javascript -response.should.have.header 'Charset','utf8' +response.should.have.header 'Content-Type','application/json; charset=utf-8' +``` + +### ✓ cache-control header correctly set +```javascript +response.should.have.header 'Cache-Control','public,max-age=60' ``` diff --git a/docs/jsonp.md b/docs/jsonp.md index 8526fd96..b8c08577 100644 --- a/docs/jsonp.md +++ b/docs/jsonp.md @@ -1,6 +1,6 @@ # jsonp -*Generated: Thu Sep 25 2014 19:25:20 GMT+0100 (BST)* +*Generated: Thu Nov 06 2014 11:44:19 GMT-0500 (EST)* ## Request ```javascript { @@ -27,7 +27,7 @@ Status: 200 "content-type": "application/javascript; charset=utf-8", "content-length": "57", "etag": "W/\"39-b8a2aba1\"", - "date": "Thu, 25 Sep 2014 18:25:20 GMT", + "date": "Thu, 06 Nov 2014 16:44:19 GMT", "connection": "close" } ``` @@ -37,14 +37,14 @@ test({"name":"pelias-api","version":{"number":"0.0.0"}}); ## Tests -### ✓ should respond with jsonp +### ✓ content-type header correctly set ```javascript -should.exist response.body -response.body.substr(0,5).should.equal 'test('; +response.should.have.header 'Content-Type','application/javascript; charset=utf-8' ``` -### ✓ content-type header correctly set +### ✓ should respond with jsonp ```javascript -response.should.have.header 'Content-Type','application/javascript; charset=utf-8' +should.exist response.body +response.body.substr(0,5).should.equal 'test('; ``` diff --git a/docs/reverse/success.md b/docs/reverse/success.md index 52fa3afb..cb7e84e1 100644 --- a/docs/reverse/success.md +++ b/docs/reverse/success.md @@ -1,6 +1,6 @@ # valid reverse query -*Generated: Thu Sep 25 2014 19:25:20 GMT+0100 (BST)* +*Generated: Thu Nov 06 2014 11:44:19 GMT-0500 (EST)* ## Request ```javascript { @@ -25,9 +25,9 @@ Status: 200 "access-control-allow-headers": "X-Requested-With,content-type", "access-control-allow-credentials": "true", "content-type": "application/json; charset=utf-8", - "content-length": "263", - "etag": "W/\"107-75b55c25\"", - "date": "Thu, 25 Sep 2014 18:25:20 GMT", + "content-length": "282", + "etag": "W/\"11a-efcd00c9\"", + "date": "Thu, 06 Nov 2014 16:44:19 GMT", "connection": "close" } ``` @@ -40,29 +40,25 @@ Status: 200 "geometry": { "type": "Point", "coordinates": [ - -82.506198, - 29.542519 + -82.50622, + 29.49136 ] }, "properties": { - "name": "Archer", + "name": "Adam", "admin0": "United States", - "admin1": "*florida", - "text": "Archer, *florida, United States" + "admin1": "Florida", + "admin2": "Alachua County", + "text": "Adam, Alachua County, Florida" } } ], - "date": 1411669520735 + "date": 1415292259729 } ``` ## Tests -### ✓ 200 ok -```javascript -response.statusCode.should.equal 200 -``` - ### ✓ valid response ```javascript now = new Date().getTime() @@ -71,6 +67,11 @@ should.not.exist json.error json.date.should.be.within now-5000, now+5000 ``` +### ✓ 200 ok +```javascript +response.statusCode.should.equal 200 +``` + ### ✓ valid geojson ```javascript json.type.should.equal 'FeatureCollection' diff --git a/docs/search/success.md b/docs/search/success.md index 5e6159e1..50789c87 100644 --- a/docs/search/success.md +++ b/docs/search/success.md @@ -1,6 +1,6 @@ # valid search query -*Generated: Thu Sep 25 2014 19:25:21 GMT+0100 (BST)* +*Generated: Thu Nov 06 2014 11:44:19 GMT-0500 (EST)* ## Request ```javascript { @@ -25,9 +25,9 @@ Status: 200 "access-control-allow-headers": "X-Requested-With,content-type", "access-control-allow-credentials": "true", "content-type": "application/json; charset=utf-8", - "content-length": "289", - "etag": "W/\"121-69343a38\"", - "date": "Thu, 25 Sep 2014 18:25:20 GMT", + "content-length": "2398", + "etag": "W/\"NldeHivz2maJ3rqa73a+2w==\"", + "date": "Thu, 06 Nov 2014 16:44:19 GMT", "connection": "close" } ``` @@ -40,20 +40,173 @@ Status: 200 "geometry": { "type": "Point", "coordinates": [ - -82.357442, - 29.72089 + -82.5052, + 29.50312 ] }, "properties": { - "name": "Hidden Lake", + "name": "Blue Pete Lake", "admin0": "United States", "admin1": "Florida", - "admin2": "Alachua", - "text": "Hidden Lake, Alachua, United States" + "admin2": "Alachua County", + "text": "Blue Pete Lake, Alachua County, Florida" + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -82.52097, + 29.47185 + ] + }, + "properties": { + "name": "Sawgrass Lake", + "admin0": "United States", + "admin1": "Florida", + "admin2": "Levy County", + "text": "Sawgrass Lake, Levy County, Florida" + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -82.39141, + 29.4468 + ] + }, + "properties": { + "name": "Johnson Lake", + "admin0": "United States", + "admin1": "Florida", + "admin2": "Marion County", + "text": "Johnson Lake, Marion County, Florida" + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -82.35435, + 29.49526 + ] + }, + "properties": { + "name": "Ledwith Lake", + "admin0": "United States", + "admin1": "Florida", + "admin2": "Alachua County", + "text": "Ledwith Lake, Alachua County, Florida" + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -82.35316, + 29.52469 + ] + }, + "properties": { + "name": "Levy Lake", + "admin0": "United States", + "admin1": "Florida", + "admin2": "Alachua County", + "text": "Levy Lake, Alachua County, Florida" + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -82.66311, + 29.54036 + ] + }, + "properties": { + "name": "Fox Lake", + "admin0": "United States", + "admin1": "Florida", + "admin2": "Levy County", + "text": "Fox Lake, Levy County, Florida" + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -82.40502, + 29.61705 + ] + }, + "properties": { + "name": "Lake Kanapaha", + "admin0": "United States", + "admin1": "Florida", + "admin2": "Alachua County", + "text": "Lake Kanapaha, Alachua County, Florida" + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -82.70856, + 29.53293 + ] + }, + "properties": { + "name": "Doorshutter Lake", + "admin0": "United States", + "admin1": "Florida", + "admin2": "Levy County", + "text": "Doorshutter Lake, Levy County, Florida" + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -82.30215, + 29.52978 + ] + }, + "properties": { + "name": "Wauberg Lake", + "admin0": "United States", + "admin1": "Florida", + "admin2": "Alachua County", + "text": "Wauberg Lake, Alachua County, Florida" + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -82.47914, + 29.30795 + ] + }, + "properties": { + "name": "Lake Stafford", + "admin0": "United States", + "admin1": "Florida", + "admin2": "Levy County", + "text": "Lake Stafford, Levy County, Florida" } } ], - "date": 1411669520989 + "date": 1415292259730 } ``` @@ -67,14 +220,14 @@ should.not.exist json.error json.date.should.be.within now-5000, now+5000 ``` -### ✓ valid geojson +### ✓ 200 ok ```javascript -json.type.should.equal 'FeatureCollection' -json.features.should.be.instanceof Array +response.statusCode.should.equal 200 ``` -### ✓ 200 ok +### ✓ valid geojson ```javascript -response.statusCode.should.equal 200 +json.type.should.equal 'FeatureCollection' +json.features.should.be.instanceof Array ``` diff --git a/docs/suggest/success.md b/docs/suggest/success.md index 6798f863..8c430b53 100644 --- a/docs/suggest/success.md +++ b/docs/suggest/success.md @@ -1,6 +1,6 @@ # valid suggest query -*Generated: Thu Sep 25 2014 19:25:20 GMT+0100 (BST)* +*Generated: Thu Nov 06 2014 11:44:19 GMT-0500 (EST)* ## Request ```javascript { @@ -25,9 +25,9 @@ Status: 200 "access-control-allow-headers": "X-Requested-With,content-type", "access-control-allow-credentials": "true", "content-type": "application/json; charset=utf-8", - "content-length": "1933", - "etag": "W/\"I89q+0HZNmXyHsTfLSP5Ww==\"", - "date": "Thu, 25 Sep 2014 18:25:20 GMT", + "content-length": "571", + "etag": "W/\"23b-5d6e3dd3\"", + "date": "Thu, 06 Nov 2014 16:44:19 GMT", "connection": "close" } ``` @@ -40,47 +40,15 @@ Status: 200 "geometry": { "type": "Point", "coordinates": [ - 7.56019, - 5.419786 + -8.481618, + 43.125692 ] }, "properties": { - "text": "Abia, Nigeria", - "score": 1, + "text": "A Coruña", + "score": 14, "type": "admin1", - "id": "1775:adm1:ng:nga:abia" - } - }, - { - "type": "Feature", - "geometry": { - "type": "Point", - "coordinates": [ - -66.908143, - -9.954353 - ] - }, - "properties": { - "text": "Acrelândia, Brazil", - "score": 1, - "type": "admin2", - "id": "708:adm2:br:bra:acrel_ndia" - } - }, - { - "type": "Feature", - "geometry": { - "type": "Point", - "coordinates": [ - -60.005461, - -3.099378 - ] - }, - "properties": { - "text": "Adrianópolis, Manaus, Brasil", - "score": 1, - "type": "neighborhood", - "id": "799:_:_:_:adrian_polis" + "id": "3374:adm1:es:esp:a_coru_a" } }, { @@ -88,95 +56,15 @@ Status: 200 "geometry": { "type": "Point", "coordinates": [ - 7.909644, - 5.013733 + 7.56019, + 5.419786 ] }, "properties": { - "text": "Akwa Ibom, Nigeria", - "score": 1, + "text": "Abia", + "score": 14, "type": "admin1", - "id": "1776:adm1:ng:nga:akwa_ibom" - } - }, - { - "type": "Feature", - "geometry": { - "type": "Point", - "coordinates": [ - 9.691808, - 4.050576 - ] - }, - "properties": { - "text": "Akwa, Littoral, Cameroun", - "score": 1, - "type": "neighborhood", - "id": "1863:_:_:_:akwa" - } - }, - { - "type": "Feature", - "geometry": { - "type": "Point", - "coordinates": [ - -56.404593, - -10.042071 - ] - }, - "properties": { - "text": "Alta Floresta, Brazil", - "score": 1, - "type": "admin2", - "id": "2986:adm2:br:bra:alta_floresta" - } - }, - { - "type": "Feature", - "geometry": { - "type": "Point", - "coordinates": [ - -62.627879, - 3.10354 - ] - }, - "properties": { - "text": "Alto Alegre, Brazil", - "score": 1, - "type": "admin2", - "id": "4611:adm2:br:bra:alto_alegre" - } - }, - { - "type": "Feature", - "geometry": { - "type": "Point", - "coordinates": [ - -63.418743, - -9.697774 - ] - }, - "properties": { - "text": "Alto Paraíso, Brazil", - "score": 1, - "type": "admin2", - "id": "4584:adm2:br:bra:alto_para_so" - } - }, - { - "type": "Feature", - "geometry": { - "type": "Point", - "coordinates": [ - -65.296384, - -3.674615 - ] - }, - "properties": { - "text": "Alvarães, Brazil", - "score": 1, - "type": "admin2", - "id": "832:adm2:br:bra:alvar_es" + "id": "1775:adm1:ng:nga:abia" } }, { @@ -184,29 +72,24 @@ Status: 200 "geometry": { "type": "Point", "coordinates": [ - -62.710104, - 3.724864 + 33.772337, + 2.826081 ] }, "properties": { - "text": "Amajari, Brazil", - "score": 1, - "type": "admin2", - "id": "4610:adm2:br:bra:amajari" + "text": "Abim", + "score": 14, + "type": "admin1", + "id": "2848:adm1:ug:uga:abim" } } ], - "date": 1411669520909 + "date": 1415292259700 } ``` ## Tests -### ✓ 200 ok -```javascript -response.statusCode.should.equal 200 -``` - ### ✓ valid geojson ```javascript json.type.should.equal 'FeatureCollection' @@ -221,3 +104,8 @@ should.not.exist json.error json.date.should.be.within now-5000, now+5000 ``` +### ✓ 200 ok +```javascript +response.statusCode.should.equal 200 +``` + diff --git a/docs/suggest/success_nearby.md b/docs/suggest/success_nearby.md new file mode 100644 index 00000000..c29643c2 --- /dev/null +++ b/docs/suggest/success_nearby.md @@ -0,0 +1,223 @@ +# valid suggest query + +*Generated: Thu Nov 06 2014 11:44:20 GMT-0500 (EST)* +## Request +```javascript +{ + "protocol": "http:", + "host": "localhost", + "method": "GET", + "port": 3100, + "path": "/suggest/nearby?input=a&lat=29.49136&lon=-82.50622" +} +``` + +## Response +```javascript +Status: 200 +{ + "x-powered-by": "mapzen", + "charset": "utf8", + "cache-control": "public,max-age=60", + "server": "Pelias/0.0.0", + "access-control-allow-origin": "*", + "access-control-allow-methods": "GET", + "access-control-allow-headers": "X-Requested-With,content-type", + "access-control-allow-credentials": "true", + "content-type": "application/json; charset=utf-8", + "content-length": "2034", + "etag": "W/\"Do9VJ5hCbynTxDjtm5fNlg==\"", + "date": "Thu, 06 Nov 2014 16:44:19 GMT", + "connection": "close" +} +``` +```javascript +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -82.05231, + 29.17998 + ] + }, + "properties": { + "text": "Abiding Hope E V Lutheran Church, Marion County, Florida", + "score": 1, + "type": "geoname", + "id": "4145572" + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -82.10231, + 29.21942 + ] + }, + "properties": { + "text": "Abundant Harvest Ministries, Marion County, Florida", + "score": 1, + "type": "geoname", + "id": "4145578" + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -82.50622, + 29.49136 + ] + }, + "properties": { + "text": "Adam, Alachua County, Florida", + "score": 1, + "type": "geoname", + "id": "4145612" + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -82.75374, + 35.17789 + ] + }, + "properties": { + "text": "Adams Branch, Transylvania County, North Carolina", + "score": 1, + "type": "geoname", + "id": "4452189" + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -82.83012, + 29.4783 + ] + }, + "properties": { + "text": "Adamsville Cemetery, Levy County, Florida", + "score": 1, + "type": "geoname", + "id": "4145634" + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -82.01511, + 35.17289 + ] + }, + "properties": { + "text": "Africa School (historical), Spartanburg County, South Carolina", + "score": 1, + "type": "geoname", + "id": "4569065" + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -82.20426, + 29.25192 + ] + }, + "properties": { + "text": "Agape Baptist Church, Marion County, Florida", + "score": 1, + "type": "geoname", + "id": "4145673" + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -82.14954, + 29.19248 + ] + }, + "properties": { + "text": "Agnew Cemetery, Marion County, Florida", + "score": 1, + "type": "geoname", + "id": "4145677" + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -82.75429, + 35.16928 + ] + }, + "properties": { + "text": "Aiken Mountain, Transylvania County, North Carolina", + "score": 1, + "type": "geoname", + "id": "4452268" + } + }, + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -82.15912, + 29.47877 + ] + }, + "properties": { + "text": "Alachua County Fire Rescue Station 31, Alachua County, Florida", + "score": 1, + "type": "geoname", + "id": "4152402" + } + } + ], + "date": 1415292259785 +} +``` + +## Tests + +### ✓ 200 ok +```javascript +response.statusCode.should.equal 200 +``` + +### ✓ valid response +```javascript +now = new Date().getTime() +should.exist json +should.not.exist json.error +json.date.should.be.within now-5000, now+5000 +``` + +### ✓ valid geojson +```javascript +json.type.should.equal 'FeatureCollection' +json.features.should.be.instanceof Array +``` + diff --git a/package.json b/package.json index 4d0bfce9..aacccf89 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,8 @@ "geojson": "^0.2.0", "geopipes-elasticsearch-backend": "0.0.8", "pelias-esclient": "0.0.25", - "toobusy": "^0.2.4" + "toobusy": "^0.2.4", + "async": "^0.9.0" }, "devDependencies": { "ciao": "^0.3.4", diff --git a/query/reverse.js b/query/reverse.js index 21b755da..3ada51be 100644 --- a/query/reverse.js +++ b/query/reverse.js @@ -9,7 +9,7 @@ function generate( params ){ lon: params.lon }; - return queries.distance( centroid, { size: 1 } ); + return queries.distance( centroid, { size: params.size || 1 } ); } module.exports = generate; \ No newline at end of file diff --git a/query/suggest.js b/query/suggest.js index ba086f88..29f2f0b3 100644 --- a/query/suggest.js +++ b/query/suggest.js @@ -2,7 +2,7 @@ var logger = require('../src/logger'); // Build pelias suggest query -function generate( params ){ +function generate( params, precision ){ var getPrecision = function(zoom) { switch (true) { @@ -29,7 +29,7 @@ function generate( params ){ 'dataset': params.layers, 'location': { 'value': [ params.lon, params.lat ], - 'precision': getPrecision(params.zoom) + 'precision': precision || getPrecision(params.zoom) } } } diff --git a/sanitiser/_id.js b/sanitiser/_id.js new file mode 100644 index 00000000..073e1824 --- /dev/null +++ b/sanitiser/_id.js @@ -0,0 +1,73 @@ +// validate inputs, convert types and apply defaults +// id generally looks like 'geoname:4163334' (type:id) +// so, both type and id are required fields. + +function sanitize( req ){ + + req.clean = req.clean || {}; + var params = req.query; + var indeces = require('../query/indeces'); + var delim = ':'; + + // ensure params is a valid object + if( Object.prototype.toString.call( params ) !== '[object Object]' ){ + params = {}; + } + + var errormessage = function(fieldname, message) { + return { + 'error': true, + 'message': message || ('invalid param \''+ fieldname + '\': text length, must be >0') + } + }; + + if(('string' === typeof params.id && !params.id.length) || params.id === undefined){ + return errormessage('id'); + } + + if( params && params.id && params.id.length ){ + req.clean.ids = []; + params.ids = Array.isArray(params.id) ? params.id : [params.id]; + + // de-dupe + params.ids = params.ids.filter(function(item, pos) { + return params.ids.indexOf(item) == pos; + }); + + for (var i=0; i valid doc query +path: '/doc?id=geoname:4221195&id=geoname:4193595' + +#? 200 ok +response.statusCode.should.equal 200 + +#? valid response +now = new Date().getTime() +should.exist json +should.not.exist json.error +json.date.should.be.within now-5000, now+5000 + +#? valid geojson +json.type.should.equal 'FeatureCollection' +json.features.should.be.instanceof Array \ No newline at end of file diff --git a/test/ciao/doc/success.coffee b/test/ciao/doc/success.coffee new file mode 100644 index 00000000..3818aca6 --- /dev/null +++ b/test/ciao/doc/success.coffee @@ -0,0 +1,16 @@ + +#> valid doc query +path: '/doc?id=geoname:4221195' + +#? 200 ok +response.statusCode.should.equal 200 + +#? valid response +now = new Date().getTime() +should.exist json +should.not.exist json.error +json.date.should.be.within now-5000, now+5000 + +#? valid geojson +json.type.should.equal 'FeatureCollection' +json.features.should.be.instanceof Array \ No newline at end of file diff --git a/test/ciao/suggest/success_nearby.coffee b/test/ciao/suggest/success_nearby.coffee new file mode 100644 index 00000000..ca958720 --- /dev/null +++ b/test/ciao/suggest/success_nearby.coffee @@ -0,0 +1,16 @@ + +#> valid suggest query +path: '/suggest/nearby?input=a&lat=29.49136&lon=-82.50622' + +#? 200 ok +response.statusCode.should.equal 200 + +#? valid response +now = new Date().getTime() +should.exist json +should.not.exist json.error +json.date.should.be.within now-5000, now+5000 + +#? valid geojson +json.type.should.equal 'FeatureCollection' +json.features.should.be.instanceof Array \ No newline at end of file diff --git a/test/unit/controller/doc.js b/test/unit/controller/doc.js new file mode 100644 index 00000000..7b481a9f --- /dev/null +++ b/test/unit/controller/doc.js @@ -0,0 +1,92 @@ + +var setup = require('../../../controller/doc'), + mockBackend = require('../mock/backend'); + +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) { + + // expected geojson features for 'client/doc/ok/1' fixture + var expected = [{ + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [ -50.5, 100.1 ] + }, + properties: { + name: 'test name1', + admin0: 'country1', + admin1: 'state1', + admin2: 'city1' + } + }, { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [ -51.5, 100.2 ] + }, + properties: { + name: 'test name2', + admin0: 'country2', + admin1: 'state2', + admin2: 'city2' + } + }]; + + test('functional success', function(t) { + var backend = mockBackend( 'client/doc/ok/1', function( cmd ){ + t.deepEqual(cmd, { body: { docs: [ { _id: 123, _index: 'pelias', _type: 'a' } ] } }, 'correct backend command'); + }); + var controller = setup( backend ); + 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.equal(json.type, 'FeatureCollection', 'valid geojson'); + t.true(Array.isArray(json.features), 'features is array'); + t.deepEqual(json.features, expected, 'values correctly mapped'); + t.end(); + } + }; + controller( { clean: { ids: [ {'id' : 123, 'type': 'a' } ] } }, res ); + }); +}; + +// functionally test controller (backend failure) +module.exports.tests.functional_failure = function(test, common) { + test('functional failure', function(t) { + var backend = mockBackend( 'client/doc/fail/1', function( cmd ){ + t.deepEqual(cmd, { body: { docs: [ { _id: 123, _index: 'pelias', _type: 'b' } ] } }, 'correct backend command'); + }); + var controller = setup( backend ); + var next = function( message ){ + t.equal(message,'a backend error occurred','error passed to errorHandler'); + t.end(); + }; + controller( { clean: { ids: [ {'id' : 123, 'type': 'b' } ] } }, undefined, next ); + }); +}; + +module.exports.all = function (tape, common) { + + function test(name, testFunction) { + return tape('GET /doc ' + name, testFunction); + } + + for( var testCase in module.exports.tests ){ + module.exports.tests[testCase](test, common); + } +}; \ No newline at end of file diff --git a/test/unit/controller/suggest.js b/test/unit/controller/suggest.js index f67f5da7..57051964 100644 --- a/test/unit/controller/suggest.js +++ b/test/unit/controller/suggest.js @@ -24,7 +24,7 @@ module.exports.tests.functional_success = function(test, common) { coordinates: [ 101, -10.1 ] }, properties: { - id: 'mockid', + id: 'mockid1', type: 'mocktype', value: 1 } @@ -35,7 +35,7 @@ module.exports.tests.functional_success = function(test, common) { coordinates: [ 101, -10.1 ] }, properties: { - id: 'mockid', + id: 'mockid2', type: 'mocktype', value: 2 } @@ -43,7 +43,12 @@ module.exports.tests.functional_success = function(test, common) { test('functional success', function(t) { var backend = mockBackend( 'client/suggest/ok/1', function( cmd ){ - t.deepEqual(cmd, { body: { a: 'b' }, index: 'pelias' }, 'correct backend command'); + if (cmd.body.layers) { + // layers are set exclusively for admin: test for admin-only layers + t.deepEqual(cmd, { body: { input: 'b', layers: [ 'admin0', 'admin1', 'admin2' ] }, index: 'pelias' }, 'correct backend command'); + } else { + t.deepEqual(cmd, { body: { input: 'b' }, index: 'pelias' }, 'correct backend command'); + } }); var controller = setup( backend, mockQuery() ); var res = { @@ -60,7 +65,7 @@ module.exports.tests.functional_success = function(test, common) { t.end(); } }; - controller( { clean: { a: 'b' } }, res ); + controller( { clean: { input: 'b' } }, res ); }); }; diff --git a/test/unit/controller/suggest_nearby.js b/test/unit/controller/suggest_nearby.js new file mode 100644 index 00000000..bfb96195 --- /dev/null +++ b/test/unit/controller/suggest_nearby.js @@ -0,0 +1,91 @@ + +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) { + + // expected geojson features for 'client/suggest/ok/1' fixture + var expected = [{ + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [ 101, -10.1 ] + }, + properties: { + id: 'mockid1', + type: 'mocktype', + value: 1 + } + }, { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [ 101, -10.1 ] + }, + properties: { + id: 'mockid2', + type: 'mocktype', + value: 2 + } + }]; + + test('functional success', 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.equal(json.type, 'FeatureCollection', 'valid geojson'); + t.true(Array.isArray(json.features), 'features is array'); + t.deepEqual(json.features, expected, '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 failure', 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); + } +}; \ No newline at end of file diff --git a/test/unit/mock/backend.js b/test/unit/mock/backend.js index 63378423..8c4e7baa 100644 --- a/test/unit/mock/backend.js +++ b/test/unit/mock/backend.js @@ -1,12 +1,14 @@ -var mockPayload = { - id: 'mocktype/mockid', - geo: '101,-10.1' +var mockPayload = function(id){ + return { + id: 'mocktype/mockid'+id, + geo: '101,-10.1' + } }; var responses = {}; responses['client/suggest/ok/1'] = function( cmd, cb ){ - return cb( undefined, suggestEnvelope([ { value: 1, payload: mockPayload }, { value: 2, payload: mockPayload } ]) ); + return cb( undefined, suggestEnvelope([ { value: 1, payload: mockPayload(1) }, { value: 2, payload: mockPayload(2) } ]) ); }; responses['client/suggest/fail/1'] = function( cmd, cb ){ return cb( 'a backend error occurred' ); @@ -32,10 +34,33 @@ responses['client/search/fail/1'] = function( cmd, cb ){ return cb( 'a backend error occurred' ); }; +responses['client/doc/ok/1'] = function( cmd, cb ){ + return cb( undefined, docEnvelope([{ + _source: { + value: 1, + center_point: { lat: 100.1, lon: -50.5 }, + name: { default: 'test name1' }, + admin0: 'country1', admin1: 'state1', admin2: 'city1' + } + }, { + _source: { + value: 2, + center_point: { lat: 100.2, lon: -51.5 }, + name: { default: 'test name2' }, + admin0: 'country2', admin1: 'state2', admin2: 'city2' + } + }])); +}; +responses['client/doc/fail/1'] = responses['client/search/fail/1']; + function setup( key, cmdCb ){ function backend( a, b ){ return { client: { + mget: function( cmd, cb ){ + if( 'function' === typeof cmdCb ){ cmdCb( cmd ); } + return responses[key].apply( this, arguments ); + }, suggest: function( cmd, cb ){ if( 'function' === typeof cmdCb ){ cmdCb( cmd ); } return responses[key].apply( this, arguments ); @@ -50,6 +75,10 @@ function setup( key, cmdCb ){ return backend; } +function docEnvelope( options ){ + return { docs: options }; +} + function suggestEnvelope( options ){ return { pelias: [{ options: options }]}; } diff --git a/test/unit/query/reverse.js b/test/unit/query/reverse.js index 4b54b646..85a0720c 100644 --- a/test/unit/query/reverse.js +++ b/test/unit/query/reverse.js @@ -58,6 +58,16 @@ module.exports.tests.query = function(test, common) { }; t.deepEqual(query, expected, 'valid reverse query'); + + // test different sizes + var sizes = [1,2,10,undefined,null]; + sizes.forEach( function(size) { + query = generate({ + lat: 29.49136, lon: -82.50622, size: size + }); + expected.size = size ? size : 1; + t.deepEqual(query, expected, 'valid reverse query for size: '+ size); + }); t.end(); }); }; diff --git a/test/unit/query/suggest.js b/test/unit/query/suggest.js index 74e138ff..77c3f099 100644 --- a/test/unit/query/suggest.js +++ b/test/unit/query/suggest.js @@ -63,9 +63,8 @@ module.exports.tests.precision = function(test, common) { {zoom:null, precision:1}, {zoom:undefined, precision:1} ]; - - test_cases.forEach( function( test_case ){ - test('valid precision where zoom = ' + test_case.zoom, function(t) { + test('valid precision', function(t) { + test_cases.forEach( function( test_case ){ var query = generate({ input: 'test', size: 10, lat: 0, lon: 0, zoom:test_case.zoom, @@ -87,9 +86,9 @@ module.exports.tests.precision = function(test, common) { } } }; - t.deepEqual(query, expected, 'valid suggest query'); - t.end(); + t.deepEqual(query, expected, 'valid suggest query for zoom = ' + test_case.zoom); }); + t.end(); }); }; diff --git a/test/unit/run.js b/test/unit/run.js index 96e9c95a..4428903c 100644 --- a/test/unit/run.js +++ b/test/unit/run.js @@ -4,9 +4,15 @@ var common = {}; var tests = [ require('./controller/index'), + require('./controller/doc'), require('./controller/suggest'), + require('./controller/suggest_nearby'), require('./controller/search'), - require('./sanitiser/sanitise'), + require('./service/mget'), + require('./service/search'), + require('./service/suggest'), + require('./sanitiser/suggest'), + require('./sanitiser/doc'), require('./query/indeces'), require('./query/suggest'), require('./query/search'), diff --git a/test/unit/sanitiser/doc.js b/test/unit/sanitiser/doc.js new file mode 100644 index 00000000..dcd15cbb --- /dev/null +++ b/test/unit/sanitiser/doc.js @@ -0,0 +1,159 @@ + +var doc = require('../../../sanitiser/doc'), + _sanitize = doc.sanitize, + middleware = doc.middleware, + indeces = require('../../../query/indeces'), + delimiter = ':', + defaultLengthError = function(input) { return 'invalid param \''+ input + '\': text length, must be >0' }, + defaultFormatError = 'invalid: must be of the format type:id for ex: \'geoname:4163334\'', + defaultError = 'invalid param \'id\': text length, must be >0', + defaultMissingTypeError = function(input) { + var type = input.split(delimiter)[0]; + return type + ' is invalid. It must be one of these values - [' + indeces.join(", ") + ']'}, + defaultClean = { ids: [ { id: '123', type: 'geoname' } ] }, + sanitize = function(query, cb) { _sanitize({'query':query}, cb); }, + inputs = { + valid: [ 'geoname:1', 'osmnode:2', 'admin0:53', 'osmway:44', 'geoname:5' ], + invalid: [ ':', '', '::', 'geoname:', ':234', 'gibberish:23' ] + }; + +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 middleware, 'function', 'middleware is a function'); + t.equal(middleware.length, 3, 'sanitizee has a valid middleware'); + t.end(); + }); +}; + +module.exports.tests.sanitize_id = function(test, common) { + test('invalid input', function(t) { + inputs.invalid.forEach( function( input ){ + sanitize({ id: input }, function( err, clean ){ + switch (err) { + case defaultError: + t.equal(err, defaultError, input + ' is invalid input'); break; + case defaultLengthError(input): + t.equal(err, defaultLengthError(input), input + ' is invalid (missing id/type)'); break; + case defaultFormatError: + t.equal(err, defaultFormatError, input + ' is invalid (invalid format)'); break; + case defaultMissingTypeError(input): + t.equal(err, defaultMissingTypeError(input), input + ' is an unknown type'); break; + default: break; + } + t.equal(clean, undefined, 'clean not set'); + }); + }); + t.end(); + }); + + test('valid input', function(t) { + inputs.valid.forEach( function( input ){ + var input_parts = input.split(delimiter); + var expected = { ids: [ { id: input_parts[1], type: input_parts[0] } ] }; + sanitize({ id: input }, function( err, clean ){ + t.equal(err, undefined, 'no error (' + input + ')' ); + t.deepEqual(clean, expected, 'clean set correctly (' + input + ')'); + }); + }); + t.end(); + }); +}; + + +module.exports.tests.sanitize_ids = function(test, common) { + test('invalid input', function(t) { + sanitize({ id: inputs.invalid }, function( err, clean ){ + var input = inputs.invalid[0]; // since it breaks on the first invalid element + switch (err) { + case defaultError: + t.equal(err, defaultError, input + ' is invalid input'); break; + case defaultLengthError(input): + t.equal(err, defaultLengthError(input), input + ' is invalid (missing id/type)'); break; + case defaultFormatError: + t.equal(err, defaultFormatError, input + ' is invalid (invalid format)'); break; + case defaultMissingTypeError(input): + t.equal(err, defaultMissingTypeError(input), input + ' is an unknown type'); break; + default: break; + } + t.equal(clean, undefined, 'clean not set'); + }); + t.end(); + }); + + test('valid input', function(t) { + var expected={}; + expected.ids = []; + inputs.valid.forEach( function( input ){ + var input_parts = input.split(delimiter); + expected.ids.push({ id: input_parts[1], type: input_parts[0] }); + }); + sanitize({ id: inputs.valid }, function( err, clean ){ + t.equal(err, undefined, 'no error' ); + t.deepEqual(clean, expected, 'clean set correctly'); + }); + t.end(); + }); +}; + +module.exports.tests.de_dupe = function(test, common) { + var expected = { ids: [ { id: '1', type: 'geoname' }, { id: '2', type: 'osmnode' } ] }; + test('duplicate ids', function(t) { + sanitize( { id: ['geoname:1', 'osmnode:2', 'geoname:1'] }, function( err, clean ){ + t.equal(err, undefined, 'no error' ); + t.deepEqual(clean, expected, 'clean set correctly'); + t.end(); + }); + }); +}; + +module.exports.tests.invalid_params = function(test, common) { + test('invalid 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(); + }; + middleware( {}, res, next ); + }); +}; + +module.exports.tests.middleware_success = function(test, common) { + test('middleware success', function(t) { + var req = { query: { id: 'geoname' + delimiter + '123' }}; + var next = function( message ){ + t.equal(message, undefined, 'no error message set'); + t.deepEqual(req.clean, defaultClean); + t.end(); + }; + middleware( req, undefined, next ); + }); +}; + +module.exports.all = function (tape, common) { + + function test(name, testFunction) { + return tape('SANTIZE /doc ' + name, testFunction); + } + + for( var testCase in module.exports.tests ){ + module.exports.tests[testCase](test, common); + } +}; \ No newline at end of file diff --git a/test/unit/sanitiser/sanitise.js b/test/unit/sanitiser/suggest.js similarity index 65% rename from test/unit/sanitiser/sanitise.js rename to test/unit/sanitiser/suggest.js index b183d2f9..52096be2 100644 --- a/test/unit/sanitiser/sanitise.js +++ b/test/unit/sanitiser/suggest.js @@ -26,25 +26,25 @@ module.exports.tests.sanitize_input = function(test, common) { invalid: [ '', 100, null, undefined, new Date() ], valid: [ 'a', 'aa', 'aaaaaaaa' ] }; - inputs.invalid.forEach( function( input ){ - test('invalid input', function(t) { + test('invalid input', function(t) { + inputs.invalid.forEach( function( input ){ 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(err, 'invalid param \'input\': text length, must be >0', input + ' is an invalid input'); t.equal(clean, undefined, 'clean not set'); - t.end(); }); }); + t.end(); }); - inputs.valid.forEach( function( input ){ - test('valid input', function(t) { + test('valid input', function(t) { + inputs.valid.forEach( function( input ){ 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(); + t.deepEqual(clean, expected, 'clean set correctly (' + input + ')'); }); }); + t.end(); }); }; @@ -53,25 +53,25 @@ module.exports.tests.sanitize_lat = function(test, common) { invalid: [ -181, -120, -91, 91, 120, 181 ], valid: [ 0, 45, 90, -0, '0', '45', '90' ] }; - lats.invalid.forEach( function( lat ){ - test('invalid lat', function(t) { + test('invalid lat', function(t) { + lats.invalid.forEach( function( lat ){ sanitize({ input: 'test', lat: lat, lon: 0 }, function( err, clean ){ - t.equal(err, 'invalid param \'lat\': must be >-90 and <90', 'invalid latitude'); + t.equal(err, 'invalid param \'lat\': must be >-90 and <90', lat + ' is an invalid latitude'); t.equal(clean, undefined, 'clean not set'); - t.end(); }); }); + t.end(); }); - lats.valid.forEach( function( lat ){ - test('valid lat', function(t) { + test('valid lat', function(t) { + lats.valid.forEach( function( lat ){ 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(); + t.deepEqual(clean, expected, 'clean set correctly (' + lat + ')'); }); }); + t.end(); }); }; @@ -80,25 +80,26 @@ module.exports.tests.sanitize_lon = function(test, common) { 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) { + test('invalid lon', function(t) { + lons.invalid.forEach( function( lon ){ 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(err, 'invalid param \'lon\': must be >-180 and <180', lon + ' is an invalid longitude'); t.equal(clean, undefined, 'clean not set'); - t.end(); + }); }); + t.end(); }); - lons.valid.forEach( function( lon ){ - test('valid lon', function(t) { + test('valid lon', function(t) { + lons.valid.forEach( function( lon ){ 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(); + t.deepEqual(clean, expected, 'clean set correctly (' + lon + ')'); }); }); + t.end(); }); }; @@ -153,11 +154,55 @@ module.exports.tests.sanitize_layers = function(test, common) { }); 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'; + var msg = 'invalid param \'layer\': must be one or more of geoname,osmnode,osmway,admin0,admin1,admin2,neighborhood,poi,admin'; t.equal(err, msg, 'invalid layer requested'); t.end(); }); }); + test('poi (alias) layer', function(t) { + var poi_layers = ['geoname','osmnode','osmway']; + sanitize({ layers: 'poi', input: 'test', lat: 0, lon: 0 }, function( err, clean ){ + t.deepEqual(clean.layers, poi_layers, 'poi layers set'); + t.end(); + }); + }); + test('admin (alias) layer', function(t) { + var admin_layers = ['admin0','admin1','admin2','neighborhood']; + sanitize({ layers: 'admin', input: 'test', lat: 0, lon: 0 }, function( err, clean ){ + t.deepEqual(clean.layers, admin_layers, 'admin layers set'); + t.end(); + }); + }); + test('poi alias layer plus regular layers', function(t) { + var poi_layers = ['geoname','osmnode','osmway']; + var reg_layers = ['admin0', 'admin1']; + sanitize({ layers: 'poi,admin0,admin1', input: 'test', lat: 0, lon: 0 }, function( err, clean ){ + t.deepEqual(clean.layers, reg_layers.concat(poi_layers), 'poi + regular layers'); + t.end(); + }); + }); + test('admin alias layer plus regular layers', function(t) { + var admin_layers = ['admin0','admin1','admin2','neighborhood']; + var reg_layers = ['geoname', 'osmway']; + sanitize({ layers: 'admin,geoname,osmway', input: 'test', lat: 0, lon: 0 }, function( err, clean ){ + t.deepEqual(clean.layers, reg_layers.concat(admin_layers), 'admin + regular layers set'); + t.end(); + }); + }); + test('alias layer plus regular layers (no duplicates)', function(t) { + var poi_layers = ['geoname','osmnode','osmway']; + sanitize({ layers: 'poi,geoname,osmnode', input: 'test', lat: 0, lon: 0 }, function( err, clean ){ + t.deepEqual(clean.layers, poi_layers, 'poi layers found (no duplicates)'); + t.end(); + }); + }); + test('multiple alias layers (no duplicates)', function(t) { + var alias_layers = ['geoname','osmnode','osmway','admin0','admin1','admin2','neighborhood']; + sanitize({ layers: 'poi,admin', input: 'test', lat: 0, lon: 0 }, function( err, clean ){ + t.deepEqual(clean.layers, alias_layers, 'all layers found (no duplicates)'); + t.end(); + }); + }); }; module.exports.tests.invalid_params = function(test, common) { @@ -197,7 +242,7 @@ module.exports.tests.middleware_success = function(test, common) { module.exports.all = function (tape, common) { function test(name, testFunction) { - return tape('SANTIZE /sanitise ' + name, testFunction); + return tape('SANTIZE /suggest ' + name, testFunction); } for( var testCase in module.exports.tests ){ diff --git a/test/unit/service/mget.js b/test/unit/service/mget.js new file mode 100644 index 00000000..0b43f717 --- /dev/null +++ b/test/unit/service/mget.js @@ -0,0 +1,82 @@ + +var setup = require('../../../service/mget'), + mockBackend = require('../mock/backend'); + +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.end(); + }); +}; + +// functionally test service +module.exports.tests.functional_success = function(test, common) { + + var expected = [ + { + value: 1, + center_point: { lat: 100.1, lon: -50.5 }, + name: { default: 'test name1' }, + admin0: 'country1', admin1: 'state1', admin2: 'city1' + }, + { + value: 2, + center_point: { lat: 100.2, lon: -51.5 }, + name: { default: 'test name2' }, + admin0: 'country2', admin1: 'state2', admin2: 'city2' + } + ]; + + test('valid query', function(t) { + var backend = mockBackend( 'client/doc/ok/1', function( cmd ){ + t.deepEqual(cmd, { body: { docs: [ { _id: 123, _index: 'pelias', _type: 'a' } ] } }, 'correct backend command'); + }); + setup( backend, [ { _id: 123, _index: 'pelias', _type: 'a' } ], function(err, data) { + t.true(Array.isArray(data), 'returns an array'); + data.forEach(function(d) { + t.true(typeof d === 'object', 'valid object'); + }); + t.deepEqual(data, expected, 'values correctly mapped') + t.end(); + }); + }); + +}; + +// functionally test service +module.exports.tests.functional_failure = function(test, common) { + + test('invalid query', function(t) { + var invalid_queries = [ + { _id: 123, _index: 'pelias' }, + { _id: 123, _type: 'a' }, + { _index: 'pelias', _type: 'a' }, + { } + ]; + + var backend = mockBackend( 'client/doc/fail/1', function( cmd ){ + t.notDeepEqual(cmd, { body: { docs: [ { _id: 123, _index: 'pelias', _type: 'a' } ] } }, 'incorrect backend command'); + }); + invalid_queries.forEach(function(query) { + setup( backend, [ query ], function(err, data) { + t.equal(err, 'a backend error occurred','error passed to errorHandler'); + t.equal(data, undefined, 'data is undefined'); + }); + }); + t.end(); + }); + +}; + +module.exports.all = function (tape, common) { + + function test(name, testFunction) { + return tape('SERVICE /mget ' + name, testFunction); + } + + for( var testCase in module.exports.tests ){ + module.exports.tests[testCase](test, common); + } +}; \ No newline at end of file diff --git a/test/unit/service/search.js b/test/unit/service/search.js new file mode 100644 index 00000000..2e1b3393 --- /dev/null +++ b/test/unit/service/search.js @@ -0,0 +1,82 @@ + +var setup = require('../../../service/search'), + mockBackend = require('../mock/backend'); + +var example_valid_es_query = { body: { a: 'b' }, index: 'pelias' }; + +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.end(); + }); +}; + +// functionally test service +module.exports.tests.functional_success = function(test, common) { + + var expected = [ + { + value: 1, + center_point: { lat: 100.1, lon: -50.5 }, + name: { default: 'test name1' }, + admin0: 'country1', admin1: 'state1', admin2: 'city1' + }, + { + value: 2, + center_point: { lat: 100.2, lon: -51.5 }, + name: { default: 'test name2' }, + admin0: 'country2', admin1: 'state2', admin2: 'city2' + } + ]; + + test('valid ES query', function(t) { + var backend = mockBackend( 'client/search/ok/1', function( cmd ){ + t.deepEqual(cmd, example_valid_es_query, 'no change to the command'); + }); + setup( backend, example_valid_es_query, function(err, data) { + t.true(Array.isArray(data), 'returns an array'); + data.forEach(function(d) { + t.true(typeof d === 'object', 'valid object'); + }); + t.deepEqual(data, expected, 'values correctly mapped') + t.end(); + }); + }); + +}; + +// functionally test service +module.exports.tests.functional_failure = function(test, common) { + + test('invalid ES query', function(t) { + var invalid_queries = [ + { }, + { foo: 'bar' } + ]; + + var backend = mockBackend( 'client/search/fail/1', function( cmd ){ + t.notDeepEqual(cmd, example_valid_es_query, 'incorrect backend command'); + }); + invalid_queries.forEach(function(query) { + setup( backend, [ query ], function(err, data) { + t.equal(err, 'a backend error occurred','error passed to errorHandler'); + t.equal(data, undefined, 'data is undefined'); + }); + }); + t.end(); + }); + +}; + +module.exports.all = function (tape, common) { + + function test(name, testFunction) { + return tape('SERVICE /search ' + name, testFunction); + } + + for( var testCase in module.exports.tests ){ + module.exports.tests[testCase](test, common); + } +}; \ No newline at end of file diff --git a/test/unit/service/suggest.js b/test/unit/service/suggest.js new file mode 100644 index 00000000..c74a72c1 --- /dev/null +++ b/test/unit/service/suggest.js @@ -0,0 +1,79 @@ + +var setup = require('../../../service/suggest'), + mockBackend = require('../mock/backend'); + +var example_valid_es_query = { body: { a: 'b' }, index: 'pelias' }; + +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.end(); + }); +}; + +// functionally test service +module.exports.tests.functional_success = function(test, common) { + + var mockPayload = function(id){ + return { + id: 'mocktype/mockid'+id, + geo: '101,-10.1' + } + }; + + var expected = [ + { value: 1, payload: mockPayload(1) }, + { value: 2, payload: mockPayload(2) } + ]; + + test('valid ES query', function(t) { + var backend = mockBackend( 'client/suggest/ok/1', function( cmd ){ + t.deepEqual(cmd, example_valid_es_query, 'no change to the command'); + }); + setup( backend, example_valid_es_query, function(err, data) { + t.true(Array.isArray(data), 'returns an array'); + data.forEach(function(d) { + t.true(typeof d === 'object', 'valid object'); + }); + t.deepEqual(data, expected, 'values correctly mapped') + t.end(); + }); + }); + +}; + +// functionally test service +module.exports.tests.functional_failure = function(test, common) { + + test('invalid ES query', function(t) { + var invalid_queries = [ + { }, + { foo: 'bar' } + ]; + + var backend = mockBackend( 'client/suggest/fail/1', function( cmd ){ + t.notDeepEqual(cmd, example_valid_es_query, 'incorrect backend command'); + }); + invalid_queries.forEach(function(query) { + setup( backend, [ query ], function(err, data) { + t.equal(err, 'a backend error occurred','error passed to errorHandler'); + t.equal(data, undefined, 'data is undefined'); + }); + }); + t.end(); + }); + +}; + +module.exports.all = function (tape, common) { + + function test(name, testFunction) { + return tape('SERVICE /suggest ' + name, testFunction); + } + + for( var testCase in module.exports.tests ){ + module.exports.tests[testCase](test, common); + } +}; \ No newline at end of file