Browse Source

Merge branch 'master' into boost-exact-matches#5

Pull in the latest of master.

Conflicts:
	query/reverse.js
	query/sort.js
pull/113/head
Severyn Kozak 10 years ago
parent
commit
98a0a33256
  1. 5
      .gitignore
  2. 2
      .jshintignore
  3. 123
      DOCS.md
  4. 6
      README.md
  5. 2
      app.js
  6. 2
      controller/doc.js
  7. 21
      controller/index.js
  8. 2
      controller/search.js
  9. 2
      controller/suggest.js
  10. 119
      helper/category_weights.js
  11. 33
      helper/geojsonify.js
  12. 7
      middleware/500.js
  13. 16
      package.json
  14. 15
      query/reverse.js
  15. 12
      query/sort.js
  16. 38
      sanitiser/_categories.js
  17. 39
      sanitiser/_details.js
  18. 168
      sanitiser/_geo.js
  19. 4
      sanitiser/_id.js
  20. 4
      sanitiser/_input.js
  21. 2
      sanitiser/_layers.js
  22. 4
      sanitiser/_size.js
  23. 3
      sanitiser/coarse.js
  24. 3
      sanitiser/doc.js
  25. 8
      sanitiser/reverse.js
  26. 1
      sanitiser/search.js
  27. 1
      sanitiser/suggest.js
  28. 7
      test/ciao/index.coffee
  29. 10
      test/ciao/jsonp.coffee
  30. 54
      test/unit/controller/doc.js
  31. 44
      test/unit/controller/index.js
  32. 54
      test/unit/controller/search.js
  33. 64
      test/unit/controller/suggest.js
  34. 84
      test/unit/helper/geojsonify.js
  35. 77
      test/unit/query/reverse.js
  36. 12
      test/unit/query/search.js
  37. 12
      test/unit/query/sort.js
  38. 3
      test/unit/sanitiser/coarse.js
  39. 46
      test/unit/sanitiser/doc.js
  40. 84
      test/unit/sanitiser/reverse.js
  41. 60
      test/unit/sanitiser/search.js
  42. 60
      test/unit/sanitiser/suggest.js

5
.gitignore vendored

@ -1,2 +1,5 @@
node_modules
*.log
coverage
.idea
*.log
reports

2
.jshintignore

@ -1 +1,3 @@
node_modules
coverage
reports

123
DOCS.md

@ -0,0 +1,123 @@
## /search
Full text search endpoint which queries the elasticsearch doc store, slightly slower than suggest.
#### Required Parameters
* **input**: query string
#### Optional Parameters
* **lat**: latitude
* **lon**: longitude
* **zoom**: zoom level from which you wish to view the world
* **size**: number of results requested (defaults to 10)
* **layers**: datasets you wish to query (defaults to ```poi,admin,address```).
* valid values are ```poi```, ```admin``` or ```address```
* ```poi``` expands internally to ```geoname```, ```osmnode```, ```osmway```
* ```admin``` expands to ```admin0```, ```admin1```, ```admin2```, ```neighborhood```, ```locality```, ```local_admin```
* ```address``` expands to ```osmaddress```, ```openaddresses```
* can also be specific to one particular dataset, for example ```geoname```
* **bbox**: the bounding box from which you want all your results to come
* can be one of the following comma separated string values
* "southwest_lng,southwest_lat,northeast_lng,northeast_lat" ```L.latLngBounds(southwestLatLng, northeastLatLng).toBBoxString()```
* bottom left lon, bottom left lat, top right lon, top right lat
* left, bottom, right, top
* min longitude, min latitude, max longitude, max latitude
* **details**: indicates if results should contain detailed, should be `true` or `false`
* when false results will only contain `id`, `layer`, and `text` properties
* when true, all available properties will be included in results
## /search/coarse
This is a coarse forward geocoder endpoint which only searches admin dataset layers.
#### Required Parameters
* **input**: query string
#### Optional Parameters
* **lat**: latitude
* **lon**: longitude
* **zoom**: zoom level from which you wish to view the world
* **bbox**: the bounding box frome which you want all your results to come
* **size**: (defaults to 10)
* **layers**: (defaults to ```admin```)
* **details**: (defaults to `true`)
## /suggest
The autocomplete endpoint, it offers fast response time. Mixes results from around the provided lat/lon and also from precision level 1 and 3.
#### Required Parameters
* **input**: query string
* **lat**: latitude
* **lon**: longitude
* lat/lon are **required** currently because of this [open issue](https://github.com/elasticsearch/elasticsearch/issues/6444)
#### Optional Parameters
* **zoom**: zoom level from which you wish to view the world
* **size**: number of results requested (defaults to 10)
* **layers**: datasets you wish to query (defaults to ```poi,admin,address```)
* **details**: (defaults to `true`)
## /suggest/coarse
Only queries the admin layers.
#### Required Parameters
* **input**: query string
* **lat**: latitude from where you are searching
* **lon**: longitude
* lat/lon are **required** currently because of this [open issue](https://github.com/elasticsearch/elasticsearch/issues/6444)
#### Optional Parameters
* **zoom**: zoom level from which you wish to view the world
* **size**: number of results requested (defaults to 10)
* **layers**: datasets you wish to query (defaults to ```admin```)
* **details**: (defaults to `true`)
## /suggest/nearby
Works as autocomplete for places located near a latitude/longitude, this endpoint is the same as ```/suggest``` but the results are all from within 50 kilometers of the specified point. Unlike ```/suggest```, ```/suggest/nearby``` does not mix results from different precision levels (500km, 1000km etc from lat/lon).
#### Required Parameters
* **input**: query string
* **lat**: latitude
* **lon**: longitude
* lat/lon are **required** currently because of this [open issue](https://github.com/elasticsearch/elasticsearch/issues/6444)
#### Optional Parameters
* **zoom**: zoom level from which you wish to view the world
* **size**: number of results you need (defaults to 10)
* **layers**: datasets you wish to query (defaults to ```poi,admin,address```)
* **details**: (defaults to `true`)
## /reverse
Reverse geocoding endpoint.
#### Required Parameters
* **lat**: latitude
* **lon**: longitude
#### Optional Parameters
* **zoom**: zoom level from which you wish to view the world
* **bbox**: bounding box
* **layers**: (defaults to ```poi,admin,address```)
* **details**: (defaults to `true`)
## /doc
Retrieves a document or multiple documents at once.
#### Required Parameters
* one of **id** or **ids**
* **id**:
* unique id of the document to be retrieved
* should be in the form of type:id, for example: ```geoname:4163334```
* **ids**:
* if multiple docs are to be fetched in bulk, an array of ids

6
README.md

@ -58,6 +58,12 @@ $ npm test
$ npm run docs
```
### Code Coverage
```bash
$ npm run coverage
```
### Continuous Integration
Travis tests every release against node version `0.10`

2
app.js

@ -38,7 +38,7 @@ app.get( '/suggest/nearby',
sanitisers.suggest.middleware,
controllers.suggest(undefined, undefined, require('./helper/queryMixer').suggest_nearby) );
app.get( '/suggest/coarse',
sanitisers.suggest.middleware,
sanitisers.coarse.middleware,
controllers.suggest(undefined, undefined, require('./helper/queryMixer').coarse) );
// search API

2
controller/doc.js

@ -23,7 +23,7 @@ function setup( backend ){
if( err ){ return next( err ); }
// convert docs to geojson
var geojson = geojsonify( docs );
var geojson = geojsonify( docs, req.clean );
// response envelope
geojson.date = new Date().getTime();

21
controller/index.js

@ -1,22 +1,31 @@
var pkg = require('../package');
var markdown = require('markdown').markdown;
var fs = require('fs');
function setup(){
function controller( req, res, next ){
// stats
var styleString = '<style>html{font-family:monospace}</style>';
var text = '# Pelias API\n';
text += '### Version: ['+ pkg.version+ '](https://github.com/pelias/api/releases)\n';
text += fs.readFileSync( './DOCS.md', 'utf8');
var indexHtml = styleString + markdown.toHTML(text);
function controller( req, res, next ) {
if (req.accepts('html')) {
res.send(indexHtml);
return;
}
// default behaviour
res.json({
name: pkg.name,
version: {
number: pkg.version
}
});
}
return controller;
}
module.exports = setup;
module.exports = setup;

2
controller/search.js

@ -28,7 +28,7 @@ function setup( backend, query ){
if( err ){ return next( err ); }
// convert docs to geojson
var geojson = geojsonify( docs );
var geojson = geojsonify( docs, req.clean );
// response envelope
geojson.date = new Date().getTime();

2
controller/suggest.js

@ -27,7 +27,7 @@ function setup( backend, query, query_mixer ){
function reply( docs ){
// convert docs to geojson
var geojson = geojsonify( docs );
var geojson = geojsonify( docs, req.clean );
// response envelope
geojson.date = new Date().getTime();

119
helper/category_weights.js

@ -0,0 +1,119 @@
/**
* These values specify how much a record that matches a certain category
* should be boosted in elasticsearch results.
*/
module.exports = {
'transport': 10,
'transport:air': 20,
'transport:air:aerodrome': 20,
'transport:air:airport': 20,
'recreation': 10,
'religion': 10,
'education': 10,
'entertainment': 10,
'nightlife': 10,
'food': 10,
'government': 10,
'professional': 10,
'finance': 10,
'health': 10,
'retail': 10,
'transport:public': 10,
'transport:bus': 10,
'transport:taxi': 10,
'transport:sea': 10,
'accomodation': 10,
'transport:station': 10,
'food:bagel': 10,
'food:barbecue': 10,
'food:bougatsa': 10,
'food:burger': 10,
'food:cake': 10,
'food:casserole': 10,
'food:chicken': 10,
'food:coffee_shop': 10,
'food:crepe': 10,
'food:couscous': 10,
'food:curry': 10,
'food:dessert': 10,
'food:donut': 10,
'food:empanada': 10,
'food:fish': 10,
'food:fish_and_chips': 10,
'food:fried_food': 10,
'food:friture': 10,
'food:gyro': 10,
'food:ice_cream': 10,
'food:kebab': 10,
'food:mediterranean': 10,
'food:noodle': 10,
'food:pancake': 10,
'food:pasta': 10,
'food:pie': 10,
'food:pizza': 10,
'food:regional': 10,
'food:sandwich': 10,
'food:sausage': 10,
'food:savory_pancakes': 10,
'food:seafood': 10,
'food:steak': 10,
'food:sub': 10,
'food:sushi': 10,
'food:tapas': 10,
'food:vegan': 10,
'food:vegetarian': 10,
'food:wings': 10,
'food:cuisine:african': 10,
'food:cuisine:american': 10,
'food:cuisine:arab': 10,
'food:cuisine:argentinian': 10,
'food:cuisine:asian': 10,
'food:cuisine:australian': 10,
'food:cuisine:baiana': 10,
'food:cuisine:balkan': 10,
'food:cuisine:basque': 10,
'food:cuisine:bavarian': 10,
'food:cuisine:belarusian': 10,
'food:cuisine:brazilian': 10,
'food:cuisine:cantonese': 10,
'food:cuisine:capixaba': 10,
'food:cuisine:caribbean': 10,
'food:cuisine:chinese': 10,
'food:cuisine:croatian': 10,
'food:cuisine:czech': 10,
'food:cuisine:danish': 10,
'food:cuisine:french': 10,
'food:cuisine:gaucho': 10,
'food:cuisine:german': 10,
'food:cuisine:greek': 10,
'food:cuisine:hunan': 10,
'food:cuisine:hungarian': 10,
'food:cuisine:indian': 10,
'food:cuisine:international': 10,
'food:cuisine:iranian': 10,
'food:cuisine:italian': 10,
'food:cuisine:japanese': 10,
'food:cuisine:korean': 10,
'food:cuisine:kyo_ryouri': 10,
'food:cuisine:latin_american': 10,
'food:cuisine:lebanese': 10,
'food:cuisine:malagasy': 10,
'food:cuisine:mexican': 10,
'food:cuisine:mineira': 10,
'food:cuisine:okinawa_ryori': 10,
'food:cuisine:pakistani': 10,
'food:cuisine:peruvian': 10,
'food:cuisine:polish': 10,
'food:cuisine:portuguese': 10,
'food:cuisine:rhenish': 10,
'food:cuisine:russian': 10,
'food:cuisine:shandong': 10,
'food:cuisine:sichuan': 10,
'food:cuisine:spanish': 10,
'food:cuisine:thai': 10,
'food:cuisine:turkish': 10,
'food:cuisine:vietnamese': 10,
'food:cuisine:westphalian': 10,
'transport:rail': 10
};

33
helper/geojsonify.js

@ -3,7 +3,7 @@ var GeoJSON = require('geojson'),
extent = require('geojson-extent'),
outputGenerator = require('./outputGenerator');
function search( docs ){
function search( docs, params ){
// emit a warning if the doc format is invalid
// @note: if you see this error, fix it ASAP!
@ -12,6 +12,9 @@ function search( docs ){
return false; // remove offending doc from results
}
var details = params ? params.details : {};
details = details === true || details === 1;
// flatten & expand data for geojson conversion
var geodata = docs.map( function( doc ){
@ -29,19 +32,21 @@ function search( docs ){
output.lat = parseFloat( doc.center_point.lat );
output.lng = parseFloat( doc.center_point.lon );
// map name
if( !doc.name || !doc.name.default ) { return warning(); }
output.name = doc.name.default;
// map admin values
if( doc.alpha3 ){ output.alpha3 = doc.alpha3; }
if( doc.admin0 ){ output.admin0 = doc.admin0; }
if( doc.admin1 ){ output.admin1 = doc.admin1; }
if( doc.admin1_abbr ){ output.admin1_abbr = doc.admin1_abbr; }
if( doc.admin2 ){ output.admin2 = doc.admin2; }
if( doc.local_admin ){ output.local_admin = doc.local_admin; }
if( doc.locality ){ output.locality = doc.locality; }
if( doc.neighborhood ){ output.neighborhood = doc.neighborhood; }
if (details) {
// map name
if( !doc.name || !doc.name.default ) { return warning(); }
output.name = doc.name.default;
// map admin values
if( doc.alpha3 ){ output.alpha3 = doc.alpha3; }
if( doc.admin0 ){ output.admin0 = doc.admin0; }
if( doc.admin1 ){ output.admin1 = doc.admin1; }
if( doc.admin1_abbr ){ output.admin1_abbr = doc.admin1_abbr; }
if( doc.admin2 ){ output.admin2 = doc.admin2; }
if( doc.local_admin ){ output.local_admin = doc.local_admin; }
if( doc.locality ){ output.locality = doc.locality; }
if( doc.neighborhood ){ output.neighborhood = doc.neighborhood; }
}
// generate region-specific text string
output.text = outputGenerator( doc );

7
middleware/500.js

@ -1,9 +1,12 @@
var logger = require( '../src/logger' );
// handle application errors
function middleware(err, req, res, next) {
logger.error( 'Error:', err );
logger.error( 'Stack trace:', err.trace );
res.header('Cache-Control','no-cache');
if( res.statusCode < 400 ){ res.status(500); }
res.json({ error: err });
res.json({ error: typeof err === 'string' ? err : 'internal server error' });
}
module.exports = middleware;
module.exports = middleware;

16
package.json

@ -1,16 +1,17 @@
{
"name": "pelias-api",
"author": "mapzen",
"version": "0.0.0",
"version": "1.1.7",
"description": "Pelias API",
"homepage": "https://github.com/pelias/api",
"license": "MIT",
"main": "index.js",
"scripts": {
"start": "node index.js",
"test": "npm run unit && npm run ciao",
"test": "npm run unit && npm run coverage",
"unit": "node test/unit/run.js | tap-spec",
"ciao": "node node_modules/ciao/bin/ciao -c test/ciao.json test/ciao",
"coverage": "node_modules/.bin/istanbul cover test/unit/run.js",
"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"
},
@ -37,16 +38,19 @@
"geojson": "^0.2.0",
"geojson-extent": "^0.3.1",
"geopipes-elasticsearch-backend": "0.0.12",
"pelias-suggester-pipeline": "2.0.2",
"is-object": "^1.0.1",
"pelias-esclient": "0.0.25"
"markdown": "0.5.0",
"pelias-esclient": "0.0.25",
"pelias-suggester-pipeline": "2.0.2"
},
"devDependencies": {
"ciao": "^0.3.4",
"istanbul": "^0.3.13",
"jshint": "^2.5.6",
"nsp": "^0.3.0",
"precommit-hook": "^1.0.7",
"tape": "^2.13.4",
"proxyquire": "^1.4.0",
"tap-spec": "^0.2.0",
"nsp": "^0.3.0"
"tape": "^2.13.4"
}
}

15
query/reverse.js

@ -1,7 +1,6 @@
var logger = require('../src/logger'),
queries = require('geopipes-elasticsearch-backend').queries,
sort = require('../query/sort');
var queries = require('geopipes-elasticsearch-backend').queries,
sort = require('./sort');
function generate( params ){
@ -13,7 +12,17 @@ function generate( params ){
var query = queries.distance( centroid, { size: params.size || 1 } );
query.sort = query.sort.concat( sort( params ) );
if ( params.categories && params.categories.length > 0 ) {
addCategoriesFilter( query, params.categories );
}
return query;
}
function addCategoriesFilter( query, categories ) {
query.query.filtered.filter.bool.must.push({
terms: { category: categories }
});
}
module.exports = generate;

12
query/sort.js

@ -1,6 +1,8 @@
var admin_boost = 'admin_boost';
var population = 'population';
var popularity = 'popularity';
var category = 'category';
var category_weights = require('../helper/category_weights');
var weights = require('pelias-suggester-pipeline').weights;
var isObject = require( 'is-object' );
@ -27,6 +29,16 @@ module.exports = function( params ){
'order': 'desc'
}
},
{
'_script': {
'params': {
'category_weights': category_weights
},
'file': category,
'type': 'number',
'order': 'desc'
}
},
{
'_script': {
'params': {

38
sanitiser/_categories.js

@ -0,0 +1,38 @@
var isObject = require('is-object');
// validate inputs, convert types and apply defaults
function sanitize( req ){
var clean = req.clean || {};
var params= req.query;
// ensure the input params are a valid object
if( !isObject( params ) ){
params = {};
}
// default case (no categories specified in GET params)
if('string' !== typeof params.categories || !params.categories.length){
clean.categories = [];
}
else {
// parse GET params
clean.categories = params.categories.split(',')
.map(function (cat) {
return cat.toLowerCase().trim(); // lowercase inputs
})
.filter( function( cat ) {
return ( cat.length > 0 );
});
}
// pass validated params to next middleware
req.clean = clean;
return { 'error': false };
}
// export function
module.exports = sanitize;

39
sanitiser/_details.js

@ -0,0 +1,39 @@
var isObject = require('is-object');
// validate inputs, convert types and apply defaults
function sanitize( req, default_value ){
var clean = req.clean || {};
var params= req.query;
if (default_value === undefined) {
default_value = true;
}
default_value = !!default_value;
// ensure the input params are a valid object
if( !isObject( params ) ){
params = {};
}
if (params.details !== undefined) {
var details = params.details;
if (typeof params.details === 'string') {
details = params.details === 'true';
}
clean.details = details === true || details === 1;
} else {
clean.details = default_value;
}
req.clean = clean;
return {'error':false};
}
// export function
module.exports = sanitize;

168
sanitiser/_geo.js

@ -1,96 +1,114 @@
var isObject = require('is-object');
// validate inputs, convert types and apply defaults
function sanitize( req, latlon_is_required ){
module.exports = function sanitize( req, latlon_is_required ){
var clean = req.clean || {};
var params= req.query;
var params = req.query;
latlon_is_required = latlon_is_required || false;
// ensure the input params are a valid object
if( Object.prototype.toString.call( params ) !== '[object Object]' ){
if( !isObject( params ) ){
params = {};
}
var is_invalid_lat = function(lat) {
return isNaN( lat ) || lat < -90 || lat > 90;
};
var is_invalid_lon = function(lon) {
return isNaN( lon ) || lon < -180 || lon > 180;
};
// lat
var lat = parseFloat( params.lat, 10 );
if (!isNaN(lat)) {
if( is_invalid_lat(lat) ){
return {
'error': true,
'message': 'invalid param \'lat\': must be >-90 and <90'
};
}
clean.lat = lat;
} else if (latlon_is_required) {
try {
sanitize_coord( 'lat', clean, params.lat, latlon_is_required );
sanitize_coord( 'lon', clean, params.lon, latlon_is_required );
sanitize_zoom_level(clean, params.zoom);
sanitize_bbox(clean, params.bbox);
}
catch (err) {
return {
'error': true,
'message': 'missing param \'lat\': must be >-90 and <90'
'message': err.message
};
}
// lon
var lon = parseFloat( params.lon, 10 );
if (!isNaN(lon)) {
if( is_invalid_lon(lon) ){
return {
'error': true,
'message': 'invalid param \'lon\': must be >-180 and <180'
req.clean = clean;
return { 'error': false };
};
/**
* Parse and validate bbox parameter
* bbox = bottom_left lon, bottom_left lat, top_right lon, top_right lat
* bbox = left, bottom, right, top
* bbox = min Longitude, min Latitude, max Longitude, max Latitude
*
* @param {object} clean
* @param {string} param
*/
function sanitize_bbox( clean, param ) {
if( !param ) {
return;
}
var bbox = [];
var bboxArr = param.split( ',' );
if( Array.isArray( bboxArr ) && bboxArr.length === 4 ) {
bbox = bboxArr.filter( function( latlon, index ) {
latlon = parseFloat( latlon, 10 );
return !(lat_lon_checks[(index % 2 === 0 ? 'lon' : 'lat')].is_invalid( latlon ));
});
if( bbox.length === 4 ) {
clean.bbox = {
right: Math.max( bbox[0], bbox[2] ),
top: Math.max( bbox[1], bbox[3] ),
left: Math.min( bbox[0], bbox[2] ),
bottom: Math.min( bbox[1], bbox[3] )
};
} else {
throw new Error('invalid bbox');
}
clean.lon = lon;
} else if (latlon_is_required) {
return {
'error': true,
'message': 'missing param \'lon\': must be >-180 and <180'
};
}
}
// zoom level
var zoom = parseInt( params.zoom, 10 );
if( !isNaN( zoom ) ){
clean.zoom = Math.min( Math.max( zoom, 1 ), 18 ); // max
}
// bbox
// bbox = bottom_left lat, bottom_left lon, top_right lat, top_right lon
// bbox = left,bottom,right,top
// bbox = min Longitude , min Latitude , max Longitude , max Latitude
if (params.bbox) {
var bbox = [];
var bboxArr = params.bbox.split(',');
if( Array.isArray(bboxArr) && bboxArr.length === 4 ) {
bbox = bboxArr.filter(function(latlon, index) {
latlon = parseFloat(latlon, 10);
return !(index % 2 === 0 ? is_invalid_lat(latlon) : is_invalid_lon(latlon));
});
if (bbox.length === 4) {
clean.bbox = {
top : Math.max(bbox[0], bbox[2]),
right : Math.max(bbox[1], bbox[3]),
bottom: Math.min(bbox[0], bbox[2]),
left : Math.min(bbox[1], bbox[3])
};
} else {
return {
'error': true,
'message': 'invalid bbox'
};
}
/**
* Validate lat,lon values
*
* @param {string} coord lat|lon
* @param {object} clean
* @param {string} param
* @param {bool} latlon_is_required
*/
function sanitize_coord( coord, clean, param, latlon_is_required ) {
var value = parseFloat( param, 10 );
if ( !isNaN( value ) ) {
if( lat_lon_checks[coord].is_invalid( value ) ){
throw new Error( 'invalid ' + lat_lon_checks[coord].error_msg );
}
}
req.clean = clean;
return { 'error': false };
clean[coord] = value;
}
else if (latlon_is_required) {
throw new Error('missing ' + lat_lon_checks[coord].error_msg);
}
}
function sanitize_zoom_level( clean, param ) {
var zoom = parseInt( param, 10 );
if( !isNaN( zoom ) ){
clean.zoom = Math.min( Math.max( zoom, 1 ), 18 ); // max
}
}
// export function
module.exports = sanitize;
var lat_lon_checks = {
lat: {
is_invalid: function is_invalid_lat(lat) {
return isNaN( lat ) || lat < -90 || lat > 90;
},
error_msg: 'param \'lat\': must be >-90 and <90'
},
lon: {
is_invalid: function is_invalid_lon(lon) {
return isNaN(lon) || lon < -180 || lon > 180;
},
error_msg: 'param \'lon\': must be >-180 and <180'
}
};

4
sanitiser/_id.js

@ -1,3 +1,5 @@
var isObject = require('is-object');
// validate inputs, convert types and apply defaults
// id generally looks like 'geoname:4163334' (type:id)
// so, both type and id are required fields.
@ -10,7 +12,7 @@ function sanitize( req ){
var delim = ':';
// ensure params is a valid object
if( Object.prototype.toString.call( params ) !== '[object Object]' ){
if( !isObject( params ) ){
params = {};
}

4
sanitiser/_input.js

@ -1,3 +1,5 @@
var isObject = require('is-object');
// validate inputs, convert types and apply defaults
function sanitize( req ){
@ -6,7 +8,7 @@ function sanitize( req ){
var delim = ',';
// ensure the input params are a valid object
if( Object.prototype.toString.call( params ) !== '[object Object]' ){
if( !isObject( params ) ){
params = {};
}

2
sanitiser/_layers.js

@ -33,7 +33,7 @@ function sanitize( req ){
if( -1 === alias_indeces.indexOf( layers[x] ) ){
return {
'error': true,
'message': 'invalid param \'layer\': must be one or more of ' + alias_indeces.join(',')
'message': 'invalid param \'layers\': must be one or more of ' + alias_indeces.join(',')
};
}
}

4
sanitiser/_size.js

@ -1,3 +1,5 @@
var isObject = require('is-object');
// validate inputs, convert types and apply defaults
function sanitize( req, default_size){
@ -7,7 +9,7 @@ function sanitize( req, default_size){
default_size = default_size || 10;
// ensure the input params are a valid object
if( Object.prototype.toString.call( params ) !== '[object Object]' ){
if( !isObject( params ) ){
params = {};
}

3
sanitiser/coarse.js

@ -9,7 +9,8 @@ var logger = require('../src/logger'),
var layers = require('../sanitiser/_layers');
return layers(req);
},
latlonzoom: require('../sanitiser/_geo')
latlonzoom: require('../sanitiser/_geo'),
details: require('../sanitiser/_details')
};
var sanitize = function(req, cb) { _sanitize(req, sanitizers, cb); };

3
sanitiser/doc.js

@ -2,7 +2,8 @@
var logger = require('../src/logger'),
_sanitize = require('../sanitiser/_sanitize'),
sanitizers = {
id: require('../sanitiser/_id')
id: require('../sanitiser/_id'),
details: require('../sanitiser/_details')
};
var sanitize = function(req, cb) { _sanitize(req, sanitizers, cb); };

8
sanitiser/reverse.js

@ -1,15 +1,19 @@
var logger = require('../src/logger'),
_sanitize = require('../sanitiser/_sanitize'),
var _sanitize = require('../sanitiser/_sanitize'),
sanitiser = {
latlonzoom: function( req ) {
var geo = require('../sanitiser/_geo');
return geo(req, true);
},
layers: require('../sanitiser/_layers'),
details: require('../sanitiser/_details'),
size: function( req ) {
var size = require('../sanitiser/_size');
return size(req, 1);
},
categories: function ( req ) {
var categories = require('../sanitiser/_categories');
return categories(req);
}
};

1
sanitiser/search.js

@ -5,6 +5,7 @@ var logger = require('../src/logger'),
input: require('../sanitiser/_input'),
size: require('../sanitiser/_size'),
layers: require('../sanitiser/_layers'),
details: require('../sanitiser/_details'),
latlonzoom: require('../sanitiser/_geo')
};

1
sanitiser/suggest.js

@ -5,6 +5,7 @@ var logger = require('../src/logger'),
input: require('../sanitiser/_input'),
size: require('../sanitiser/_size'),
layers: require('../sanitiser/_layers'),
details: require('../sanitiser/_details'),
latlonzoom: function( req ) {
var geo = require('../sanitiser/_geo');
return geo(req, true);

7
test/ciao/index.coffee

@ -6,7 +6,7 @@ path: '/'
response.statusCode.should.equal 200
#? content-type header correctly set
response.should.have.header 'Content-Type','application/json; charset=utf-8'
response.should.have.header 'Content-Type','text/html; charset=utf-8'
#? charset header correctly set
response.should.have.header 'Charset','utf8'
@ -20,8 +20,3 @@ 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.exist json
should.exist json.name
should.exist json.version

10
test/ciao/jsonp.coffee

@ -1,10 +0,0 @@
#> 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(';

54
test/unit/controller/doc.js

@ -17,6 +17,52 @@ 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: {
id: 'myid1',
layer: 'mytype1',
text: 'test name1, city1, state1'
}
}, {
type: 'Feature',
geometry: {
type: 'Point',
coordinates: [ -51.5, 100.2 ]
},
properties: {
id: 'myid2',
layer: 'mytype2',
text: 'test name2, city2, state2'
}
}];
test('functional success', function(t) {
var backend = mockBackend( 'client/mget/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 );
});
var detailed_expectation = [{
type: 'Feature',
geometry: {
type: 'Point',
@ -47,8 +93,8 @@ module.exports.tests.functional_success = function(test, common) {
text: 'test name2, city2, state2'
}
}];
test('functional success', function(t) {
test('functional success (with details)', function(t) {
var backend = mockBackend( 'client/mget/ok/1', function( cmd ){
t.deepEqual(cmd, { body: { docs: [ { _id: 123, _index: 'pelias', _type: 'a' } ] } }, 'correct backend command');
});
@ -63,11 +109,11 @@ module.exports.tests.functional_success = function(test, common) {
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.deepEqual(json.features, detailed_expectation, 'values correctly mapped along with details');
t.end();
}
};
controller( { clean: { ids: [ {'id' : 123, 'type': 'a' } ] } }, res );
controller( { clean: { ids: [ {'id' : 123, 'type': 'a' } ], details: true } }, res );
});
};

44
test/unit/controller/index.js

@ -11,9 +11,15 @@ module.exports.tests.interface = function(test, common) {
});
};
module.exports.tests.info = function(test, common) {
test('returns server info', function(t) {
module.exports.tests.info_json = function(test, common) {
test('returns server info in json', function(t) {
var controller = setup();
var req = {
accepts: function (format) {
t.equal(format, 'html', 'check for Accepts:html');
return false;
}
};
var res = { json: function( json ){
t.equal(typeof json, 'object', 'returns json');
t.equal(typeof json.name, 'string', 'name');
@ -21,7 +27,39 @@ module.exports.tests.info = function(test, common) {
t.equal(typeof json.version.number, 'string', 'version number');
t.end();
}};
controller( null, res );
controller( req, res );
});
};
module.exports.tests.info_html = function(test, common) {
test('returns server info in html', function(t) {
var style = '<style>html{font-family:monospace}</style>';
var mockText = 'this text should show up in the html content';
var fsMock = {
readFileSync: function (path, format) {
t.equal(path, './DOCS.md', 'open DOCS.md file');
t.equal(format, 'utf8', 'file format');
return mockText;
}
};
var proxyquire = require('proxyquire');
var setup = proxyquire('../../../controller/index', { 'fs': fsMock });
var controller = setup();
var req = {
accepts: function () {
return true;
}
};
var res = { send: function( content ){
t.equal(typeof content, 'string', 'returns string');
t.assert(content.indexOf(style) === 0, 'style set');
t.assert(content.indexOf(mockText) !== -1, 'file content added');
t.end();
}};
controller( req, res );
});
};

54
test/unit/controller/search.js

@ -18,6 +18,52 @@ 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: [ -50.5, 100.1 ]
},
properties: {
id: 'myid1',
layer: 'mytype1',
text: 'test name1, city1, state1'
}
}, {
type: 'Feature',
geometry: {
type: 'Point',
coordinates: [ -51.5, 100.2 ]
},
properties: {
id: 'myid2',
layer: 'mytype2',
text: 'test name2, city2, state2'
}
}];
test('functional success', function(t) {
var backend = mockBackend( 'client/search/ok/1', function( cmd ){
t.deepEqual(cmd, { body: { a: 'b' }, index: 'pelias', searchType: 'dfs_query_then_fetch' }, '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 );
});
var detailed_expectation = [{
type: 'Feature',
geometry: {
type: 'Point',
@ -49,9 +95,9 @@ module.exports.tests.functional_success = function(test, common) {
}
}];
test('functional success', function(t) {
test('functional success (with details)', function(t) {
var backend = mockBackend( 'client/search/ok/1', function( cmd ){
t.deepEqual(cmd, { body: { a: 'b' }, index: 'pelias', searchType: 'dfs_query_then_fetch' }, 'correct backend command');
t.deepEqual(cmd, { body: { a: 'b', details: true }, index: 'pelias', searchType: 'dfs_query_then_fetch' }, 'correct backend command');
});
var controller = setup( backend, mockQuery() );
var res = {
@ -64,11 +110,11 @@ module.exports.tests.functional_success = function(test, common) {
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.deepEqual(json.features, detailed_expectation, 'values with details correctly mapped');
t.end();
}
};
controller( { clean: { a: 'b' } }, res );
controller( { clean: { a: 'b', details: true } }, res );
});
};

64
test/unit/controller/suggest.js

@ -18,6 +18,62 @@ module.exports.tests.functional_success = function(test, common) {
// expected geojson features for 'client/mget/ok/1' fixture
var expected = [{
type: 'Feature',
geometry: {
type: 'Point',
coordinates: [ -50.5, 100.1 ]
},
properties: {
id: 'myid1',
layer: 'mytype1',
text: 'test name1, city1, state1'
}
}, {
type: 'Feature',
geometry: {
type: 'Point',
coordinates: [ -51.5, 100.2 ]
},
properties: {
id: 'myid2',
layer: 'mytype2',
text: 'test name2, city2, state2'
}
}];
test('functional success', function(t) {
var backend = mockBackend( 'client/suggest/ok/1', function( cmd ){
// the backend executes suggest (vanilla and admin-only) and mget, so we check them all based on cmd
if( cmd.body.docs ){
t.deepEqual(cmd, {
body: { docs: [
{ _id: 'mockid1', _index: 'pelias', _type: 'mocktype' },
{ _id: 'mockid2', _index: 'pelias', _type: 'mocktype' }
]}
}, 'correct mget command');
} else {
t.deepEqual(cmd, { body: { input: 'b' }, index: 'pelias' }, 'correct suggest 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: { input: 'b' } }, res );
});
var detailed_expectation = [{
type: 'Feature',
geometry: {
type: 'Point',
@ -49,7 +105,7 @@ module.exports.tests.functional_success = function(test, common) {
}
}];
test('functional success', function(t) {
test('functional success (with details)', function(t) {
var backend = mockBackend( 'client/suggest/ok/1', function( cmd ){
// the backend executes suggest (vanilla and admin-only) and mget, so we check them all based on cmd
if( cmd.body.docs ){
@ -60,7 +116,7 @@ module.exports.tests.functional_success = function(test, common) {
]}
}, 'correct mget command');
} else {
t.deepEqual(cmd, { body: { input: 'b' }, index: 'pelias' }, 'correct suggest command');
t.deepEqual(cmd, { body: { input: 'b', details: true }, index: 'pelias' }, 'correct suggest command');
}
});
var controller = setup( backend, mockQuery() );
@ -74,11 +130,11 @@ module.exports.tests.functional_success = function(test, common) {
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.deepEqual(json.features, detailed_expectation, 'values with details correctly mapped');
t.end();
}
};
controller( { clean: { input: 'b' } }, res );
controller( { clean: { input: 'b', details: true } }, res );
});
};

84
test/unit/helper/geojsonify.js

@ -6,7 +6,7 @@ module.exports.tests = {};
module.exports.tests.interface = function(test, common) {
test('valid interface .search()', function(t) {
t.equal(typeof geojsonify.search, 'function', 'search is a function');
t.equal(geojsonify.search.length, 1, 'accepts x arguments');
t.equal(geojsonify.search.length, 2, 'accepts x arguments');
t.end();
});
};
@ -178,11 +178,89 @@ module.exports.tests.search = function(test, common) {
]
};
test('geojsonify.search()', function(t) {
var truthy_params = [true, 1];
test('geojsonify.search(doc, true) with details', function(t) {
var json = geojsonify.search( input, { details: true } );
t.deepEqual(json, expected, 'all docs (with details) mapped');
t.end();
});
truthy_params.forEach(function(details) {
test('geojsonify.search(doc, '+ details +') with details', function(t) {
var json = geojsonify.search( input, { details: details } );
t.deepEqual(json, expected, 'all docs (with details) mapped');
t.end();
});
});
var no_details_expected = {
'type': 'FeatureCollection',
'bbox': [ -73.985656, 40.748432, -0.101795, 51.5337144 ],
'features': [
{
'type': 'Feature',
'geometry': {
'type': 'Point',
'coordinates': [
-0.1069716,
51.5337144
]
},
'properties': {
'id': 'id1',
'layer': 'type1',
'text': '\'Round Midnight Jazz and Blues Bar, test3, Angel'
}
},
{
'type': 'Feature',
'geometry': {
'type': 'Point',
'coordinates': [
-0.101795,
51.517806
]
},
'properties': {
'id': 'id2',
'layer': 'type2',
'text': 'Blues Cafe, test3, Smithfield'
}
},
{
'type': 'Feature',
'geometry': {
'type': 'Point',
'coordinates': [
-73.985656,
40.748432
]
},
'properties': {
'id': '34633854',
'layer': 'osmway',
'text': 'Empire State Building, Manhattan, NY'
}
}
]
};
test('geojsonify.search(doc) with no details (default)', function(t) {
var json = geojsonify.search( input );
t.deepEqual(json, expected, 'all docs mapped');
t.deepEqual(json, no_details_expected, 'all docs (with no details) mapped');
t.end();
});
var falsy_params = [false, undefined, null, 0, -1, 123, 'abc'];
falsy_params.forEach(function(details) {
test('geojsonify.search(doc, '+ details +') with no details', function(t) {
var json = geojsonify.search( input, { details: details } );
t.deepEqual(json, no_details_expected, 'all docs (with no details) mapped');
t.end();
});
});
};
module.exports.all = function (tape, common) {

77
test/unit/query/reverse.js

@ -3,6 +3,8 @@ var generate = require('../../../query/reverse');
var admin_boost = 'admin_boost';
var population = 'population';
var popularity = 'popularity';
var category = 'category';
var category_weights = require('../../../helper/category_weights');
var weights = require('pelias-suggester-pipeline').weights;
module.exports.tests = {};
@ -37,6 +39,16 @@ var sort = [
'order': 'desc'
}
},
{
'_script': {
'params': {
'category_weights': category_weights
},
'file': category,
'type': 'number',
'order': 'desc'
}
},
{
'_script': {
'params': {
@ -111,6 +123,71 @@ module.exports.tests.query = function(test, common) {
});
t.end();
});
test('valid query with categories', function(t) {
var params = { lat: 29.49136, lon: -82.50622, categories: ['food', 'education', 'entertainment'] };
var query = generate(params);
var expected = {
'query': {
'filtered': {
'query': {
'match_all': {}
},
'filter': {
'bool': {
'must': [
{
'geo_distance': {
'distance': '50km',
'distance_type': 'plane',
'optimize_bbox': 'indexed',
'_cache': true,
'center_point': {
'lat': '29.49',
'lon': '-82.51'
}
}
},
{
'terms': {
'category': params.categories
}
}
]
}
}
}
},
'sort': [
'_score',
{
'_geo_distance': {
'center_point': {
'lat': 29.49136,
'lon': -82.50622
},
'order': 'asc',
'unit': 'km'
}
}
].concat(sort.slice(1)),
'size': 1,
'track_scores': true
};
t.deepEqual(query, expected, 'valid reverse query with categories');
// test different sizes
var sizes = [1,2,10,undefined,null];
sizes.forEach( function(size) {
params.size = size;
query = generate(params);
expected.size = size ? size : 1;
t.deepEqual(query, expected, 'valid reverse query for size: '+ size);
});
t.end();
});
};
module.exports.all = function (tape, common) {

12
test/unit/query/search.js

@ -3,6 +3,8 @@ var generate = require('../../../query/search');
var admin_boost = 'admin_boost';
var population = 'population';
var popularity = 'popularity';
var category = 'category';
var category_weights = require('../../../helper/category_weights');
var weights = require('pelias-suggester-pipeline').weights;
module.exports.tests = {};
@ -37,6 +39,16 @@ var sort = [
'order': 'desc'
}
},
{
'_script': {
'params': {
'category_weights': category_weights
},
'file': category,
'type': 'number',
'order': 'desc'
}
},
{
'_script': {
'params': {

12
test/unit/query/sort.js

@ -3,6 +3,8 @@ var generate = require('../../../query/sort');
var admin_boost = 'admin_boost';
var population = 'population';
var popularity = 'popularity';
var category = 'category';
var category_weights = require('../../../helper/category_weights');
var weights = require('pelias-suggester-pipeline').weights;
module.exports.tests = {};
@ -36,6 +38,16 @@ var expected = [
'type': 'number',
'order': 'desc'
}
},
{
'_script': {
'params': {
'category_weights': category_weights
},
'file': category,
'type': 'number',
'order': 'desc'
}
},
{
'_script': {

3
test/unit/sanitiser/coarse.js

@ -53,7 +53,8 @@ module.exports.tests.middleware_success = function(test, common) {
size: 10,
layers: [ 'admin0', 'admin1', 'admin2', 'neighborhood', 'locality', 'local_admin' ],
lat: 0,
lon: 0
lon: 0,
details: true
};
t.equal(message, undefined, 'no error message set');
t.deepEqual(req.clean, defaultClean);

46
test/unit/sanitiser/doc.js

@ -10,7 +10,7 @@ var doc = require('../../../sanitiser/doc'),
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' } ] },
defaultClean = { ids: [ { id: '123', type: 'geoname' } ], details: true },
sanitize = function(query, cb) { _sanitize({'query':query}, cb); },
inputs = {
valid: [ 'geoname:1', 'osmnode:2', 'admin0:53', 'osmway:44', 'geoname:5' ],
@ -56,7 +56,7 @@ module.exports.tests.sanitize_id = function(test, common) {
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] } ] };
var expected = { ids: [ { id: input_parts[1], type: input_parts[0] } ], details: true };
sanitize({ id: input }, function( err, clean ){
t.equal(err, undefined, 'no error (' + input + ')' );
t.deepEqual(clean, expected, 'clean set correctly (' + input + ')');
@ -94,6 +94,7 @@ module.exports.tests.sanitize_ids = function(test, common) {
var input_parts = input.split(delimiter);
expected.ids.push({ id: input_parts[1], type: input_parts[0] });
});
expected.details = true;
sanitize({ id: inputs.valid }, function( err, clean ){
t.equal(err, undefined, 'no error' );
t.deepEqual(clean, expected, 'clean set correctly');
@ -102,8 +103,47 @@ module.exports.tests.sanitize_ids = function(test, common) {
});
};
module.exports.tests.sanitize_details = function(test, common) {
var invalid_values = [null, -1, 123, NaN, 'abc'];
invalid_values.forEach(function(details) {
test('invalid details param ' + details, function(t) {
sanitize({ id:'geoname:123', details: details }, function( err, clean ){
t.equal(clean.details, false, 'default details set (to false)');
t.end();
});
});
});
var valid_values = ['true', true, 1];
valid_values.forEach(function(details) {
test('valid details param ' + details, function(t) {
sanitize({ id:'geoname:123', details: details }, function( err, clean ){
t.equal(clean.details, true, 'details set to true');
t.end();
});
});
});
var valid_false_values = ['false', false, 0];
valid_false_values.forEach(function(details) {
test('test setting false explicitly ' + details, function(t) {
sanitize({ id:'geoname:123', details: details }, function( err, clean ){
t.equal(clean.details, false, 'details set to false');
t.end();
});
});
});
test('test default behavior', function(t) {
sanitize({ id:'geoname:123' }, function( err, clean ){
t.equal(clean.details, true, 'details set to true');
t.end();
});
});
};
module.exports.tests.de_dupe = function(test, common) {
var expected = { ids: [ { id: '1', type: 'geoname' }, { id: '2', type: 'osmnode' } ] };
var expected = { ids: [ { id: '1', type: 'geoname' }, { id: '2', type: 'osmnode' } ], details: true };
test('duplicate ids', function(t) {
sanitize( { id: ['geoname:1', 'osmnode:2', 'geoname:1'] }, function( err, clean ){
t.equal(err, undefined, 'no error' );

84
test/unit/sanitiser/reverse.js

@ -8,7 +8,9 @@ var suggest = require('../../../sanitiser/reverse'),
layers: [ 'geoname', 'osmnode', 'osmway', 'admin0', 'admin1', 'admin2', 'neighborhood',
'locality', 'local_admin', 'osmaddress', 'openaddresses' ],
lon: 0,
size: 1
size: 1,
details: true,
categories: []
},
sanitize = function(query, cb) { _sanitize({'query':query}, cb); };
@ -124,6 +126,45 @@ module.exports.tests.sanitize_size = function(test, common) {
});
};
module.exports.tests.sanitize_details = function(test, common) {
var invalid_values = [null, -1, 123, NaN, 'abc'];
invalid_values.forEach(function(details) {
test('invalid details param ' + details, function(t) {
sanitize({ input: 'test', lat: 0, lon: 0, details: details }, function( err, clean ){
t.equal(clean.details, false, 'details set to false');
t.end();
});
});
});
var valid_values = [true, 'true', 1];
valid_values.forEach(function(details) {
test('valid details param ' + details, function(t) {
sanitize({ input: 'test', lat: 0, lon: 0, details: details }, function( err, clean ){
t.equal(clean.details, true, 'details set to true');
t.end();
});
});
});
test('test default behavior', function(t) {
sanitize({ input: 'test', lat: 0, lon: 0 }, function( err, clean ){
t.equal(clean.details, true, 'details set to true');
t.end();
});
});
var valid_false_values = ['false', false, 0];
valid_false_values.forEach(function(details) {
test('test setting false explicitly ' + details, function(t) {
sanitize({ input: 'test', lat: 0, lon: 0, details: details }, function( err, clean ){
t.equal(clean.details, false, 'details set to false');
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 ){
@ -133,7 +174,7 @@ 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 ';
var msg = 'invalid param \'layers\': must be one or more of ';
t.true(err.match(msg), 'invalid layer requested');
t.true(err.length > msg.length, 'invalid error message');
t.end();
@ -200,6 +241,45 @@ module.exports.tests.sanitize_layers = function(test, common) {
});
};
module.exports.tests.sanitize_categories = function(test, common) {
var queryParams = { input: 'test', lat: 0, lon: 0 };
test('unspecified', function(t) {
queryParams.categories = undefined;
sanitize(queryParams, function( err, clean ){
t.deepEqual(clean.categories, defaultClean.categories, 'default to empty categories array');
t.end();
});
});
test('single category', function(t) {
queryParams.categories = 'food';
sanitize(queryParams, function( err, clean ){
t.deepEqual(clean.categories, ['food'], 'category set');
t.end();
});
});
test('multiple categories', function(t) {
queryParams.categories = 'food,education,nightlife';
sanitize(queryParams, function( err, clean ){
t.deepEqual(clean.categories, ['food', 'education', 'nightlife'], 'categories set');
t.end();
});
});
test('whitespace and empty strings', function(t) {
queryParams.categories = 'food, , nightlife ,';
sanitize(queryParams, function( err, clean ){
t.deepEqual(clean.categories, ['food', 'nightlife'], 'categories set');
t.end();
});
});
test('all empty strings', function(t) {
queryParams.categories = ', , ,';
sanitize(queryParams, function( err, clean ){
t.deepEqual(clean.categories, defaultClean.categories, 'empty strings filtered out');
t.end();
});
});
};
module.exports.tests.middleware_failure = function(test, common) {
test('middleware failure', function(t) {
var res = { status: function( code ){

60
test/unit/sanitiser/search.js

@ -7,7 +7,8 @@ var search = require('../../../sanitiser/search'),
defaultClean = { input: 'test',
layers: [ 'geoname', 'osmnode', 'osmway', 'admin0', 'admin1', 'admin2', 'neighborhood',
'locality', 'local_admin', 'osmaddress', 'openaddresses' ],
size: 10
size: 10,
details: true
},
sanitize = function(query, cb) { _sanitize({'query':query}, cb); };
@ -167,9 +168,9 @@ module.exports.tests.sanitize_optional_geo = function(test, common) {
module.exports.tests.sanitize_bbox = function(test, common) {
var bboxes = {
invalid_coordinates: [
'90,-181,-180,34', // invalid top_right lon, bottom_left lat
'91,-170,45,-181', // invalid top_right lat, bottom_left lon
'91,-181,-91,181', // invalid top_right lat/lon, bottom_left lat/lon
'-181,90,34,-180', // invalid top_right lon, bottom_left lat
'-170,91,-181,45', // invalid top_right lat, bottom_left lon
'-181,91,181,-91', // invalid top_right lon/lat, bottom_left lon/lat
'91, -181,-91,181',// invalid - spaces between coordinates
],
invalid: [
@ -179,7 +180,7 @@ module.exports.tests.sanitize_bbox = function(test, common) {
'' // invalid - empty param
],
valid: [
'90,-179,-80,34', // valid top_right lat/lon, bottom_left lat/lon
'-179,90,34,-80', // valid top_right lon/lat, bottom_left lon/lat
'0,0,0,0' // valid top_right lat/lon, bottom_left lat/lon
]
@ -211,10 +212,10 @@ module.exports.tests.sanitize_bbox = function(test, common) {
return parseInt(i);
});
expected.bbox = {
top : Math.max(bboxArray[0], bboxArray[2]),
right : Math.max(bboxArray[1], bboxArray[3]),
bottom: Math.min(bboxArray[0], bboxArray[2]),
left : Math.min(bboxArray[1], bboxArray[3])
right: Math.max(bboxArray[0], bboxArray[2]),
top: Math.max(bboxArray[1], bboxArray[3]),
left: Math.min(bboxArray[0], bboxArray[2]),
bottom: Math.min(bboxArray[1], bboxArray[3])
};
t.equal(err, undefined, 'no error');
t.deepEqual(clean, expected, 'clean set correctly (' + bbox + ')');
@ -266,6 +267,45 @@ module.exports.tests.sanitize_size = function(test, common) {
});
};
module.exports.tests.sanitize_details = function(test, common) {
var invalid_values = [null, -1, 123, NaN, 'abc'];
invalid_values.forEach(function(details) {
test('invalid details param ' + details, function(t) {
sanitize({ input: 'test', lat: 0, lon: 0, details: details }, function( err, clean ){
t.equal(clean.details, false, 'default details set (to false)');
t.end();
});
});
});
var valid_values = ['true', true, 1];
valid_values.forEach(function(details) {
test('valid details param ' + details, function(t) {
sanitize({ input: 'test', lat: 0, lon: 0, details: details }, function( err, clean ){
t.equal(clean.details, true, 'details set to true');
t.end();
});
});
});
var valid_false_values = ['false', false, 0];
valid_false_values.forEach(function(details) {
test('test setting false explicitly ' + details, function(t) {
sanitize({ input: 'test', lat: 0, lon: 0, details: details }, function( err, clean ){
t.equal(clean.details, false, 'details set to false');
t.end();
});
});
});
test('test default behavior', function(t) {
sanitize({ input: 'test', lat: 0, lon: 0 }, function( err, clean ){
t.equal(clean.details, true, 'details set to true');
t.end();
});
});
};
module.exports.tests.sanitize_layers = function(test, common) {
test('unspecified', function(t) {
sanitize({ layers: undefined, input: 'test' }, function( err, clean ){
@ -275,7 +315,7 @@ module.exports.tests.sanitize_layers = function(test, common) {
});
test('invalid layer', function(t) {
sanitize({ layers: 'test_layer', input: 'test' }, function( err, clean ){
var msg = 'invalid param \'layer\': must be one or more of ';
var msg = 'invalid param \'layers\': must be one or more of ';
t.true(err.match(msg), 'invalid layer requested');
t.true(err.length > msg.length, 'invalid error message');
t.end();

60
test/unit/sanitiser/suggest.js

@ -9,7 +9,8 @@ var suggest = require('../../../sanitiser/suggest'),
layers: [ 'geoname', 'osmnode', 'osmway', 'admin0', 'admin1', 'admin2', 'neighborhood',
'locality', 'local_admin', 'osmaddress', 'openaddresses' ],
lon: 0,
size: 10
size: 10,
details: true
},
sanitize = function(query, cb) { _sanitize({'query':query}, cb); };
@ -136,9 +137,9 @@ module.exports.tests.sanitize_lon = function(test, common) {
module.exports.tests.sanitize_bbox = function(test, common) {
var bboxes = {
invalid_coordinates: [
'90,-181,-180,34', // invalid top_right lon, bottom_left lat
'91,-170,45,-181', // invalid top_right lat, bottom_left lon
'91,-181,-91,181', // invalid top_right lat/lon, bottom_left lat/lon
'-181,90,34,-180', // invalid top_right lon, bottom_left lat
'-170,91,-181,45', // invalid top_right lat, bottom_left lon
'-181,91,181,-91', // invalid top_right lon/lat, bottom_left lon/lat
'91, -181,-91,181',// invalid - spaces between coordinates
],
invalid: [
@ -148,7 +149,7 @@ module.exports.tests.sanitize_bbox = function(test, common) {
'' // invalid - empty param
],
valid: [
'90,-179,-80,34', // valid top_right lat/lon, bottom_left lat/lon
'-179,90,34,-80', // valid top_right lon/lat, bottom_left lon/lat
'0,0,0,0' // valid top_right lat/lon, bottom_left lat/lon
]
@ -180,10 +181,10 @@ module.exports.tests.sanitize_bbox = function(test, common) {
return parseInt(i);
});
expected.bbox = {
top : Math.max(bboxArray[0], bboxArray[2]),
right : Math.max(bboxArray[1], bboxArray[3]),
bottom: Math.min(bboxArray[0], bboxArray[2]),
left : Math.min(bboxArray[1], bboxArray[3])
right: Math.max(bboxArray[0], bboxArray[2]),
top: Math.max(bboxArray[1], bboxArray[3]),
left: Math.min(bboxArray[0], bboxArray[2]),
bottom: Math.min(bboxArray[1], bboxArray[3])
};
t.equal(err, undefined, 'no error');
t.deepEqual(clean, expected, 'clean set correctly (' + bbox + ')');
@ -235,6 +236,45 @@ module.exports.tests.sanitize_size = function(test, common) {
});
};
module.exports.tests.sanitize_details = function(test, common) {
var invalid_values = [null, -1, 123, NaN, 'abc'];
invalid_values.forEach(function(details) {
test('invalid details param ' + details, function(t) {
sanitize({ input: 'test', lat: 0, lon: 0, details: details }, function( err, clean ){
t.equal(clean.details, false, 'default details set (to false)');
t.end();
});
});
});
var valid_values = ['true', true, 1];
valid_values.forEach(function(details) {
test('valid details param ' + details, function(t) {
sanitize({ input: 'test', lat: 0, lon: 0, details: details }, function( err, clean ){
t.equal(clean.details, true, 'details set to true');
t.end();
});
});
});
var valid_false_values = ['false', false, 0];
valid_false_values.forEach(function(details) {
test('test setting false explicitly ' + details, function(t) {
sanitize({ input: 'test', lat: 0, lon: 0, details: details }, function( err, clean ){
t.equal(clean.details, false, 'details set to false');
t.end();
});
});
});
test('test default behavior', function(t) {
sanitize({ input: 'test', lat: 0, lon: 0 }, function( err, clean ){
t.equal(clean.details, true, 'details set to true');
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 ){
@ -244,7 +284,7 @@ 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 ';
var msg = 'invalid param \'layers\': must be one or more of ';
t.true(err.match(msg), 'invalid layer requested');
t.true(err.length > msg.length, 'invalid error message');
t.end();

Loading…
Cancel
Save